Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
238
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

I 210

I

Часть! # Введение в програтмшрование на С^^^

 

 

 

 

Неинициализированные указатели могут ссылаться на произвольную область

 

 

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

 

 

В С+Ч- подобные ошибки неинициализированных указателей являются ошибками

 

 

этапа выполнения, а не этапа компиляции. Это неприятно: если возникает данная

 

 

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

 

 

дится догадываться об ошибке, анализируя результаты тестового выполнения

 

 

программы.

 

 

 

 

 

 

 

 

 

Указатели могут инициализироваться с помош,ью операции адреса (&). Неко­

 

 

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

 

 

ции mainO

определяются две

автоматические

переменные — типа int

и char.

 

 

Кроме того, определяются два указателя на int

и char, инициализируется указа­

 

 

тель символьного типа для ссылки на символьную переменную и присваивается

 

 

указатель целого типа для ссылки на целое. После этого с помоидью разыменова­

 

 

ния указателя целому присваивается новое значение. Потом программа проверяет

 

 

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

 

 

разыменованного символьного указателя символьное значение. В конце програм­

 

 

ма устанавливает символьный указатель на целочисленное значение.

 

fl как десятичное: 28791

 

Большинство компиляторов запрещают прямое присваивание

 

вида

рс = &i. Действительно,

рс имеет тип char*, а &i — тип

7077

int*. C-f + здесь строг: он допускает неявное преобразование

1 как шестнадцатеричное

между числовыми типами, но не между указателями разных

1 через

указатель int:

28791

1 через

указатель char:

119

типов. Чтобы присваивание указателей было законным, они

i через

указатель char Ii hex: 77

должны быть одного типа. Между тем, можно без ограничений

Рис. 6 . 4 . Вывод программы

использовать явное преобразование указателей разных типов

(приведение типа). Предполагается, программист знает, что он

 

из листинга

6.5

делает. Приведение типов указателей — опасная практика. Пу-

 

(обратите

внимание

^

-^

J

Г

J

 

на некорректный

тем разыменования символьного указателя можно обраш,аться

 

доступ к int)

к частям целого и изменять их. Из рис. 6.4 видно, что разыме­

 

 

 

 

нование двух указателей на одну целочисленную переменную

 

 

 

 

дает разные результаты (в зависимости от типа указателя).

Листинг 6.5.

Использование указателей с обычными именованными переменными

 

#inclucle <iostream> using namespace std;

int mainO

{

int i; intpi; char *pc;

// неинициализированные указатели

pi = &i;

// указатель на i

*pi = 502;

// нормально, i = 502

if (*pi>0) *pc = 28791;

// тоже, что и if(i>0) i=28791

рс = (char*) &i;

// в некоторых компиляторах нетребуется

int a1 = *pi;

// доступ к i через указатель

int а2 = *рс;

// доступ к i через указатель

cout

«

" i как десятичное: " « i « endl

 

endl;

cout

«

" i как шестнадцатеричное: " « hex « i «

«

" i через указатель int: " « dec «

a1 «

endl;

cout

«

" i через указатель char: " << a2 «

endl;

a2 « endl;

cout

«

" i через указатель char в hex: " «hex «

return 0;

}

Глава 6 » Управление памятью

211

В листинге 6.5 hex и dec называются манипуляторами. Они указывают объекту cout основание системы счисления для вывода значений (шестнадцатеричное или десятичное). Как и манипулятор endl, они вставляются в поток вывода и изменяют его характеристики. Как видно из рис. 6.4, значение, считываемое указателем pi, корректно (28791), а значение, получаемое символьным указате­ лем рс,— нет. Как показывает вывод в шестнадцатеричном виде, символьный указатель считывает только часть битовой последовательности (77 в шестнад­ цатеричном виде) значения i (7077 в шестнадцатеричном представлении). По суш.еству, указатель целого типа может видеть все значение, а символьный указа­ тель — только один байт. Ни один из них не извлечет правильно значение типа double. Вот почему важно разыменовывать указатели корректных типов.

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

Операции с указателями нельзя назвать интуитивно понятными. Очень трудно следить за операциями с указателями, читая программу, поэтому важно помочь своей интуиции, рисуя картинки. Здесь будут полезны два вида рисунков: один, показываюш,ий переменные, для которых память выделяется в стеке (рис. 6.5а), а другой — демонстрируюш,ий, какие указатели на какие значения указывают (рис. 6.5Ь).

Рис. 6.5а демонстрирует целое i, целочис­

А)

 

 

 

 

 

ленный указатель pi и символьный указатель

 

 

 

 

 

 

рс, для которых память распределена в стеке.

 

28791

 

 

 

Хотя их реальный размер может быть одним

Р»

рс

 

 

 

 

и тем же, принято представлять указатели

 

Стек

Динамически

маленькими прямоугольниками. Здесь показа­

 

 

 

 

распределяемая

но значение, содержаил.ееся в целом i. Указа­

В)

28791

 

 

область памяти

 

 

 

 

тели pi и рс содержат адрес i, но этот адрес

 

 

 

 

 

 

Pi

 

 

 

 

нам не важен. Вместо адреса используются

 

 

 

 

 

стрелки, показываюш.ие, что указатели ссыла­

 

 

 

 

 

 

ются на один адрес. Хотя стрелки показывают

 

рс

 

 

 

 

на разные места, это допустимое приближение.

 

 

 

 

 

Рис. 6.5. Указатель целого

типа

 

 

Неизвестно, содержит указатель адрес старше­

 

 

го или младшего байта. Отмечено лишь, что

 

и символьный

указатель,

 

 

ссылающийся

на

именованную

указатели ссылаются на значение, а разыме­

 

целочисленную

переменную

i,

нование этих указателей позволяет получить

 

которая распределяется

в

стеке

значение (если тип указателя корректен).

На рис. 6.5Ь показана та же конфигурация без указания, где распределяются переменные i, pi и рс — в стеке или в динамически распределяемой памяти. Рабочее предположение должно быть таким: если задаются имена переменных, они распределяются в стеке. Если имена не задаются, они распределяются в ди­ намической области памяти. Стрелки показывают, что указатели содержат адреса переменных. Следовательно, их можно использовать для доступа к данным пере­ менных.

Из рисунка видно, что применение указателей для операций с именованными переменными не особенно полезно. Использование указателей для такого вида операций с данными не более эффективно, чем обычная работа с переменными (в данном примере i), на которые ссылаются эти указатели. Установки указателей на значения несоответствующих типов приводит к излишней сложности и ошиб­ кам. Некоторые программисты используют аналогичные методы в вызовах функ­ ций, чтобы избежать операции получения адреса (это будет продемонстрировано в следуюш,ей главе). Но пока будем считать, что указатели нужны не для этого. Они нужны для распределения памяти в динамической области.

I 212 I Часть I ^ ВввАВмте в програмтирошатш но С+4'

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

Операции C + + в основном представляют собой простые символы, но в данном

языке

больше операций, чем специальных символов на клавиатуре, а потому

в C + +

применяются двух- и даже трехсимвольные операции. Но и этого оказа­

лось недостаточно — в C + + для некоторых операций зарезервированы слова new и delete. Они обозначают унарные операции, т. е. имеют только один операнд. Эти операции используются для управления памятью в динамически распределяе­ мой области. Динамически распределяемая область памяти — тоже просто тер­ минология. Программист не знает, где она находится. Что такое динамически распределяемая область? Это область памяти, где операции new и delete вы­ деляют и освобождают память. Нужно лишь знать, что выделенная память

вподходяш^ее время должна освобождаться.

Воперации new в качестве операнда используется имя типа. Она запрашивает

уОС выделения объема памяти, вмеи;аюш,его значение типа, который задается

воперанде. Если память выделена успешно, операция new возвраш,ает адрес памя­ ти, выделенной операционной системой в динамически распределяемой области. Значение адреса обычно присваивается указателю соответствуюш^его типа, и этот указатель может использоваться для операций с неименованной выделенной памятью. Если ОС не хватает памяти, то new возвраш,ает вместо адреса нулевое значение. Программа может проверить результат (возвраш^аемое значение) и ре­ шить, что делать дальше (например, вывести сообш,ение и завершить работу).

Воперации delete операндом является имя указателя. Она находит в динами­ чески распределяемой области выделенную память (размер которой соответствует типу указателя) и просит ОС пометить ее как неиспользуемую. Очень важно, чтобы каждой операции new соответствовала операция delete. Чтобы избежать утечек памяти, программа всегда должна освобождать выделяемую память.

Листинг 6.6 Использование указателей с неименованными переменными в динамически распределяемой области

#include <iostream> using namespace std;

Int mainO

{

 

 

 

// неинициализированные указатели

int *pi; char* pc;

 

pi = new int;

 

 

// получение неименованной памяти и ссылка на нее

if (pi == NULL)

 

// в случае неудачи возвращает ноль

{ cout «

"Нет памяти\п"; return 0; }

 

// или пытается

восстановить

рс = new char;

 

// получение неименованной памяти и ссылка на нее

if (рс == 0)

" Нет памяти \п"; return 0;}

 

// необходимая

предосторожность

{ cout «

 

// или попытка восстановления

*pi = 28791;

 

// операции с неименованными

объектами

if (*pi > 0) *рс = 'а' ;

cout «

" целое вдинамической области: " «

* pi «

endl;

*рс « endl;

 

cout «

" символьное значение вдинамической

области: " «

 

delete pi; delete рс;

// часть жизненного цикла

области

cout «

" (после удаления) int: " « pi «

// динамически распределяемой

"char: «

"*рс «

endl;

 

return 0;

 

 

 

 

 

 

}

 

 

 

 

 

 

 

В листинге 6.6 приведены примеры использования данных операций. В функ­ ции mainO определяются два указателя: pi типа int и рс типа char. Затем они инициализируются с помонлью операции new. Программа проверяет, успешно ли

 

 

 

Глава 6 i* Упровдение памятью

213

целое в динамической области: 28791

 

выделена память. Если нет, программа заверша­

 

ет работу, так как ей не удается достичь своих

символьное значение в динамической области: а

 

целей. Нередко в этом случае предпринимаются

(после удаления) int: -572662307

char: ||

 

 

 

 

некоторые меры для корректного завершения

Рис. 6 . 6 . Вывод программы

из листинга

6.6

(сохранение данных). Иногда программа может

попытаться освободить какую-то

память, чтобы

 

 

 

продолжить работу в специальном режиме с огра­

ниченной

функциональностью. Как видно из рис. 6.6, распределение памяти

прошло успешно, а через указатели корректно присваиваются и считываются

значения (целое 28791 и символьное 'а') .

 

В данном примере

NULL — библиотечная константа. Многие

программисты

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

Операция delete возвращает память, выделенную операцией new, в динами­ чески распределяемую область. Она достаточно "интеллектуальна" и знает тип своего указателя-операнда, а потому освобождает в точности столько памяти, сколько было выделено по new. Если программист забывает о delete, программа все равно будет работать, но со временем может исчерпать всю память в дина­ мически распределяемой области и при очередном использовании new будет возвраш^аться О (особенно если приложение работает продолжительное время). Программисту очень важно не забывать освобождать всю память, запрашиваемую программой из динамически распределяемой области.

Прочитайте программу, содержащую операции удаления, громко вслух. Скажите "delete pi, delete рс". Замечательно. Но не стоит убеждать себя, что указатели при этом действительно удаляются. Удаляется (освобождается) только неимено­ ванная область памяти соответствующего размера, на которую ссылаются pi и рс. Указатели здесь представляют собой именованные стековые переменные, а память для них распределяется в соответствии с правилами области действия, о которых рассказывалось ранее. Память выделяется при определении (здесь в начале функции mainO) и освобождается при завершении области действия, т. е. когда выполнение достигает завершающей фигурной скобки области действия (здесь закрывающей фигурной скобки функции main()).

Удаляются только неименованные переменные динамически распределяемой области. Не стоит пытаться удалять именованные переменные, для которых па­ мять выделяется в стеке, например переменную i в листинге 6.5 (через указа­ тель pi, указатель рс или непосредственно без указателей).

После удаления переменной динамически распределяемой области, на которую ссылается указатель, этот указатель снова становится неинициализированным и не должен использоваться для разыменования. В конце листинга 6.6 делается попытка извлечь значения по указателям pi и рс. Как показано на рис. 6.6, эти указатели теперь ссылаются на произвольные значения, а не на те, куда они дол­ жны указывать. Стоит отметить, что компилятор на это никак не реагирует и не сообщает об ошибке. Операционная система также не препятствует подобным действиям (хотя могла бы и даже должна).

И еще пара слов о delete. Не следует использовать эту операцию с неинициа­ лизированным указателем. Нужно применять только указатель, ссылающийся на память в динамически распределяемой области, выделенную операцией new.

Например,

двукратное освобождение

памяти даст ошибку этапа выполнения

(а не компиляции):

 

delete pi;

delete pi;

/ / некорректно

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

Рис. 6.7. Ynasa,те яъ uej
и символьный ссылающиеся целочисленную и символьную в динамически области
pi
рс
ои
ZO #91

214

Часть i # Введение в г1рограм1^ирован11е на С+^-

А)

Pi рс

Стек

В) W

• Q '

W а

тем, что вы делаете с управлением памятью. Особенно это касается циклов. Не удаляйте дважды один и тот же указатель. Удаление указателя NULL допуска­ ется и не дает никакого эффекта.

 

На рис. 6.7 показана работа с памятью согласно

 

с листингом 6.6. На рис. 6.7а, 6.76 демонстриру­

 

ются указатели рс и pi, распределяемые в стеке,

28791

неименованное целочисленное значение и символь­

 

ное значение в динамической области. Указатели

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

являются именованными (память для них выделя­

область памяти

ется в стеке), а целочисленная и символьная пере­

 

менная — не именованными. Можно приблизительно

 

представить, что целочисленная и символьная пере­

 

менная имеют разные размеры, но об указателях

 

такдумать не стоит. Кажется, что указатели обычно

 

меньше, чем значения, на которые они ссылаются

 

(даже когда ддя них выделяется больше памяти).

 

Операции new и delete доступны только в C++,

указатель,

но не в С. В языке С для динамического распределе­

ния памяти использутеся вызов библиотечной функ­

па неименованную

ции mallocO, а возвраш,ается память при вызове

переменную

функции f гее(). Функция mallocO менее интеллек­

переменную

распределяемой

туальна, чем операция new. Она ничего не знает

 

о размерах типов данных, а поэтому в аргументе

 

нужно указывать, сколько байтов освобождается.

Кроме того, она возвраш^ает обобш^енный указатель, так называемый указатель void, который нельзя разыменовывать. Возвраш.аемый функцией mallocO указа­ тель нужно преобразовывать в соответствующий тип с помоидью операции приве­ дения типа. Если распределение памяти выполнить не удается, mallocO возвращ^аетуказатель NULL. Таким образом, программа может проверить, действи­ тельно ли доступна запрошенная память. Так как C + + обратно совместим с язы­ ком С, функция mallocO поддерживается и здесь. Она определена в стандартной библиотеке cstdlib (или stdlib.h).

pi = ( i n t * ) malloc(sizeof(int));

/ /

получить неименованную память в

 

/ /

динамически распределяемой области

В качестве аргумента функции f гее() используется указатель. Она достаточно интеллектуальна и знает, сколько байтов памяти нужно освободить.

free(pi);

/ / возвращает память динамически

// распределяемой области для других целей

Влистинге 6.7 показаны те же операции, что и в программе из листинга 6.5, но с использованием функций mallocO и f гее(). Обратите внимание на включе­ ние заголовочного файла cstdlib.h. Результат программы будет таким же, как на рис. 6.5.

Многие

компиляторы C + + в действительности реализуют

операции new

и delete в

терминах функций mallocO и free(), однако в C + +

new и delete

используются намного чаш,е, чем mallocO и f гее(). Они прош,е, и, кроме того, эти функции применяются для управления памятью при работе с такими объекта­ ми, как классы, где вызываются неявно через конструкторы и деструкторы. Этого нельзя сказать о функциях mallocO и f гее(). Но есть и общий момент. И опера­ ции, и библиотечные функции mallocO и f гее() используются парами. Если па­ мять выделяется с помош^ью функции new, то ее нельзя освободить функцией f гее(), а если функция выделялась функцией mallocO, ее нельзя освободить фун­ кцией delete. Компилятор не обнаруживает ошибок такого типа. Чтобы избежать подобных ошибок, многие программисты применяют только операции new и delete и избегают использования mallocO и f гее().

Глава 6 » Управление памятью

215

Листинг 6.7. Использование функций malloc() и f гее(), служащих для управления памятью

#inclucle

<iostream>

 

 

 

 

 

 

#inclucle

<cstdlib>

/ /

заголовочный файл для mallocO и freeO

using

namespace std;

 

 

 

 

 

 

int

mainO

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

int

*pi;

char* pc;

/ /

неинициализированные

указатели

pi = ( i n t * )

malloc(sizeof(int));

/ /

получение неименованной памяти

 

 

 

 

 

 

/ /

и ссылка на нее

 

i f

(pi

== NULL) / / в случае неудачи возвращает ноль

 

 

 

 

 

{

cout

«

"Нет памяти\п"; return 0; }

/ /

или попытается восстановить

рс = (char*)

malloc(sizeof(char));

/ /

получение неименованной памяти

 

 

 

 

 

 

/ /

и ссылка на нее

 

 

i f

(рс

== NULL)

/ /

необходимая

предосторожность

{

cout

«

"Нет памяти\п"; return 0; }

/ /

или попытка восстановления

*pi

= 28791;

 

 

 

 

 

 

 

i f

(*pi

> 0)

*рс = 'а' ;

/ /

операции с неименованными объектами

cout

«

"

целое в динамической области: "

«

*pi «

endl;

 

 

cout

«

" символьное значение в динамической области: "

«

*рс «

endl;

free

(pi); free(pc);

 

 

 

 

 

 

cout

«

"

(после удаления) i n t : " « * p i «"char: «

"pc

«

endl;

 

return

0;

 

 

 

 

 

 

 

 

Между тем есть большое количество унаследованных программ С (и C + + ), где применяются функции mallocO и free(). Судя по долговечности программ, вызвавших проблему 2000 г., следует быть готовым к тому, что еще долгие годы придется иметь дело с этими функциями.

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

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

Массивы и указатели

Необходимость указывать длину массива во время компиляции в C + + направ­ лено на эффективное использование памяти и повышение производительности на этапе выполнения. В то же время это создает проблемы переполнения массивов и непроизводительного расходования памяти, поскольку во многих приложениях размер наборов данных известен только на этапе выполнения. Между тем синтак­ сис C + + допускает в качестве размеров массивов только константы. Именно в таких ситуациях полезно динамическое распределение памяти.

г

Часть I ^ Введение в програмтирошаиыв на C'^^-

216

Чтобы можно было использовать динамически распределяемые массивы, сле­ дует изучить взаимосвязь между массивами С + + и указателями. Это соотношение основано на еще одном уникальном средстве C+ + : имя массива (без квадратных скобок и других модификаторов) означает то же самое, что адрес начального элемента массива. Следовательно, имя массива может использоваться для ини­ циализации указателя соответствующего типа (того же, что и элемент массива). Содержимое указателя становится адресом первого элемента массива. Разымено­ вание указателя приводит к считыванию (или изменению) первого элемента мас­ сива. Это открывает возможность использования указателя как синонима имени массива в вызовах функции и с индексами массива.

В следующем примере выделяется память для двух коротких символьных мас­ сивов buf[] и clata[], определяются два символьных указателя р и q. Указатели инициализируются с помощью адреса начального элемента массива. Данный при­ мер показывает, что это можно сделать явно через адрес (р = &buf [0];) или неявно (с помощью имени массива (q = data;):

chaг buf[6] data[6] int i;

P = &buf[0] q = data;

for (i=0; i < 6; i++)

{p[i] = A'+i; q[i] = a'+i; }

*q;

/ / массивы и указатели

/ /

явный синтаксис для адреса первого элемента

/ /

неявный синтаксис для адреса первого элемента

/ /

присваивание компонентов массива

/ /

буквы в верхнем регистре "ABCDEF"

/ /

буквы в нижнем регистре

Единственная разница между указателем и именем массива в том, что указа­ тель можно переназначить, и он будет ссылаться на другую ячейку памяти (с по­ мощью операции получения адреса & или присваивания указателя), а имя массива — это константа. Ей нельзя присвоить другой адрес. В следующем при­ мере первая часть массива data[] (символы в нижнем регистре) копируется во вторую часть массива buf [ ], после чего он будет содержать буквы "АВСаЬс", а не "ABCDEF".

р = &buf[3];

for (i=0; i < 3; i++) p [ i ] = q [ i ] :

/ /

указывает на вторую половину, массива

/ /

замена первых трех компонентов

/ / т о ж е , что buf[i+3]=data[i];

В обоих примерах имена указателей те же, что в именах массивов. Везде, где используется q[i], можно применять data[i]. Это хорошо, но не очень практично, так как не позволяет сделать что-нибудь новое. Однако работа с массивами через указатели открывает гораздо большие возможности.

Еще одно уникальное средство C + + состоит в том, что при арифметических операциях с указателями учитывается тип и размер элементов памяти, на которые ссылается указатель. Например, если ptr — указатель на значение double по адресу 2000, то ptr + 1 указывает на значение double после адреса, на который указывает ptr (2008, а не 2001).

Это особенно удобно, когда указатель ссылается на элемент массива. Увели­ чение указателя на 1 — не то, что вы думаете. При этом не добавляется 1 к содержимому указателя-переменной (как в случае арифметических операций с числовыми типами данных). Такая операция приводит к тому, что указатель будет ссылаться на следующий элемент массива. Разыменование указателя дает значение следующего элемента. Увеличение указателя на 2 перемещает указатель на два элемента массива. В следующем примере первая часть массива data[] (те же символы "аЬс") копируется в первую часть массива buf [ ], так что содержи­ мое его превращается в "abcabc".

р -= buf;

// снова указывает на начало массива

for (i=0; i < 3; i++)

// заменить первую половину массива

*(p+i) = *(q+i);

// также эквивалентно buf[i]=data[i];

Глава 6 ^ Управление памятью

217

Обратите внимание, что операция разыменования имеет более высокий прио­

ритет, чем арифметические операции, поэтому *р + i здесь использовать не сле­

дует. Это означает р[0] + i, а не p[i].

 

Еще более эффектный код можно написать с помощью применяемой к указате­

лям операции инкремента (или декремента). Во всех случаях добавление 1 к ука­

зателю фактически означает сложение размера типа с указателем (т. е. с адресом,

хранимым в указателе). В результате указатель перемещается на следующий

элемент массива. В листинге 6.8 сведены предыдущие примеры. В первом цикле

устанавливается и содержимое массива buf[] (ABCDEF), где вместо buf[i] ис­

пользуется p[i]. Во втором цикле модифицируется вторая половина

массива.

Начальный

буфер:

 

ABCDEF

В этом цикле p[i] означает не buf[i], а buf[i+3]. Третий

половина:

цикл выводит массив buf[] (ABCabc) с помощью обычных

Замененная

вторая

АВСаЬс

обозначений. Затем указатель р устанавливается снова на

Замененная

первая

половина:

abcabo

начало массива buf[], а четвертый цикл заменяет первую

 

 

 

 

 

 

 

 

половину buf[]. Результат выводится путем применения к

Рис. 6 . 8 . Вывод

программы

указателю операции инкремента. Вывод программы пред­

 

из листинга 6.8

ставлен на рис. 6.8.

Листинг 6.8. Использование указателей для обработки массива

«include <iostream> using namespace std;

int mainO

char buf[6], data[6], *p,

*q;

/ /

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

int i;

 

 

 

 

 

/ /

индекс массива

p = &buf[0];

 

 

/ /

явный синтаксис для адреса

g = data

 

 

 

 

/ /

неявный синтаксис для адреса

cout «

"Начальный буфер: ";

 

 

 

for (i=0; i < 6;

i++)

 

/ /

присваивание компонентов массива

{ p[i] = 'A'+i;

 

/ /

буквы в верхнем регистре

 

cout « p[i];

 

/ /

выводит ABCDEF

 

q [ i ]

= ' a ' + i ; }

 

/ /

q и data - синонимы

p = &buf[3];

 

 

 

/ /

указывает на вторую половину

for

(i=0;

i

< 3;

i++)

 

/ /

заменить последние три компонента

p [ i ]

= q [ i ] ;

 

 

/ /

то

же, что buf[i+3]=data[i];

cout

«

endl

«

"Замененная

вторая половина:

";

for

(i=0;

i

< 6;

i++)

 

/ /

выводит ABCabc

cout

« b u f [ i ] ;

 

p = buf;

 

 

 

 

/ /

указывает на начало массива

for (i=0; i < 3;

i++)

 

/ /

замена первой половины массива

*(p+i)

= *(q+i);

 

/ /

то же, что buf[i]=data[i];

cout

«

endl

«

"Замененная

первая половина:

";

while (p - buf < 6)

 

/ /

увеличенный указатель

cout

«

*p++;

 

 

/ /

не следует злоупотреблять этим средством

cout

«

endl;

 

 

 

 

 

return

0;

 

 

 

 

 

 

 

Когда операции инкремента и разыменования используются в одном выраже­ нии (как *р++), приоритету них одинаковый, но они вычисляются справа налево, а не слева направо, как большинство операций C + + (см. таблицу 3.1 в главе 3). Между тем постфиксная операция передает значение указателю до инкремента. Следовательно, выражение *р++ имеет следующий смысл: сохранить старый

I 218 I

Часть I # Введение в програттшрошаишв на С^-ь- .

указатель, увеличить его для ссылки на следующий элемент массива и вернуть значение по адресу, на который ссылается старый указатель. Другими словами, если temp — указатель символьного типа, то *р++ эквивалентно следующему:

(temp = р, P++, *temp)

Указатели и имена массивов эквивалентны во всех отношениях, кроме одного: указатель можно увеличивать (инкремент) или переназначать, а имя массива — нет. Например, в конце листинга 6.8 было бы ошибкой снова выводить массив buf[] таким образом:

while

-

buf < 6)

/ /

смещение в элементах массива

cout

«

 

*buf++;

/ /

синтаксическая ошибка

Для этой цели можно без проблем использовать другой указатель:

q = buf;

 

 

while (р - q 1=0)

//

смещение в элементах массива

cout « *q++;

/ /

не следует злоупотреблять данным средством

Обратите внимание, что указатель р используется здесь как контрольное значение. В конце листинга 6.8 указатель р был увеличен для ссылки на позицию после завершающего элемента массива buf[]. Вот почему приведенный выше цикл завершается, когда указатель q заканчивает проход по элементам массива buf[] и ссылается на элемент после последнего, т. е. на то же место, что и указатель р.

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

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

Динамические массивы

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

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

Листинг 6.9 показывает упрощенную версию программы, представленной в листинге 5.11, где обрабатываются введенные с клавиатуры суммы транзакций.

Метод, который использовался в листинге 5.11 (символьное контрольное зна­ чение в конце вводимых данных), можно считать хорошим решением для интерак­ тивного ввода. В листинге 6.9 применялось нулевое контрольное значение. Цикл ввода завершался по break, когда вводилась нулевая сумма. Кроме того, цикл завершался при превышении счетчика введенных пользователем данных размера массива clata[].

Рис. 6 . 9 . Вывод программы из листинга 6.9 (усечен)
1 Введите сумму (или 0 для завершения):22
1 Введите сумму (или 0 для завершения):33
1 Введите сумму (или 0 для завершения):44
1 Введите сумму (или 0 для завершения):55
1 Не хватает памяти: вывод прекращен Значение 55 несохранено
Общая сумма 3 значений равна 99

Глава 6 ^ Управление памятью

219

Листинг 6.9. Считывание данных транзакций с защитой от переполнения массива

#inclucle

<iostream>

 

 

 

 

 

 

 

 

#inclucle

<iomanip>

 

 

 

 

 

 

 

 

using

namespace std;

 

 

 

 

 

 

 

 

int

mainO

 

 

 

 

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

/ /

для отладки: должно быть больше

const int

NUM = 3;

 

 

 

 

double

amount,

t o t a l

= 0,

data[NUM];

 

/ /

инициализация текущих данных

int

count

= 0;

 

 

 

 

 

do {

 

 

 

 

 

 

 

 

 

 

 

/ /

выполнение до конца файла или до переполнения

массива

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cout

«

"Введите сумму (или О для завершения)

 

 

 

cin »

 

amount;

 

 

 

 

/ /

получить из файла следующее значение double

 

i f

(count==NUM 11 amount==0) break;

/ /

переполнение или равенство контрольному значению

 

t o t a l

+= amount;

 

 

 

 

/ /

обработка текущих действительных данных

 

data[count++]

= amount;

 

 

 

/ /

и получение следующей строки ввода

}

while

(true);

 

 

 

 

 

/ /

прочитаны ли все данные?

i f

(amount

 

!= 0)

 

 

 

 

 

{

cout

«

"Нет памяти: ввод был прекращен\п";

 

 

 

 

cout

«

 

"Значение

" «

amount «

"не сохранено" « endl;

}

cout

 

«

 

"\пОбщая сумма "

«

count

«

"значений

равна "

 

 

 

 

«

 

t o t a l

« endl;

 

 

 

/ /

нет результатов,

если нет ввода

i f

(count

== 0)

return 0;

 

 

 

c o u t «

"\nHoM. транз. Сумма\п\п";

 

/ /

установить формат fixed для double

cout . setf(ios::fixed);

 

 

 

cout.precision(2);

 

 

 

 

/ /

вС;его цифр, если НЕ ios: :fixed

for

(int

i

 

= 0;

i < count;

i++)

 

/ /

снова

проход по данным

 

{

cout

«

setw(4);

cout

«

i+1)

 

/ /

номер транз.

 

 

 

cout

«

 

setw(11);

cout

«

data[i]

« endl }

/ / знач. тразакции

return

0;

 

 

 

 

 

 

 

 

 

 

 

 

Ном. транз.

1

2

3

Сумма

22.00

33.00

44.00

Таким образом, цикл чтения может прерываться по двум причинам: конец ввода и переполнение массива. Если поведение программы должно прерываться для разных случаев завершения цикла, то нужно проверять, по какой причине завершен цикл. В данном примере программа выводит пользователю предупреждаюш,ее сообш^ение о переполнении массива. Проверку count == NUM здесь нельзя считать надежной, так как вводимый набор данных может содержать в точности столько записей, сколько элементов в массиве данных. Для реальных программ,

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

В листинге 6.9 проверяется наличие контрольного значения (сумма транзакции равна 0). При вводе тако­ го значения цикл завершается. Если его не было, то можно предположить, что причиной завершения цикла стало переполнение массива. Если контрольное значе­ ние найдено, предполагается, что все данные считаны.

Вывод этой программы содержит только три записи массива. Они показаны на рис. 6.9.

Соседние файлы в предмете Программирование на C++