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

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



 

 


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

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

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

Вы можете сразу инициализировать переменную операцией new при ее создании.
Если взять пример с переменной double, которая распределяется операцией new и
чей адрес сохраняется в pvalue, то можно сразу установить ей значение 999.0 с по-
мощью следующего оператора:

 


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

Это обеспечивает возможность последующего использования этой памяти другими
переменными. Если вы не применяете delete и позже присвоите указателю pvalue
другое значение адреса, то станет невозможным освободить память или использовать
переменную, которая в ней содержится, поскольку доступ к этому адресу будет утерян.
В такой ситуации происходит то, что называется утечкой памяти (memory leak) — осо-
бенно если данная ситуация в программе повторяется многократно.

Динамическое распределение памяти для массивов

Динамически выделить памяти для массива очень просто. Если вы хотите распре-
делить массив типа char, то, предполагая, что pstr — указатель на char, можно на-
писать следующий оператор:



pstr = new char[20]; // Распределение строки в 20 символов


Массивы, строки и указатели 213

 

Это выделяет память для символьного массива размером в 20 символов и сохраня-
ет указатель на него в pstr.

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

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

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

pstr = 0; // Установить указатель в null

Чтобы увидеть на практике, как работает динамическое выделение памяти, вы мо-
жете переписать программу вычисления простых чисел, используя память из свобод-
ного хранилища для их размещения.


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

Введите количество простых чисел, которые хотите получить (минимум 4): 20

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

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

pprime = new long[max];

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

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


Массивы, строки и указатели 215

 

Вы используете операцию разыменования для доступа к первым трем элементам
массива. Как было показано ранее, скобки во втором и третьем операторе необходи-
мы потому, что приоритет операции * выше, чем операции +.

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

Вычисление простых чисел выполняется точно так же, как и раньше; единствен-
ное отличие в том, что вместо имени массива primes, использованного в предыдущей
версии, подставляется указатель pprime. Процесс вывода — тот же. Динамическое по-
лучение пространства памяти — вообще не проблема. После того, как память выделе-
на, она никак не влияет на методику вычислений.

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

Хотя в данном случае это и не существенно, все же указатель устанавливается в 0:

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

Динамическое распределение многомерных массивов

Выделение памяти в свободном хранилище для многомерного массива предпо-
лагает использование операции new в несколько более сложной форме, чем для од-
номерного массива. Если предположить, что у вас уже есть соответствующим обра-
зом объявленный указатель pbeans, то получение пространства памяти для массива
beans [ 3 ] [ 4 ], с которым вы имели дело ранее в этой главе, может выглядеть так:

Вы просто специфицируете оба измерения между квадратными скобками после
имени типа элементов массива.

Выделение памяти для трехмерного массива просто требует указания дополни-
тельного измерения с операцией new, как показано в следующем примере:

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

То есть при освобождении памяти массива после операции delete вы должны
указать лишь одну пару квадратных скобок, независимо от того, сколько измерений
было в этом массиве.

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


216 Глава 4

 

Все прочие измерения должны быть заданы константами или константными вы-
ражениями. Поэтому вы можете записать следующим образом:

где max — переменная; однако спецификация переменной для всех других измере-
ний массива кроме крайнего левого, вызовет сообщение компилятора об ошибке.

 

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

Ссылка (reference) во многих отношениях подобна указателю — вот почему я пред-
ставляю ее здесь, хотя на самом деле это совсем другая вещь. Истинная ценность ссы-
лок становится ясной, только когда рассматривается их применение с функциями, в
частности в контексте объектно-ориентированного программирования. Не позволяй-
те ввести себя в заблуждение их кажущейся простотой и тривиальностью концепции.
Как вы увидите позже, ссылки являются чрезвычайно мощным средством, и в некото-
рых контекстах позволяют достичь таких результатов, которые без них вообще были
бы невозможными.

Что такое ссылка?

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

Объявление и инициализация ссылок

Предположим, вы объявили переменную следующим образом:

После этого вы можете объявить ссылку на эту переменную с помощью такого
оператора объявления:

Знак амперсанд, следующий за именем типа long и предшествующий имени пе-
ременной rnumber, говорит о том, что это объявление ссылки, а имя переменной,
которую она представляет — number — специфицировано как инициализирующее зна-
чение, следующее за знаком равенства; таким образом, переменная rnumber имеет
тип "ссылка на long". После этого вы можете использовать ссылку вместо имени ис-
ходной переменной. Например, оператор:

в качестве эффекта дает увеличение значения переменной number на 10.

Сравните ссылку rnumber с указателем pnumber, объявленным в операторе:

Здесь объявляется указатель pnumber, который инициализируется адресом пере-
менной number. Это позволяет увеличивать значение number оператором вроде:

*pnumber +=10; // Инкремент number через указатель


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

 

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

Динамическое выделение памяти в CLR работает иначе, и CLR поддерживает
свою собственную "кучу" памяти, которая полностью независима от кучи родного
С++. CLR автоматически очищает память, которая выделена в куче CLR и необходи-
мость в которой отпала, поэтому вам не нужно использовать операцию delete в про-
граммах, написанных для CLR. CLR может также время от времени упорядочивать
память кучи, дабы избежать фрагментации. Таким образом, CLR исключает вероят-
ность утечек памяти и ее фрагментации. Управление очисткой кучи, которое пред-
усматривает CLR, описывается как сборка мусора (мусор представляет собой отбро-
шенные переменные и объекты, а куча, которой управляет CLR, называется кучей,
очищаемой сборщиком мусора). Вы используете операцию gcnew вместо new для вы-
деления памяти в программе C++/CLI, и префикс дс отражает тот факт, что память
выделяется в очищаемой куче, а не в родной куче С++, где за все хозяйство вы отвеча-
ете самостоятельно.

Сборщик мусора CLR способен удалять объекты и освобождать память, которую
они занимали, когда необходимость в них отпадает. Возникает очевидный вопрос:
как может знать сборщик мусора о том, когда объект кучи более не нужен? Ответ до-
статочно прост. CLR отслеживает каждую переменную, которая ссылается на каждый
объект кучи, и когда не остается переменных, содержащих адрес данного объекта,
это значит, что на него невозможно сослаться в программе, а потому он может быть
удален.

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

Отслеживаемые дескрипторы

Отслеживаемые дескрипторы (tracking handle) имеют сходство с родными указа-
телями С++, однако есть и существенные отличия. Дескриптор хранит адрес, и адрес,
который в нем содержится, автоматически обновляется сборщиком мусора, если объ-


 

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

программ

 

До настоящего момента вы не были готовы организовывать код своих программ в
модульном стиле, поскольку могли конструировать программу как единственную функ-
цию — main (); однако вы использовали библиотечные функции различного рода, а
также функции, принадлежащие объектам. Всякий раз, начиная писать программу на
С++, вы должны продумывать ее модульную структуру с самого начала, и как вы уви-
дите, хорошее понимание того, как должны быть реализованы функции, существенно
для объектно-ориентированного программирования на С++. В этой главе вы изучите
следующие вопросы.

□ Как объявлять и писать собственные функции С++.

□ Как определять и использовать аргументы функций.

□ Как передавать в функции и получать от них массивы.

□ Что означает передача по значению.

□ Как передавать в функции указатели.

□ Как использовать ссылки в качестве аргументов функций, и что означает пере-
дача по ссылке.

□ Как модификатор const влияет на аргументы функции.

□ Как возвращать значения из функций.

□ Как использовать рекурсию.

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


250 Глава 5

 

Что такое функции

Для начала рассмотрим общие принципы работы функций. Функция — это изоли-
рованный блок кода, имеющий определенное специфическое назначение. Функция
обладает именем, которое как идентифицирует ее, так и служит для вызова на вы-
полнение внутри программы. Имя функции глобально, но не обязательно уникально
в С++, как вы увидите это в следующей главе; однако функции, которые выполняют
различные действия, обычно должны иметь разные имена.

Имена функций подчиняются тем же правилам, что и имена переменных. То есть
имя функции — это последовательность букв и цифр, начинающаяся с буквы, причем
знак подчеркивания тоже считается буквой. Имя функции должно обычно отражать
то, что она делает, поэтому, например, вы можете назвать функцию, которая подсчи-
тывает бобы,count_beans().

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


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

 

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

Зачем нужны функции?

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

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

Структура функции

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

Заголовок функции

Сначала рассмотрим в этом примере заголовок функции. Следующая строка — пер-
вая строка функции:

Она состоит из трех частей, которые описаны ниже.

□ Тип возвращаемого значения (в данном случае — double).

□ Имя функции (в данном случае — power).


252 Глава 5

 

□ Параметры функции, заключенные в скобки (в данном случае — х и п, типа do-
uble и int соответственно).

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

Наша функция имеет два параметра: х — значение типа double, которое нужно
возвести в степень, и п — значение степени типа int. Эти переменные параметры
участвуют в вычислениях, выполняемых функцией, вместе с другой переменной —
result, объявленной в ее теле. Имена параметров и любые переменные, определен-
ные в теле функции являются локальными по отношению к ней.

Обратите внимание, что в конце заголовка функции и после закрывающей фигурной скобки
ее тела точка с запятой не требуется

 

Общая форма заголовка функции

Общая форма заголовка функции может быть записана следующим образом:

тип_возврата может быть любым легальным типом. Если функция не возвращает
значения, то тип возврата указывается ключевым словом void. Ключевое слово void
также применяется для обозначения отсутствия параметров, поэтому функция, кото-
рая не имеет параметров и не возвращает значения, должна иметь следующую форму
заголовка:

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

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

 

Тело функции

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

Вычисление производится в цикле for. Управляющая переменная цикла i объяв-
лена в самом цикле, и предполагается, что она последовательно принимает значения
от 1 до п. Переменная result умножается на х при каждой итерации цикла, поэтому
это происходит п раз, чтобы сгенерировать необходимое значение. Если п равно 0,


то оператор цикла не будет выполнен ни разу, поскольку условное выражение сразу
возвратит false, и result останется равным 1.0.

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

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

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

Оператор return

Оператор return возвращает значение result в точку вызова функции. Общая
форма оператора return такова:

return выражение;

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

Если тип возврата функции специфицирован как void, то за оператором return
не должно следовать никакого выражения. Оно должна записываться очень просто:

return;

 

Использование функций

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

Прототипы функций

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

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


254 Глава 5

 

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

Для примера функции power () вы можете написать следующий прототип:

Не забывайте о точке с запятой в конце прототипа функции. Без этого вы получите от
компилятора сообщение об ошибке.

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

При желании вы можете вообще пропустить имена параметров в прототипе функ-
ции и просто написать так:


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







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







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