Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI 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 » Управление памятью |
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 + + допускает в качестве размеров массивов только константы. Именно в таких ситуациях полезно динамическое распределение памяти.
Глава 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[].