Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI |
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 — имя указателя, ссылающегося на неименованный массив в динамически распреде ляемой области.
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), то программа завершает работу.
Глава 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. Такая операция никакого эффекта не дает.
Если при первом чтении вы чувствуете, что тема слишком сложна, пропустите материал. По мере приобретения опыта управление памятью будет казаться вам все менее сложным. Однако для получения такого опыта нужно поупражняться на простых примерах, составлении диаграмм (подобных представленным в данной главе), развивать навыки отладки и интуицию.
Если материал вам вполне понятен, идите дальше. В предыдуш,ем примере вводилась одна строка данных произвольной длины. В СН-+ с его распределяе мыми в стеке массивами фиксированной длины для работы с такими строками требуется определенное искусство. Аналогичные методы можно применять во мно гих реальных приложениях.
В следуюш.ем примере усложняется предыдуилая задача: здесь может считываться любое число строк произвольной длины. Конечно, нет никакого смысла просто так считывать данные с клавиатуры, поэтому показаны также методы сохранения информации в файле на диске.
Глава 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. Такая хорошая програм ма — и такая ошибка! Обидно.