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

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




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

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

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

Экономия памяти — не единственное преимущество использования указателей.
Во множестве случаев также экономится время. Подумайте, что случится, если вы за-
хотите переместить "Oliver Hardy" на первую позицию, a "Robert Redford" — в конец
списка. В случае с массивом указателей нужно всего лишь обменять значения двух
указателей — сами строки при этом останутся там, где и были. Если же они будут хра-
ниться как массивы char, как это было сделано в Ех4_04. срр, понадобится выпол-
нить значительный объем копирования — придется полностью скопировать строку
"Robert Redford" в какое-то временное место, затем скопировать на ее место "Oliver
Hardy", после чего скопировать "Robert Redford" в конечную позицию. Это потребует
заметно больше времени для выполнения.

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


 
 

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



Операция sizeof

И здесь на помощь приходит новая операция. Операция sizeof возвращает целое
значение типа sizet, которое означает количество байт, занятых ее операндом.
Вспомните из того, что было сказано ранее, что size_t — это тип, определенный в
стандартной библиотеке, и обычно он основан на базовом типе unsigned int.

Взгляните на следующий оператор, который ссылается на переменную dice из
предыдущего примера:

Значение выражения sizeof dice равно 4, поскольку переменная dice объявле-
на как int, а потому занимает 4 байта. Поэтому приведенный оператор выведет на
экран значение 4.

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


202 Глава 4

 

Выражение (sizeof pstr) / (sizeof pstr [0]) делит число байт, занятых масси-
вом указателей на число байт, занятых первым его элементом. Поскольку каждый эле-
мент массива занимает одно и то же место в памяти, в результате получается число
элементов массива.

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

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

переменная size получает значение 4. Эта переменная объявлена с типом size_t,
чтобы обеспечить соответствие типу значения, которое возвращает операция sizeof.
Использование другого целочисленного типа для этой переменной может привести к
появлению предупреждающих сообщений компилятора.


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

 

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

Как видите, необходимое изменение в этом примере очень простое. Мы вычисля-
ем количество элементов в массиве указателей pstr и сохраняем результат в перемен-
ной count. Затем везде, где было указано общее количество элементов массива (6),
мы просто вставляем переменную count. Теперь вы можете добавлять новые имена в
список счастливых звезд, и программа "подгоняется" автоматически.

Константные указатели и указатели на константы

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

Однако массив указателей требует более тщательного рассмотрения. Массив объ-
явлен следующим образом:

Каждый указатель в массиве инициализирован адресом строчного литерала —
"Robert Redford", "Hopalong Cassidy" и так далее. Типом строчного литерала являет-
ся массив const char, поэтому получается, что вы сохраняете адрес константного
массива в не константном указателе. Причина, по которой компилятор позволяет
использовать строчные литералы для инициализации элементов массива char *, за-
ключается в необходимости обеспечения обратной совместимости с существующим
кодом.

Если вы попытаетесь изменить символьный массив оператором вроде следующего:

то программа не скомпилируется.

Если вы сбросите один из элементов массива в указатель на символ посредством
оператора:

то программа скомпилируется, но потерпит крах при выполнении этого оператора.

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


204 Глава 4

 

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

Однако вы все еще можете написать такой оператор:

Те счастливчики, которые выберут мистера Редфорда (Redford), получат вместо
него мистера Кессиди (Cassidy), поскольку оба указателя теперь указывают на одно
и то же имя. Обратите внимание, что это не изменяет значения объектов, на кото-
рые указывают указатели-элементы массива, но изменяет значение самого указателя,
хранящегося в pstr [0]. Поэтому вы должны запрещать подобного рода изменения,
поскольку некоторые люди считают, что старина Хоппи (Hopland Cassidy) не столь
импозантен, как Роберт (Robert Redford). И сделать это можно с помощью следующе-
го оператора:

 

 


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

□ указатель на константный объект;

□ константный указатель на объект;

□ константный указатель на константный объект.

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

Во второй ситуации адрес, сохраненный в указателе, не может быть изменен, но
объект, на который он указывает, может:

 

И, наконец, в третьей ситуации как указатель, так и объект, на который он указы-
вает, определены как константы, а потому никогда не могут быть изменены:

 

 


Конечно, все это касается указателей любого типа. Указатель на тип char использован
нами исключительно для примера.

 

Указатели и массивы

Имена массивов в некоторых случаях могут вести себя как указатели. В большин-
стве ситуаций, если вы используете имя одномерного массива само по себе, оно ав-
томатически преобразуется в указатель на первый элемент этого массива. Обратите
внимание, что это не касается того случая, когда имя массива выступает в качестве
операнда sizeof.


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

 

Если имеются следующие объявления:

то вы можете написать такое присваивание:

Это присваивает адрес первого элемента массива data указателю pdata. Приме-
нение имени массива самого по себе означает ссылку на его адрес. Если вы использу-
ете имя массива data с индексным значением, то это означает ссылку на содержимое
элемента, соответствующего значению индекса. Поэтому, если вы хотите сохранить
адрес элемента в указателе, то должны использовать операцию взятия адреса:

Здесь указатель pdata получает адрес второго элемента массива.

Арифметика указателей

Над указателями можно выполнять арифметические операции. Правда, они огра-
ничены только сложением и вычитанием, но можно также сравнивать значения ука-
зателей, получая логический результат. Арифметика над указателями неявно предпо-
лагает, что указатель указывает на массив, и арифметические операции выполняются
над адресом, содержащимся в указателе. Так, например, указателю pdata можно при-
своить адрес третьего элемента массива data с помощью следующего оператора:

В этом случае выражение pdata + 1 будет ссылаться на адрес data [ 3 ] — четверто-
го элемента массива data, поэтому вы можете переставить указатель на этот элемент
следующим образом:

 


Этот оператор увеличивает адрес, содержащийся в pdata, на количество байт, ко-
торое занимает каждый элемент массива data. В общем случае выражение pdata + п,
где п — любое целочисленное выражение, добавляет n*sizeof (double) к адресу, со-
держащемуся в pdata, потому что pdata объявлен как указатель на double. Это про-
иллюстрировано на рис. 4.8.


206 Глава 4

 

Другими словами, инкремент и декремент указателя работает в терминах типа
объекта, на который он указывает. Увеличение на единицу указателя на long изме-
няет его содержимое на адрес следующего long, то есть увеличивает его адрес на че-
тыре. Аналогично, инкремент указателя на short на единицу увеличивает значение
адреса на два. Более распространенная нотация для увеличения указателя использует
операцию инкремента. Например:

 


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

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

Вы можете, конечно, разыменовать указатель, к которому применено арифмети-
ческое действие (а иначе в нем не было бы особого смысла). Например, если предпо-
ложить, что pdata все еще указывает на data [2], то оператор:

эквивалентен следующему:

Когда вы хотите разыменовать указатель после увеличения адреса, который он со-
держит, скобки необходимы, поскольку приоритет операции разыменования выше,
чем приоритет арифметических операций + или -. Если вы напишете выражение
*pdata + 1 вместо * (pdata + 1), это добавит единицу к значению, находящемуся по
адресу, хранящемуся в pdata, что эквивалентно выполнению data [2] + 1. Поскольку
это не lvalue, его применение в предыдущем операторе присваивания заставит ком-
пилятор сгенерировать сообщение об ошибке.

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

то, применив нотацию указателя, вы можете сослаться на элемент data [3], напри-
мер, так: * (data + 3). Этот вид нотации может применяться совершенно свободно,
так что для доступа к элементам data [ 0 ], data [ 1 ], data [ 2 ] вы можете писать *data,
* (data + 1), * (data+2) и так далее.

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


 


 
 


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

Если скомпилировать и запустить этот пример, то получится следующий вывод:


208 Глава 4

 

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

Здесь присутствуют обычные операторы #include с заголовочным файлом
<iostream> для ввода и вывода, а также с <iomanip>, поскольку используются мани-
пуляторы потока для установки ширины полей при выводе.

Константа МАХ определяет количество простых чисел, которые нужно получить
от программы. Массив primes, в котором сохраняются результаты, инициализирован
первыми тремя простыми числами, чтобы было с чего запустить процесс. Вся работа
выполняется в двух циклах: внешнем do-while, который указывает следующее прове-
ряемое значение и добавляет найденное значение в массив primes, если оно является
простым числом, и внутренним циклом for, который в действительности проверяет
значение на принадлежность к множеству простых чисел.

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

 

Этот оператор присваивает переменной found значение 1, если нет остатка от де-
ления значения trial на текущее простое число * (primes + i) (напомним, что это
эквивалентно primes [i]), и 0 — в противном случае. Оператор if прерывает цикл
for, если found равно 1, поскольку число-кандидат в trial в этом случае не является
простым.

После завершения цикла for (по любой причине) необходимо решить, является
ли текущее значение trial простым. Это определяется значением индикаторной пе-
ременной found.

Если trial действительно содержит простое число, этот оператор сохраняет его
значение в primes [count], после чего увеличивает count с помощью постфиксной
операции инкремента.

После того, как найдено МАХ простых чисел, они выводятся в поле шириной 10
символов, по 5 в строке, в результате выполнения такого оператора:

Это начинает новую строку, когда i получает значения 0, 5, 10 и так далее.

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

 

 


 


 
 

Вот пример типичного вывода этого примера:

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

Здесь программа работает с указателем pbuf fer, а не с именем массива buffer.
Не нужна переменная count, поскольку указатель увеличивается в цикле while, пока
не найдет \0. Когда обнаруживается символ \0, pbuf fer содержит адрес его положе-
ния в строке. Счетчик количества символов строки, таким образом, вычисляется, как
разница между адресом, записанным в pbuf fer, и адресом начала массива, который
обозначен buffer.

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

Этот цикл не содержит в себе никаких операторов, а только проверочное условие.
Это должно работать адекватно, за исключением того факта, что значение счетчика
увеличивается после достижения \0, поэтому адрес будет на единицу больше, чем по-
следняя позиция в строке. Поэтому в данном случае количество символов в строке
должно быть вычислено как pbuf fer — buffer — 1.

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

Применение указателей с многомерными массивами

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


210 Глава 4

 

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

Вы можете объявить и присвоить значение указателю pbeans так:

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

 


Это эквивалентно применению имени одномерного массива, который заменяется
его адресом. Мы использовали это в предыдущих дискуссиях; однако поскольку beans —
двумерный массив, вы не можете присвоить указателю адрес таким оператором:

Проблема заключается в типе. Тип указателя определен как double*, но массив
имеет тип double [3] [4]. Указатель, который может сохранить адрес этого массива,
должен быть double* [4]. С++ ассоциирует измерения массива с его типом, и опера-
тор, приведенный выше, был бы легальным только в том случае, если бы указатель
был объявлен вместе с необходимым измерением. Это делается с применением не-
сколько более сложной нотации, чем вы видели до сих пор:

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

Нотация указателей с многомерными массивами

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

□ Используя имя массива с двумя значениями индексов.

□ Используя имя массива в нотации указателей.
Таким образом, следующие два оператора эквивалентны:

 


Давайте разберемся, как это работает. В первой строке используется нормальная
индексация массива для ссылки на элемент со смещением j в строке i массива.

Вы можете определить значение второй строки, разбирая ее изнутри наружу,
beans ссылается на адрес первой строки массива, поэтому beans + i ссылается на
строку номер i. Выражение * (beans + i) — это адрес первого элемента строки i, по-
этому * (beans + i) + j — адрес элемента в строке i со смещением j. Таким образом,
полное выражение ссылается на конкретный элемент массива.

Если вы действительно хотите все запутать (хотя это не рекомендуется), то следу-
ющие два оператора, в которых смешивается нотация массивов с нотацией указате-
лей, также легально ссылаются на тот же элемент массива:


 


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

 

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

Работа с фиксированным набором переменных в программе может серьезно стес-
нять разработчика. Часто в приложениях возникает необходимость в оперативном
принятии решений относительно динамического выделения места для размещения
переменных различных типов непосредственно во время выполнения — в зависимо-
сти от входных данных, полученных программой. Для одного набора данных может
быть разумно применять большой массив целых чисел, в то время как другой набор
входных данных может потребовать большого массива чисел с плавающей точкой.
Понятно, что поскольку динамически распределенные переменные не могут быть
определены во время компиляции, они не имеют имен в исходном тексте програм-
мы. Когда они создаются, то идентифицируются адресами в памяти, которые сохра-
няются в указателях. Благодаря мощности указателей и средствам динамического
управления памятью в Visual С++ 2005, написание программ, обладающих такого рода
гибкостью, выполняется легко и быстро.

Свободное хранилище, псевдоним "куча"

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

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

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

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


212 Глава 4


 

Операции new и delete

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


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







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







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