Студопедия
Случайная страница | ТОМ-1 | ТОМ-2 | ТОМ-3
АрхитектураБиологияГеографияДругоеИностранные языки
ИнформатикаИсторияКультураЛитератураМатематика
МедицинаМеханикаОбразованиеОхрана трудаПедагогика
ПолитикаПравоПрограммированиеПсихологияРелигия
СоциологияСпортСтроительствоФизикаФилософия
ФинансыХимияЭкологияЭкономикаЭлектроника

Программирование в Visual С++ 2005 18 страница



Эта программа просто перечисляет переданные ей в командной строке аргументы.

 


У вас есть два варианта ввода аргументов командной строки. После того, как при-
мер программы собран в IDE, можно открыть командное окно в папке, содержащей
файл.ехе, и затем ввести имя программы с последующими аргументами командной
строки. Альтернативно вы можете специфицировать аргументы командной строки в
IDE, прежде чем запускать программу. Для этого просто откройте окно свойств проек-
та, выбрав Projects Properties (Проект^Свойства) в главном меню, и затем разверни-
те дерево Configuration Properties (Свойства конфигурации) в левой панели, щелкнув
на значке "плюс" (+). Затем щелкните на папке Debugging (Отладка), чтобы увидеть
на правой панели место, куда можно ввести аргументы командной строки.

Вот типичный пример запуска этого примера в командном окне:

 



Структурная организация программ 269

 

Описание полученных результатов

Во-первых, программа выводит значение argc, а затем значения каждого аргумен-
та из массива argv в цикле for. Можно воспользоваться тем фактом, что последний
элемент argv равен null, тогда код вывода значений аргументов будет таким:

 

 


Выполнение цикла завершится по достижении элемента массива argv [argc], ко-
торый равен null.

Прием функцией переменного количества аргументов

Можно определить функцию так, что она сможет принимать любое количество
переданных ей аргументов. Переменное число аргументов функции задается в ее
определении посредством многоточия в конце списка параметров, например:

При этом должен быть указан как минимум один обычный параметр, но можно и
больше. Многоточия всегда помещаются в конце списка параметров.

Очевидно, что в переменном списке параметров нет информации о числе и типах
аргументов, поэтому ваш код должен каким-то образом определять, что ему передано
при вызове функции. Стандартная библиотека "родного" С++ определяет в заголовоч-
ном файле stdarg.h макросы vastart, vaarg и vaend, которые могут в этом по-
мочь. Легче всего продемонстрировать их применение на примере.

Эта программа использует функцию, которая суммирует значения переданных ей
аргументов.


 


 
 



Этот пример сгенерирует следующий вывод:

Описание полученных результатов

Функция main () вызывает функцию sum () в двух операторах вывода, в первом
случае с шестью аргументами, а во втором — с девятью.



Функция sum () принимает один нормальный параметр типа int, который пред-
ставляет счетчик числа аргументов, следующих за ним. Многоточие в списке параме-
тров функции указывает на то, что ей может быть передано произвольное количество
аргументов. В основном, у вас есть два способа определения того, сколько аргументов
было передано при вызове функции — можно потребовать явной передачи числа ар-
гументов фиксированным параметром, как это сделано в случае функции sum (), или
же можно передавать в последнем аргументе специальный маркер, который можно
проверить и распознать.

Для начала обработки переменного списка аргументов объявляется указатель типа
va_list:

Тип valist определен в заголовочном файле stdarg.h, и этот указатель исполь-
зуется для указания на каждый аргумент в списке по порядку.

Макрос va_start служит для инициализации argptr, так что он указывает на
первый аргумент в списке:

Второй аргумент этого макроса — имя фиксированного параметра, который пред-
шествует многоточию в списке параметров; он используется макросом для определе-
ния того, где находится первый аргумент из переменного списка.

Значения аргументов из списка извлекаются в цикле for:

 


Макрос va_arg возвращает значение аргумента, находящегося по адресу arg_ptr, и
увеличивает его с тем, чтобы он указывал на следующее по порядку значение. Второй
аргумент макроса vaarg — тип аргумента, и он определяет интерпретацию значе-
ния, которое вы получаете, а также размер инкремента argptr, потому что если он
будет неправильным, вы получите хаос; программа в этом случае, возможно, будет вы-
полняться, но полученные значения окажутся мусором, и argptr, увеличенный не-
правильно, только породит еще больше мусора.

Когда завершается извлечение значений аргументов, с помощью следующего опе-
ратора argptr сбрасывается:


Структурная организация программ 271

 

Макрос vaend устанавливает указатель типа va_list, который передается ему в
качестве аргумента, равным null. Это нужно делать всегда, потому что после обра-
ботки arg_ptr указывает на адрес, который не содержит корректных данных.

 

Возврат значений функциями

Все примеры функций, который вы создавали, возвращали единственное значе-
ние. Можно ли вернуть что-то другое, отличное от единственного значения? Вообще-
то нет, но как я уже говорил, возвращаемое единственное значение не обязательно
должно быть числовым; оно может быть адресом, что является ключом к возврату лю-
бого объема данных. Вы просто используете указатель. К сожалению, здесь начинают-
ся ловушки, поэтому вам нужно быть заранее готовым к некоторым "приключениям".

Возврат указателя

Вернуть значение указателя легко. Значение указателя — это просто адрес, поэто-
му если вы хотите вернуть адрес некоторой переменной value, то можете просто за-
писать так:

До тех пор, пока заголовок функции и ее прототип правильно и согласованно
указывают тип возврата, у вас нет никаких проблем — по крайней мере, никаких ви-
димых проблем. Предполагая, что переменная value имеет тип double, прототип
функции по имени treble, которая может содержать приведенный выше оператор
return, должен быть таким:

Список параметров я указал произвольным образом.

Давайте рассмотрим функцию, возвращающую указатель. Заранее должен предупре-
дить — эта функция работать не будет, но она нам понадобиться в качестве наглядного
пособия. Предположим, что вам нужна функция, которая возвращает указатель на ме-
стоположение в памяти, где находится значение ее аргумента, умноженное на 3. Первая
попытка реализовать такую функцию могла бы выглядеть следующим образом:

 


Для того чтобы посмотреть, что получится, можно написать небольшую тестовую
программу (напомню, что функция treble не будет работать, как ожидается).

 

 




При компиляции сразу возникает подсказка, что все не так, как хотелось бы — в
виде предупреждающего сообщения компилятора:

Вывод, который я получил при запуске этой программы, был таким:

Описание полученных результатов (или почему это не работает)

Функция main () вызывает treble () и сохраняет возвращенный адрес в указателе
ptr, который по идее должен указывать на утроенное значение аргумента num. Затем
отображается результат простого умножения num на три, за которым следует значе-
ние адреса, возвращенного функцией.

Ясно, что вторая строка вывода не показывает корректного значения — 15, но в
чем же ошибка? Вообще-то это не секрет, потому что компилятор ясно предупредил
о проблеме. Ошибка возникает потому, что переменная result в функции treble ()
создается, когда функция начинает выполнение, а уничтожается при выходе из функ-
ции. Поэтому память, на которую указывает указатель, уже не содержит исходного
корректного значения. Память, ранее выделенная result, становится доступной для
других целей, и здесь она как раз использована для чего-то другого.

Железное правило возврата адресов

Существует абсолютное железное правило относительно возвращаемых адресов:

Никогда не возвращать из функции адрес локальной автоматической переменной.

Очевидно, что нельзя применять функцию, которая не работает, но как же это ис-
править? Можно использовать ссылочный параметр и модифицировать в функции
его исходное значение, но это не совсем то, что нужно. Вы пытаетесь вернуть указа-
тель на нечто полезное, потому что в конечном итоге, вам может понадобиться воз-
вращать нечто более сложное, чем отдельный элемент данных. Ответ заключается в
динамическом выделении памяти (вы видели это в действии в предыдущей главе).


Структурная организация программ 273

 

С помощью операции new вы создаете новую переменную в свободном хранили-
ще, которая продолжает существовать до тех пор, пока не будет уничтожена операци-
ей delete или пока не завершится программа. С таким подходом функция выглядит
так:

 


Вместо объявления result типа double теперь эта переменная объявлена с ти-
пом double* и ей присваивается адрес, возвращенный операцией new. Поскольку
результат — указатель, остальная часть функции изменена соответствующим образом,
и адрес, записанный в result, в конечном итоге возвращается вызывающей програм-
ме. Можете проверить эту версию, заменив ею функцию из предыдущего примера.

Необходимо помнить, что при динамическом распределении памяти в функции
"родного" С++, вроде этой, при каждом вызове выделяется новый фрагмент памяти.
Ответственность за освобождение выделенной памяти, когда она более не нужна, ло-
жится на программу, которая вызывает эту функцию. На практике очень легко забыть
об этом, в результате чего функция будет последовательно "отгрызать" куски памяти
от свободного хранилища до тех пор, пока в определенный момент не израсходует
ее всю, и программа потерпит крах. Как уже упоминалось, проблема подобного рода
известна как утечка памяти.

Ниже приведен пример использования новой версии функции. Единственное не-
обходимое отличие от исходного кода — применение delete для освобождения памя-
ти, возвращенной функцией treble ().


 




Возврат ссылки

Функция может возвращать ссылку. Это также чревато потенциальными ошиб-
ками, как и в случае возврата указателя, поэтому здесь вы также должны быть осто-
рожны. Поскольку ссылка не является какой-то отдельной сущностью (это всегда
псевдоним чего-то другого), вы должны быть уверены, что объект, на который она
ссылается, все еще существует после завершения выполнения функции. Очень легко
забыть об этом, используя в функции ссылки, поскольку они выглядят как обычные
переменные.

Ссылки, как возвращаемые типы, особенно важны в контексте объектно-ори-
ентированного программирования. Как вы увидите позднее в этой книге, они по-
зволяют делать такие вещи, которые невозможно осуществить без их применения
(в частности, это касается "перегрузки операций", о которой речь пойдет в главе 9).
Принципиальная характеристика возвращаемого значения типа ссылки в том, что
оно является lvalue. Это значит, что вы можете использовать результат функции, ко-
торая возвращает ссылку, в левой части оператор присваивания.




Теперь рассмотрим пример, иллюстрирующий использование возвращаемых ти-
пов ссылок, а также демонстрирующий, как функция может применяться в левой ча-
сти операции присваивания, когда она возвращает lvalue. Этот пример предполагает,
что у вас есть массив, который содержит смешанный набор значений. Всякий раз,
когда вы хотите вставить новое значение в массив, вы заменяете элемент с наимень-
шим значением.

 

;


 
 

Ниже показан вывод этого примера.

Описание полученных результатов

Посмотрим сначала, как реализована функция. Прототип функции lowest () ис-
пользует doubles в качестве спецификации возвращаемого типа, который, таким
образом, является "ссылкой на double". Возвращаемое значение ссылочного типа
пишется точно так же, как это вы видели в случае объявления ссылочных перемен-
ных — с добавлением & к имени типа. Функция принимает два параметра — одномер-
ный массив типа double и параметр типа int, указывающий длину массива.

Тело функции содержит цикл for, в котором определяется, какой элемент пере-
данного массива содержит минимальное значение. Индекс j найденного элемента с
минимальным значением изначально равен 0, а затем модифицируется внутри цикла,
если текущий элемент a [i] меньше а [ j ]. Таким образом, по завершении цикла j рав-
но индексу элемента массива с минимальным значением. Оператор return выглядит
следующим образом:

Несмотря на тот факт, что это выглядит точно так же, как оператор, возвращаю-
щий значение, поскольку тип возврата объявлен как ссылка, здесь возвращается не
значение элемента а [ j ], а ссылка на него. Адрес а [ j ] используется для инициализа-
ции возвращаемой ссылки. Эта ссылка создается компилятором, потому что возвра-
щаемый тип объявлен как ссылка.

Не путайте возврат &а [ j ] с возвратом ссылки. Если вы укажете в качестве возвра-
щаемого значения &а [ j ], это будет означать адрес а [ j ], то есть указатель. Если вы
сделаете это после спецификации типа возврата как ссылки, то получите от компиля-
тора сообщение об ошибке. Если конкретно, вы получите:

 


Функция main (), которая вызывает lowest (), очень проста. Здесь объявляется
массив типа double и инициализируется 12-ю произвольными значениями, а пере-
менная 1еп инициализируется длиной массива. Начальные значения массива выво-
дятся на экран для сравнения.

Опять-таки, использование в программе манипулятора потока setw() для выравнивания
выводимых значений по ширине требует директивы ^include <iomanip>.

Функция main () использует функцию lowest () в левой части операции присваи-
вания, чтобы изменить элемент, содержащий минимальное значение в массиве. Это
делается дважды, чтобы продемонстрировать, что все это действительно работает, и
написано не случайно. Затем содержимое массива снова выводится на дисплей, с той


276 Глава 5

 

же шириной полей, что и раньше, так что соответствующие значения оказываются
друг под другом.

Как вы можете видеть из вывода перед первым вызовом lowest (), третий элемент
массива, array [2], содержит минимальное значение, так что функция возвращает
ссылку на него и его значение изменяется на б. 9. Аналогично, после второго вызова
array [ 10 ] изменяется на 7. 9. Это достаточно ясно демонстрирует, что возврат ссыл-
ки позволяет использовать функцию в левой части оператора присваивания.

Конечно, при желании вы можете использовать ее и в правой части присваива-
ния или в составе любых других подходящих выражений. Если есть два массива — X
и Y — с количеством элементов, указанным соответственно в lenx и leny, вы можете
присвоить минимальному элементу массива х удвоенное значение удвоенного мини-
мального элемента у в следующем операторе:

Этот оператор должен вызвать функцию lowest () дважды — один раз с аргумен-
тами у и leny в выражении правой части присваивания и один — с аргументами х и
lenx для получения адреса, куда должен быть сохранен результат правого выраже-
ния.

Еще одно железное правило: возврат ссылок

То же правило, которое применяется к возврату указателей из функций, также ка-
сается возврата ссылок.

Никогда не возвращайте из функции ссылку на локальную переменную.

Пока мы оставим тему возврата ссылок из функций, но пока не будем закрывать
ее окончательно. Мы вернемся к ней опять в контексте определяемых пользователем
типов и объектно-ориентированного программирования и узнаем еще о нескольких
волшебных вещах, которые можно делать со ссылками.

Статические переменные в функциях

Есть несколько вещей, которые невозможно делать с автоматическими переменны-
ми внутри функций. Например, невозможно подсчитать, сколько раз функция вызы-
валась, поскольку нельзя накапливать значение счетчика от вызова к вызову. Однако
существует не один способ обойти это при необходимости. Например, вы можете
использовать параметр-ссылку для обновления счетчика в вызывающей программе,
хотя это не поможет, если функция вызывается из множества разных мест програм-
мы. Вы можете применить глобальную переменную, значение которой увеличивать
внутри функции, но глобальные объекты — рискованная вещь, поскольку доступны в
любой точке программы, что открывает путь к их непреднамеренному изменению.

Глобальные переменные также опасно применять в многопоточных программах,
когда несколько одновременно выполняющихся потоков управления имеют доступ к
ним, и вы должны специально позаботиться об управлении доступом из разных по-
токов. Основная проблема, связанная с тем, что более одного потока имеют доступ
к глобальной переменной, заключается в том, что один поток может изменить значе-
ние глобальной переменной в то время как другой работает с ней. Наилучшее реше-
ние в таких случаях состоит в том, чтобы вообще избегать применения глобальных
переменных.

Чтобы создать переменную, чье значение сохраняется от одного вызова функции
до другого, можно объявить ее внутри функции с ключевым словом static. При этом


Структурная организация программ 277

 

используется точно такая же форма объявления static переменной, как вы уже ви-
дели в главе 2. Например, чтобы объявить переменную count как static, можно вос-
пользоваться таким оператором:

Это также инициализирует ее нулем.

Инициализация статической переменной в функции происходит только при первом вызове
функции. Фактически, именно при первом вызове статическая переменная создается и ини-
циализируется. Затем она продолжает существовать на протяжении всего времени выпол-
нения программы, и любое значение, которое она имеет при завершении функции, остается
доступным при следующемеевызове.

Следующий простой пример демонстрирует поведение статической переменной
в функции.


278 Глава 5

 

Функция record () служит только для фиксации того факта, что она была вызва-
на. В результате запуска этого примера получается следующий вывод:

Описание полученных результатов

Статическая переменная count инициализируется нулем и увеличивается в пер-
вом же приложении вывода внутри функции. Поскольку операция инкремента пре-
фиксная, отображается уже увеличенное значение. То есть оно будет равно 1 при
первом вызове, 2 — при втором и так далее. Поскольку переменная count является
статической, она продолжает существовать и сохраняет свое значение от одного вы-
зова функции до другого.

Остаток функции сосредоточен на обработке окончания числового прилагатель-
ного, то есть выборе окончания ' st', ' nd', ' rd1 или 1 th', которое должно быть до-
бавлено при отображении count. Эти окончания в английском языке удивительно не-
регулярны. (Я вот думаю, 101-й вызов должен отобразить 101st, или все-таки 101th?).

Обратите внимание на оператор return. Поскольку типом возврата функции является
void, включение сюда какого-то значения вызовет ошибку компиляции. Вообще в данном
конкретном случае указывать оператор return не обязательно, поскольку выход управле-
ния за закрывающую скобку тела функции эквивалентен оператору return без значения.
Программа должна компилироваться и запускаться без ошибок, даже если не включить в
эту функцию return.

 

Рекурсивные вызовы функции

Когда функция содержит вызов самой себя, такая функция называется рекурсив-
ной. Рекурсивный вызов функции может быть непрямым, когда функция f unl вызы-
вает функцию fun2, которая в свою очередь, вызывает funl.

Может показаться, что рекурсия — прямой путь к бесконечному циклу, и если вы
будете невнимательны, так и случится. Бесконечный цикл заблокирует вашу машину
и потребует нажатия комбинации клавиш <Ctrl+Alt+Del>, чтобы прервать программу,
а это всегда неприятно. Предпосылкой избегания бесконечных циклов является на-
личие в функции некоторого способа, прерывающего процесс.

Если вы не сталкивались с этой техникой ранее, то случаи, когда может пригодить-
ся рекурсия, не слишком очевидны. Однако в физике и математике есть много вещей,
которые для своего описания или управления предполагают применение рекурсии.
Простой пример — факториал целого числа, который для некоторого заданного N ра-
вен произведению 1x2x3x...xN. Этот пример очень часто приводится для демонстра-
ции рекурсии в действии. Рекурсия также применяется для анализа программы во
время процесса компиляции. Однако мы рассмотрим нечто более простое.


 
 

В начале этой главы (см. Ех5_01.срр) мы написали функцию возведения значения
в целочисленную степень — то есть для вычисления хn. Это эквивалентно х, умножен-
ному на себя n раз. Теперь в качестве элементарного примера применения рекурсии
мы можем реализовать это в виде рекурсивной функции. Можно также при этом усо-
вершенствовать реализацию этой функции, чтобы она могла работать с отрицатель-
ными значениями степени, когда х-n эквивалентно 1/хn.

 


г

 

 

Вывод этой программы будет таким:

Описание полученных результатов

Теперь наша функция поддерживает положительные и отрицательные степени х,
поэтому первое, что необходимо сделать — проверить, не является ли степень п, в
которую нужно возвести х, отрицательной:


280 Глава 5

 

Поддержка отрицательных степеней проста; здесь просто используется тот факт,
что х" может быть вычислено как (1/х)n. Поэтому если n отрицательно, то х присва-
ивается 1.0/х, а знак п меняется на положительный.

В следующем операторе i f принимается решение о том, должна ли функция
power () вызывать саму себя еще раз:

 


В случае если п равно нулю, функция возвращает 1. О, в других случаях возвра-
щает результат вычисления выражения x*power (х, п-1), то есть функция power ()
вызывается еще раз с показателем степени, на 1 меньше. Таким образом, ветвь else
оператора if обеспечивает механизм, необходимый для прерывания бесконечной по-
следовательности рекурсивных вызовов функции.

Понятно, что если внутри функции power () значение п больше нуля, то выпол-
няется следующий вызов power (). Фактически для любого заданного значения п,
отличного от 0, функция вызывается п раз, независимо от знака п. Этот механизм
проиллюстрирован на рис. 5.4, где предполагается, что аргумент — показатель степе-
ни — равен 3.

Как видите, функция power () вызывается четыре раза, чтобы сгенерировать х3,
из которых три вызова рекурсивны, то есть, функция три раза вызывает саму себя.

Использование рекурсии

Если только вы не имеете дело с проблемой, которая сама по себе диктует не-
обходимость использования рекурсивных функций, или существуют очевидные аль-
тернативы, то лучше применить другой подход, например, цикл. Это намного более
эффективно, чем рекурсивные вызовы функций. Подумайте о том, что происходит в
последнем примере, когда нужно вычислить простое произведение х*х*... х, причем
п раз. При каждом вызове компилятор генерирует копии двух аргументов функции и
отслеживает место в памяти, куда нужно вернуть значение, при каждом выполнении
return. Кроме того, ему необходимо сохранить содержимое различных регистров
компьютера, чтобы они использовались внутри функции power (), и конечно, восста-
навливать их значение при каждом возврате. При сравнительно небольшой глубине
рекурсивных вызовов накладные расходы будут заметно больше, чем при использова-
нии цикла.

Однако из этого не следует, что вы никогда не должны использовать рекурсию.
Когда проблема для своего решения требует применения рекурсивных вызовов функ-
ций, это может оказаться весьма мощной техникой, значительно упрощающей исхо-
дный код. Пример такого случая будет показан в следующей главе.


 
 

 

Программирование на C++/CLI

В основном функции в программах C++/CLI работают точно так же, как и в про-
граммах "родного" С++. Конечно, здесь вам приходится иметь дело с дескрипторами
и отслеживаемыми ссылками вместо родных указателей и ссылок, и это ведет к неко-
торым отличиям. Существуют несколько моментов, которые слегка отличаются, по-
пробуем систематизировать их.

□ Параметры функций и возвращаемые значения в программе CLR могут быть
типами классов значений, отслеживаемыми дескрипторами, отслеживаемыми
ссылками и внутренними указателями.

□ Когда параметром является массив, нет необходимости в дополнительном па-
раметре, передающем его длину, поскольку массивы C++/CLI хранят в себе ин-
формацию о собственной длине — в свойстве Length.

□ Нельзя применять адресную арифметику с параметрами-массивами в програм-
мах C++/CLI, как это делается в программах на родном С++, то есть всегда нуж-
но использовать индексацию массивов.


Дата добавления: 2015-11-04; просмотров: 27 | Нарушение авторских прав







mybiblioteka.su - 2015-2024 год. (0.035 сек.)







<== предыдущая лекция | следующая лекция ==>