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

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

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

I

220

I

Часть 1 ^ Введение в

 

 

 

 

Форматированный вывод здесь отличается от вывода программы из листин­

 

 

 

га 5.11. Функция setfO устанавливает управляющие флаги объекта cout. Она

 

 

 

использует в качестве аргумента флаг ios: : fixed, задающий вывод десятичных

 

 

 

значений с фиксированной точкой, а не в экспоненциальном представлении (в виде

 

 

 

мантиссы и порядка). У функции

precision() в аргументе указывается число

 

 

 

выводимых цифр при отображении

значения на экране. Если флаг ios::fixed

 

 

 

установлен, это число обозначает число цифр после десятичной точки (запятой),

 

 

 

а если нет — общее число значащих цифр. Не путайте это.

 

W

О с т о р о ж н о ! Смысл аргумента функции precision() зависит

 

 

от флага ios: :fixed. Когда флаг установлен, аргумент означает число цифр

 

 

после десятичной точки; в противном случае — это общее число

 

 

значащих цифр.

 

В примере из листинга 5.11 функция width() задает минимальное число пози­ ций, занимаемых на экране следующим выводимым значением. Если значению требуется для отображения больше позиций, то используются дополнительные позиции (тем самым форматирование не соблюдается). Другие функции формати­ рования, setfO и precisionO, влияют на формат до следующего вызова данных функций с другим аргументом. Функция width () действует только на одно выводи­ мое значение. После вывода значения устанавливается илирина по умолчанию (0), т. е. форматирование данных отсутствует. Таким образом, функция width() должна вызываться перед выводом каждого значения.

В листинге 6.9 используется манипулятор setw(), включаемый в поток вывода аналогично манипуляторам endl, dec и hex, о которых говорилось выше. Аналогич­ но функции width (), областью действия этих манипуляторов является только одно значение вывода, поэтому манипулятор setw() нужно включать перед каждым значением, даже если ширина поля остается той же. Обратите внимание, что про­ грамма из листинга 6.9 использует заголовочный файл iomanip. В более ранних примерах также применялись манипуляторы (по крайней мере, endl), но для них этот заголовочный файл был не нужен. Он требуется только для манипуляторов, использующих аргументы. Если программист забудет включить его, программа компилироваться не будет. Увы, манипуляторы и форматтеры не согласованы,

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

Ос т о р о ж н о ! При использовании функций форматирования

(например, widthO) или манипуляторов без аргументов (таких, как endl) нет необходимости включать заголовочный файл iomanip.

Если манипуляторы используются с аргументами (например, setw()), заголовочный файл iomanip нужно включить.

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

Вместо выделения памяти для массива data[] в стеке путем указания заранее определенной константы этапа компиляции (например, 3), программа распреде­ ляет массив в динамической области с помощью задающей размер массива пере­ менной size. Эта переменная имеет значение на этапе выполнения, а не на этапе компиляции. Учтите, что у динамического массива нет имени и к нему можно обращаться только через указатель data. Когда указатели используются для ссылки на именованные массивы (см. листинг 6.8), приходится выбирать между

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

I 221 I

именем указателя (например, p[i]) или именем массива (например, buf[i]). Здесь выбора нет. Массив в динамически распределяемой области памяти имени не имеет, поэтому нужно использовать для доступа к его элементам имя указателя.

Листинг 6.10. Считывание данных в массив, распределяемый в динамической памяти

#inclucle <iostream> #inclucle <iomanip> using namespace std;

int mainO

 

 

 

 

 

 

 

{

 

 

 

 

 

 

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

const int NUM = 3;

 

 

 

double amount, total = О, *data;

 

 

int count = 0, size = NUM;

 

 

// инициализация массива в heap

data = new double[size];

 

 

do {

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

 

cout «

 

cin » amount;

 

 

 

 

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

if (amount==0) break;

 

 

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

if (count == size)

 

 

 

// нет памяти, запросить больше.

{ size = size * 2;

 

 

// сделать заметным

double *q = new double[size];

// удвоенный размер массива

if (q == 0)

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

endl;

 

{

cout «

 

break; }

 

 

 

 

 

else {

 

"Выделено больше памяти: размер = " «

size <<endl;

 

cout «

for (int i=0; i < count; i++)

// копирование старых данных

 

q[i] = data[i]; .

 

 

// запись с индексами

delete [] data;

 

 

 

// незабывайте об освобождении старых данных

data = q;

}

"

 

// подключить основной указатель

total += amount;

 

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

data[count++] = amount;

 

 

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

} while (true);

 

 

 

 

 

if (amount ! = 0)

 

 

 

 

 

{ cout «

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

endl; }

cout «

"Значение " «

amount «

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

cout << "\пОбщая сумма " «

count << "значений равна " « total « endl;

if (count == 0) return 0

 

 

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

cout «

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

// задать фиксированный формат для double

cout. setf(ios::flexed);

 

 

cout.precision(2);

 

 

 

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

for (int i = 0; i < count; i++)

// снова проход по данным

{ cout «

setw(4); cout «

i++)

// номер транзакции

cout «

setw(11); cout «

data[i] « endl; } // значение транзакции

return 0;

 

 

 

 

 

 

 

Если следующая считываемая программой сумма помещается в массив, т. е. условие count =- size все еще ложно, значение сохраняется в массиве. Обратите внимание на использование указателя data как имени массива. Оператор data[count++]= amount тот же, что и впредыдущих версиях программы, но смысл его другой. В листинге 6.9 data — это имя массива в стеке, а влистинге 6.10 — имя указателя, ссылающегося на неименованный массив в динамически распреде­ ляемой области.

222

Часть I • Введение в программирование но C++

 

Когда все ячейки динамического массива оказываются заполненными и усло­

 

вие count == size становится истинным, дело принимает интересный оборот.

 

В листинге 6.9 выводится сообщение об ошибке и вводимые данные отбрасывают­

 

ся. В листинге 6.10 делается попытка восстановить работу после переполнения

 

массива, выделить в динамически распределяемой области больше памяти и ско­

 

пировать существующие данные в новый массив.

 

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

 

дополнительной памяти. Если data получит адрес нового фрагмента памяти, он по­

 

теряет адрес существующих данных. Поэтому для выделения массива в динамиче­

 

ски распределяемой области потребовался другой локальный указатель q. Размер

 

нового массива вдвое превышает размер существующего. Удваивание размера —

 

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

 

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

 

double q = new double[size*=2];

/ /

получить дополнительную динамически

 

 

/ /

распределяемую память

 

Изменение size в операторе, выделяющем память,— нежелательный метод.

 

Сопрово>вдающий приложение программист может легко проглядеть данное дей­

 

ствие. Лучше сделать это явно перед операцией new:

 

size = size * 2;

 

 

 

double *q = newdouble[size];

/ /

удвоение размера массива

 

if (q == NULL)

 

 

 

{ cout « "Нет памяти \ n " ; return;

}

 

 

else

 

 

 

/ * копирование данных в массив, на который указывает q */

Обратите внимание, что один и тот же тип указателя double* используется для ссылки на одно значение типа double и на массив значений double. Обычный случай. По типу указателя нельзя сказать, ссылается он на массив или на одно значение. Можно только запомнить, на что он указывает. Это затрудняет поясне­ ние программисту намерений разработчика.

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

for (int i=0; i < count;

i++)

 

q [ i ] = d a t a [ i ] ;

/ /

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

 

/ /

половину новых данных

Некоторые программисты предпочли бы организовать этот цикл, используя арифметические операции с указателем через индексы, например: '

for (int

i=0; i < count; i++)

/ / копировать старые данные

*(q+i)

= *(data+i);

 

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

for

(double *p=q, *r=data; p-q < count; p++, r++)

/ / годится?

*p

= *r;

 

Третьи используют такую форму:

double

= q, *r = data,

int i = 0;

while

(i++

< count)

 

*p++ = *r++;

/ / правда, красиво?

Рис. 6.10. Вывод программы из листинга 6.10 (с отладочными сообщениями)
1 Введите сумму (или 0 для завершения): 22
1 Введите сумму (или 0 для завершения): 33
1 Введите сумму (или 0 для завершения): 44
1 Введите сумму (или 0 для завершения): 55
1 Выделено больше памяти: размер = 6
1 Введите сумму (или 0 для завершения): 66
1 Введите сумму (или 0 для завершения): 0
Общая сумма 5 3Ha4eHifft равна 220

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

223

Ном. транз. Сумма

122.00

233.00

344.00

455.00

566.00

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

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

Результат выполнения программы из листинга 6.10 представлен на рис. 6.10.

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

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

Енде один пример такой склонности программистов — размеш,ение определения amount. Чтобы избежать конфликтов имен, особенно при сопровождении про­ граммы, и сделать программу более понятной, важно определять переменные на максимально глубоком уровне в структуре блоков. Вот почему указатель q определяется в локальном блоке оператора if. С пере­

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

Завершая работу, наша программа не возвращает оставшуюся память в дина­ мически распределяемую область. Вероятно, в данном случае опасности нет, так как операционная система выполнит "очистку", но это нельзя считать хорошим стилем программирования. И не стоит полагаться на предусмотрительность разра­ ботчиков ОС.

Бессмысленно даже обсуждать, опасны ли "утечки памяти". Нужно выработать Д9 автоматизма привычку: при распределении памяти из динамической области выбрать место, где эта память будет возвращаться обратно. В данном случае функ­ ции main О следовало бы делать это непосредственно перед оператором return:

delete [ ] data; / / массив (а не указатель) удаляется

Обратите внимание на квадратные скобки в операции delete при освобожде­ нии занятой массивом памяти. Операция delete не настолько интеллектуальна, чтобы понять, на что ссылается указатель — на массив или на отдельное значе­ ние. Скобки показывают, что удаляется массив, а не переменная. Здесь также вы­ бран компромисс между интересами программиста и разработчиков компилятора. C + + облегчает жизнь создателям компилятора.

При копировании существующего массива в новый перед освобождением имею­ щегося массива для ограничения цикла используется переменная count, а не size, хотя данный сегмент начинается с проверки count == size. В момент копирования

224

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

 

они не равны, так как перед распределением массива значение size было увели­

 

чено. Возможно, лучше увеличивать size после копирования.

 

i f (count == size)

/ / нет памяти, запросить еще

{ double

*q = new d o u b l e [ 2 * s i z e ] ;

 

/ /

удвоить размер

массива

cout

«

"Выделена дополнительная

память: размер = " «

size

«

endl;

f o r

( i n t

i=0; i

< s i z e ; i++)

 

/ /

копирование

старых данных

q

[ i ]

= data

[ i ] ;

 

 

 

 

 

 

size

*=

2;

 

/ /

удвоить размер для следующей проверки

delete

[ ]

data;

 

/ /

не забывайте об освобождении памяти

data

= q;

}

 

/ /

привязка главного

указателя

 

На рис. 6.11 сведены операции для обработки ситуации переполнения массива. Рис. 6.11а демонстрирует выделенный в динамически распределяемой области массив data[] и переменные в стеке — amount, count и size, когда обнаружено переполнение (под data[ ] понимается ссылка на массив data в динамически рас­ пределяемой области). На рис. 6.1 lb приведен массив q[ ] в динамически распре­ деляемой области после копирования значений из массива data[ ] и массив data[ ] после возврата памяти в динамическую область. На рис. 6.11с демонстрируются оба указателя, data и q, ссылающиеся на новый массив в динамически распреде­ ляемой области, а на рис. 6.1 Id — массив после удаления указателя q в соответ­ ствии с правилами области действия.

^^' g

count

• — • f i ^

 

 

 

g |Т|

 

 

 

 

 

 

 

 

 

 

44

 

 

 

amount

size

data

 

 

 

 

 

 

 

 

count

D-

 

 

 

- ^ ^

^

Возвращено

В)

Гбб] ГзП Гб]

-V 22 33

1 \ X ^ ^ в динамически

^ I

/

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

 

amount

size

 

 

 

^^^^

 

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

 

data

 

 

 

 

 

 

 

 

 

 

 

- >

22

33

44

 

 

 

count

 

 

 

 

 

 

 

 

amount

size

data N.

 

 

 

 

 

 

 

 

 

 

D- • >

22

33

44

 

 

D)

count

D- ->•

 

 

 

 

 

^

 

з|

22

33

44

55

 

 

amount

 

size

data

 

 

 

 

 

 

Рис. 6-11. Последовательность действий при обработ^ке ситпуации переполнения массива

Следующий пример полезного динамического массива связан с вводом текста. Когда программа получает вводимые пользователем символьные данные (напри­ мер, фамилию заказчика и пр.), то трудно представить, что потребуется массив больше 30—50 символов. А вдруг на клавиатуре залипла клавиша? Данные пере­ полнят этот короткий массив и запортят содержимое памяти. Кроме того, при чте­ нии данных из файла или из телекоммуникационной линии нет никакой гарантии, что размер будет ограничиваться конкретным значением. Риск порчи содержимого памяти всегда есть, как бы ни был велик резервируемый массив фиксированного размера.

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

225 J

В листинге 6.11 показано решение данной проблемы. Идея состоит в том, что­ бы вводить данные в относительно короткий именованный массив в стеке (buf[]) и копировать их в массив в динамически распределяемой области (на который ссылается указатель data). Если данные продолжают поступать, они снова считываются в стек (записываются поверх данных, уже скопированных в динамический массив), затем в динамически распределяемой памяти выделяется массив больше­ го размера (указатель temp), в него копируются данные из предыдуш,его динамиче­ ского массива (указательdata) и добавляются данные из массива в стеке (buf [ ]).

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

#include <iostream>

using namespace std;

int

main(void)

 

 

 

 

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

 

/ /

короткий массив для отладки

const int

LEN = 8;

int

len

= 1;

 

 

 

char

buf[LEN],

*data

= 0;

 

 

 

 

/ /

инициализировать нулем при первом проходе

cout

«

"Наберите текст, нажмите Enter: \ n "

 

 

 

 

do {

 

 

 

 

 

 

 

 

 

 

 

 

/ /

данные поступают в массив в стеке

cin.get(buf,LEN);

 

 

 

 

 

 

 

len

+=

strlen(buf);

 

 

 

 

 

 

 

/ /

общая длина старых данных

char

*temp = new char(len);

 

 

 

 

/ /

запрос нового динамического массива

i f

(temp == 0)

 

 

 

 

 

 

 

 

/ /

проверка успешности распределения

 

{

cout

«

Нет памяти: программа завершена\п"

 

 

 

 

 

return 0;

}

 

 

 

 

 

 

 

/ / неудача, отказ

i f

(data

== 0)

 

 

 

 

 

 

 

 

/ /

копирование данных из буфера ввода

 

strcpy(temp,buf);

}

 

 

 

 

 

else

 

 

 

 

 

 

strcat(temp,buf);

}

/ /

копирование данных

 

{strcpy(temp,data);

 

delete [] data;

 

 

 

 

 

 

 

/ /

удалить существующий массив

 

data = temp;

 

 

 

 

 

 

 

 

/ /

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

 

cout

«

" Всего: "

«

len

«

добавлено

«

buf

«

endl;

 

 

cout

«

Динамический буфер «

data

«

endl

/ /

отладка

 

char

ch = cin. peek();

 

 

 

 

 

 

/ /

что осталось

в буфере?

i f

(ch

==

' \ n ' )

 

 

 

 

 

 

 

/ /

выйти, если

новая строка

 

{ ch = cin . getO;

break;

}

 

 

 

/ /

или продолжать до конца файла

} while

(true)

 

 

 

 

 

 

 

 

cout

«

"\пВы^ввели

следующую строку:

\ n \ n " ;

/ /

тот же синтаксис, что и для массивов

cout

«

data «

endl;

 

 

 

 

 

 

delete

[ ]

data;

 

 

 

 

 

 

 

 

 

 

 

return

0;

 

 

 

 

 

 

 

 

 

 

 

 

 

В листинге 6. И пользователь вводит данные в массив buf[] в стеке, размер которого для демонстрации работы алгоритма специально выбран небольшим (LEN = 8 символам). Функция get() считывает вводимые данные, пока не получит LEN - 1 символов или не найдет в потоке ввода символ новой строки. (Его следует удалить другими способами, например вызовом функции get(), считываюидей ровно один символ.)

В других случаях get() добавляет к строке в buf[] завершающий ноль. Далее программа отводит len символов в динамической области, чтобы вместить считанные из buf[] символы. В первый раз это len = len + strlen(buf), где len содержит 1, а strlenO — число символов без завершаюш,его нуля. Если распределение памяти не удалось (указатель temp установлен в 0), то программа завершает работу.

226 Часть I « Введение в г1рограг«1мирование на C-^-i^

При первом проходе цикла (указатель data все еще инициализирован нулем) этим все заканчивается: программа копирует введенные данные из buf [ ] в массив, на который указывает temp. Здесь эквивалентность между указателем и традици­ онной записью массива оказывается очень удобной. Передается указатель temp на функцию strcpyO, и копируются символы из buf[] в массив, на который ука­ зывает temp.

Если это не первый проход цикла (указатель data не ноль, он ссылается на массив, выделенный ранее в динамической области), все усложняется. Сначала программа копирует предыдущие данные во вновь распределенный массив с по­ мощью strcpyO, а затем в конец предыдущих данных вызовом функции strcatO присоединяются символы из buf[].

Теперь указатель temp ссылается на обновленную строку ввода, а указатель data — на предыдущие данные. Далее программа удаляет предыдущие данные и устанавливает указатель data на обновленную строку ввода.

Следующая задача — определить, что произошло при вызове get(). Был ли ввод прекращен из-за обнаружения символа новой строки (который остался в строке ввода) или из-за того, что введено LEN - 1 символов и массив but[ ] пере­ полнен? Чтобы выяснить это, программа анализирует следующий введенный сим­ вол, вызывая функцию реек(). Если это действительно символ новый строки (что будет иметь место для короткой строки ввода), программа удаляет его, вызывая другую функцию get(), которая считывает один символ и завершает цикл.

Если введенная строка не помещается в буфер ввода, функция get() в начале цикла do завершает ввод после считывания LEN - 1 символов и добавляет в конец массива buf [ ] нулевой терминатор. Функция реек() будет возвращать следующий символ, отличный от символа новой строки. Не стоит удалять его из потока ввода, ведь это будет первый символ, считываемый при следующем вызове get().

При следующей итерации цикла do оператор get() в начале цикла считывает следующую порцию символов в массив buf[]. На этой итерации нужно скопиро­ вать данные из buf[] в динамический массив.

На этот раз массив, на который ссылается символьный указатель data, уже есть. Его длина (включая нулевой завершающий символ) определяется перемен­ ной 1еп. Программа использует локальный указатель temp для выделения в дина­ мической области нового массива (с указателем data) достаточного размера для размещения существующего массива в динамически распределяемой области и вновь введенных символов в buf[]. Вот откуда взялось выражение 1еп+= strlen(buf). Программа наполняет вновь выделенный массив, копируя в него массив с указате­ лем data и соединяя результат с содержимым массива buf [ ]. После этого она уда­ ляет имеющийся массив, на который ссылается data, и устанавливает указатель data на вновь распределенный массив (на него указывает temp). Итерации продол­ жаются, пока следующий вызов реек() не обнаружит символ новой строки или конца файла.

Наберите текст, нажмите Enter: Hello World!

Всего: 8 добавлено: Hello W Динамический буфер: Hello W Всего: 13 добавлено: orldl Динамический буфер: Hello World!

Вы ввели следующую строку:

Hello World!

Рис. 6.12. Вывод программы из листинга 6.11 (с отладочными сообщениями)

Результат выполнения программы показан на рис. 6.12. Здесь массивы в динамически распределяемой области

памяти также не имеют имен. На них можно ссылаться через указатели, а память для указателей выделяется в стеке. (Поэтому указатели имеют имена temp и data.) Программа использует эти указатели точно так же, как массивы, для которых память выделяется в стеке. Например, указатели temp и data передаются функциям strcpyO, strcatO и strlenO как обычный массив buf[]. Это же относится к операции вставки « в конце листинга 6.11. Указатель data используется как имя массива. Разница в том, что память для именованных массивов возвращается по правилам языка в конце области действия, а динамические массивы освобождаются с помощью явной операции delete (обратите внимание на скобки в опера­ ции delete).

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

227 " 1

На рис. 6.13 показаны операции распределения памяти для ввода данных из рис. 6.12. Рис. 6.13а демонстрирует заполнение массива buf[] строкой "Hello W", когда указатель data устанавливается в 0. Из рис. 6.13Ь видно, что переменная 1еп содержит 8, temp указывает на массив в динамически распределяемой области из 8 символов, а data указывает на тот же массив. (Обратите внимание, что операция delete с нулевым указателем не дает никакого эффекта.) Рис. 6.13с демонстри­ рует массив buf[] после ввода "orld!". Рис. 6.13d показывает, что 1еп содер­ жит 13, temp указывает на массив со строкой "Hello World!", массив, на который ссылается data, удаляется, а data указывает на тот же массив, что и temp.

*' и

\^HL

 

[H" e

1

1 0

wlol

1еп

data

 

buf[8]

 

 

 

В) 8

 

W

н1е

1

1 0

Iwlol

1еп d a t a r y

 

1 1

 

 

 

 

buf[8]

 

 

 

 

I I temp

 

 

 

 

 

•=' и П-^И!

W

0 r

1

d^ ! 0

 

 

 

 

len

data

 

buf[8]

 

 

 

 

 

 

0 r

1 d ! 0

 

 

 

 

buf[8]

 

 

 

'temp

Рис. 6.13. Диаграмма для указателей при вводе данных в соответствии с рис. 6.12

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

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

Если материал вам вполне понятен, идите дальше. В предыдуш,ем примере вводилась одна строка данных произвольной длины. В СН-+ с его распределяе­ мыми в стеке массивами фиксированной длины для работы с такими строками требуется определенное искусство. Аналогичные методы можно применять во мно­ гих реальных приложениях.

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

228

Часть I ^ Введение в прог;. •; <

В листинге 6.12 приведен алгоритм, реализованный в листинге 6.11,— он ис­ пользуется здесь как внутренний цикл. Внешний цикл продолжает чтение данных, пока пользователь не нажмет Enter, не набрав в строке никаких данных. Такая пустая строка служит контрольным значением, завершаюш^им ввод.

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

#inclucle

<iostream>

 

 

 

 

 

 

 

 

 

 

 

using namespace std;

 

 

 

 

 

 

 

 

 

 

 

int main(voicl)

 

 

 

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

const int

LEN = 8; char

buf[LEN];

 

 

 

 

 

 

 

 

 

int

cnt

= 0;

 

 

 

 

 

 

 

 

 

 

 

cout

«

"Наберите текст

(или просто нажмите Enter, чтобы закончить ввод): \ п " ;

do {

 

 

 

 

 

 

 

 

 

 

 

/ /

начать внешний цикл для ввода строк

char

*data

= new char[1]; data[0]

= 0;

 

 

/ /

первоначально пусто

 

int

len

= 0;

 

 

 

 

 

 

 

/ /

сначала

размер равен О

do {

 

 

 

 

 

 

 

 

 

 

 

/ /

начало

внутреннего цикла для

 

 

 

 

 

 

 

 

 

 

 

 

/ /

сегментов строк

 

cin.get(buf,LEN);

 

 

 

 

 

 

 

/ /

данные поступают в массив в стеке

len += strlen(buf);

 

 

 

 

 

 

 

/ /

общая длина старых данных

char

*temp = new char(len+1);

 

 

 

 

 

 

 

 

 

strcpy(temp,data);

strcat(temp,buf);

 

 

 

 

 

 

delete

data;

 

 

 

 

 

 

 

 

 

 

 

data

= temp;

 

 

 

 

 

 

 

/ /

расширение длины строки

cout

«

"Выделено «

len+1 « ":

"

«

data

«

endl;

 

 

 

char ch = cin.peekO;

 

 

 

 

 

 

/ /

что осталось в буфере?

 

 

i f

(ch == ' \ n '

II

ch == EOF)

 

 

 

 

/ /

выход, если новая строка

 

 

 

{

ch = cin . getO;

 

 

 

 

 

/ /

удалить из ввода

 

 

 

 

 

break; }

 

 

 

 

 

 

 

 

 

 

 

}

while

(true);

 

 

 

 

 

 

 

 

 

 

 

i f

(len

== 0) break;

 

 

 

 

 

 

 

/ /

завершение на пустой

строке

cout

«

"

строка " «

 

++cnt «

":

"

«

data

«

endl;

 

 

 

delete

[ ]

data;

 

 

 

 

 

 

 

 

 

 

 

} while

(true);

 

 

 

 

 

 

 

/ /

продолжать до пустой

строки

return 0;

 

 

 

 

 

 

 

 

 

 

 

 

 

Между программами в листинге 6.11 и 6.12 есть несколько интересных раз­ личий. В листинге 6.11 переменная len обозначает размер массива, выделенного в динамической области. В листинге 6.12 переменная len обозначает число сим­ волов, копируемых в массив в динамически распределяемой области. Размер массива на единицу больше, чтобы вместить нулевой терминатор.

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

i f (data == 0)

len = strlen(buf) + 1 ;

// первый раз копируется только из buf[]

else

// в противном случае копируется изdata[] и buf[]

sen = strlen(data)+strlen(buf)+1;

Глава 6 # Управление па1У1ятью

229

Кроме того, при первом чтении массив в динамически распределяемой области принимает данные только из buf [ ]. Во время других итераций во вновь выделенный в динамической области массив копируются данные из существующего динамиче­ ского массива и из массива buf [ ]. Вот почему листинг 6.11 содержит оператор if:

i f

(data == 0)

 

 

 

 

strcpyCtemp,buf);

/ /

первый раз

копировать только

из buf[]

else

/ /

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

из data[] и buf[]

{

strcpy(tefnp,data),

strcat(tenip, buf);

}

 

Листинг 6.11 не содержит первых операторов if. Нередко программисты чув­ ствуют, что дополнительные проверки усложняют программу и пытаются избе­ жать их путем более разумного использования данных при обработке различных ситуаций. В листинге 6.11 data инициализируется нулем, а 1еп — единицей. Сле­ довательно, можно использовать оба случая в следующем операторе:

1еп = 1еп + strlen(buf);

/ / работает для первого и следующего чтения

Тестирование программы показывает, что она работает, но все равно остается некоторое беспокойство. Данный оператор нуждается в пояснении (и тщательной проверке). Расположенный выше оператор if говорит сам за себя. Что предпочти­ тельнее: понятный с первого взгляда объемный исходный код или более компакт­ ный вариант, нуждающийся в дополнительных пояснениях? В предыдущих главах говорилось одно, но теперь я хочу сказать о другом.

В листинге 6.12 нужно отметить еще один момент. Указатель data первонача­ льно ссылается на массив в динамически распределяемой области, у которого раз­ мер равен 1. Массив этот содержит единственный символ — нулевой терминатор (обычно такую строку называют пустой). Переменная 1еп инициализируется зна­ чением О — длиной данной пустой строки:

int

len = 0;

/ /

начальная длина данных

char

*data = new char[1]; data[0] = ' \ 0 ' ;

/ /

пустая строка

В то же время нет никакой разницы ме>кду первой итерацией и всеми остальными. В программе добавляется длина buf[] к длине data[], затем data[] копируется в новый массив в динамически распределяемой области (сначала это пустая стро­ ка, а потом к ней добавляется содержимое buf[]:

do {

/ /

начало внутреннего цикла

cin.get(buf, LEN);

/ /

получить

следующий сегмент строки

1еп += strlen(buf);

/ /

обновить общую длину строки

char *temp = new char[len+1];

/ /

выделить

новый массив в heap

strcpy(temp,data); strcat(temp,buf);

 

/ / слить данные

Неплохой код, но, честно говоря, вариант с двумя операторами if более понятен. Еще один вопрос — завершение работы программы. Если пользователь на­ жимает Enter (Return), программа из листинга 6.12 должна заканчивать работу. Предполагается, что при этом вводится символ новой строки '\п' (ASCII 10), а вызов функции реек() принимает его и завершает внутренний цикл. Проверка len == О прерывает внешний цикл. Но только не на моей машине. Когда я ввожу символы и нажимаю Enter, вводится символ новой строки, но если просто нажать

Enter, вводится символ "конца файла" (EOF). Вот поэтому в листинге 6.12 прове­ ряется как символ новой строки, так и EOF (константа со значением -1):

i f

(ch == ' \ n ' I I ch == EOF)

/ /

выход, если новая строка или EOF

{

ch = cin . getO; break; }

/ /

но сначала удалить его из буфера ввода

Кстати, программа из листинга 6.11 этого не делает, а следовательно, при на­ жатии Enter без набора символов, она входит в бесконечный цикл, не встречая символов новой строки, удовлетворяющих условию if. Такая хорошая програм­ ма — и такая ошибка! Обидно.

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