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

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

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

СжГ]

 

Часть I« Введение в программирование на 04-+

Листинг 5.1

Ошибки при переборе массива

 

#inclucle <iostream>

// или #include <iostream.h>

 

using namespace std;

 

 

int mainO

 

 

 

 

 

int size[5] = { 39, 40. 41, 42, 43

};

 

 

for (int i = 1; i <= 5; i++)

// плохое начало, плохой конец

 

 

cout «

" " «

size[i]; cout «

endl;

 

 

return 0;

 

 

39

40

41

42

43

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

ка. Но иногда, если программист упорно делает ошибки, проверка резуль­

 

 

 

 

 

 

 

 

 

 

тата ошибки

не показывает. В листинге 5.2 представлена программа,

 

 

 

 

 

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

Рис. 5.2.

вывод

выводяш^ая. Она не использует принадлежаш,его массиву адреса side[0].

Правильный

Вместо этого она работает с не принадлежаш,им массиву адресом side[5].

показывает

ошибку

Вывод корректен, хотя программа портит ячейку памяти по адресу

в обработке

массива

side[5].

 

 

 

 

 

 

 

Листинг 5.2.

Ошибка, скрытая корректным выводом

 

#include <iostream>

 

// или #include <iostream.h>

 

using namespace std;

 

 

int mainO

 

 

 

 

{

 

 

 

 

 

 

int size[5;

 

 

 

size[1] = 39; size[2] = 40; size[3] = 41; size[4] = 42; size[5] = 43;

 

for (int i = 1; i <= 5; i++)

// плохое начало, плохой конец

 

 

cout «

" " «

size[i]; cout « endl;

 

return 0;

 

 

 

Насколько опасна испорченная память? Если эта память в данной программе не выделяется под что-то полезное (а есть немало машин с большими объемами свободной памяти), то проблем не будет. Если же такая память используется программой, то ошибку найти довольно труд­ но. Как показано в листинге 5.2, трудно даже понять, что программа некорректна и где начинать искать ошибку. Из рис. 5.3 видно, что некор­ ректно значение а[0]. Оно изменяется с 11 на 43 даже при отсутствии второго присваивания а[0]. На вашей машине данная программа может портить содержимое памяти по-другому. Как бы то ни было, эта невинная на вид программа некорректна.

39

40

41

42

43

43

11

12

 

 

Рис. 5.3.

Массив а[] испорчен из-за обрабоппки массива side[]

Правильная итерация по компонентам массива должна начинаться с О, а не с 1. Итерацию следует заканчивать на одно значение меньше, чем размер массива. Если размер равен 5, то корректной формой проверки будет i < 5; если размер равен 3, то корректная форма проверки i < 3. В обш.ем случае, если число допус­ тимых элементов массива хранится в переменной NUM, то корректной формой проверки на продолжение будет i < NUM. Перебор массива а[] в листинге 5.3 по­ строен корректно. Заметим, что индекс определяется в первом цикле, а не в на­ чале программы. Его имя известно только до конца функции. Следовательно, второй цикл не определяет эту переменную, а использует ее, как если бы она была

Глава 5 • Агрегирование с помощью типов, определяемых программистов

[ 151 |

опредедена в начале функции. Мой компилятор (Microsoft Visual C++ 6.0) некор­ ректно реализует стандарт C+ + : областью действия индексной переменной i должен быть только первый цикл, а не вся функция.

Листинг 5.3. Ошибка в одном месте портит содержимое памяти в другом

#inclucle

<iostream.h>

 

 

 

void mainO

 

 

 

 

 

 

{ int a[3]; int size[5];

 

 

 

a[1]=11;

a[2]=12;

a[3]=13;

/ /

жертва порчи памяти

size[1]

= 39;

size[2] = 40; size[3]

= 41; size[4] = 42; size[5] = 43;

 

for (int i = 1; i

<= 5; i++)

/ /

плохое начало,

плохой конец

cout

«

" "

«

s i z e [ i ] ;

 

 

 

cout «

endl;

 

 

 

 

 

for ( i = 0; i

< 3;

i++)

/ /

хорошее начало

и конец

cout

«

" "

«

a [ i ] ;

 

 

 

cout «

endl;

 

 

 

 

 

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

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

Многомерные массивы

C++ поддерживает многомерные массивы. Теоретически на число размерно­ стей массива никаких ограничений не налагается. При определении многомерных массивов задается тип компонентов массива, имя массива, а потом, в отдельных квадратных скобках, число элементов в первом "измерении", втором, третьем и т. д. Например, двумерный массив целых чисел из двух строк и трех столбцов (пусть пока он будет простым) можно определить так:

int т [ 2 ] [ 3 ] ;

/ / 2 строки массивов по 3 элемента в каждом

Многомерные массивы могут инициализироваться с помош,ью синтаксиса, аналогичного инициализации одномерных массивов. Начальные значения пере­ числены в блоке с разделителями-запятыми:

int m[2][3] = { 10, 20, 30, 40, 50, 60 };

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

int m[2][3] = { { 10. 20, 30 }, { 40. 50, 60 } };

152

Чость I ^ Введение в nporpaiviivi^ipoiaHiie на С+Ф

Начальных значений, как и в одномерных массивах, может быть меньше, чем число элементов массива. Остальным элементам будет присваиваться нулевое значение. Например:

i nt m[2][3] = { { 10, 20 }, { 30. 40 } };

Это эквивалентно следуюш,ему явному определению, где первые три значения попадают в первую строку, а последние — во вторую:

int m[2][3] = { 10, 20, О, 30, 40, О };

Подобно одномерным массивам, число начальных значений не должно превышать числа элементов в строке:

int m[2][3] = { { 10, 20, 30, 40 }, { 50, 60 } };

/ / ошибка

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

int т [ ] [ 3 ] = { { 10, 20. 30 }. { 40, 50. 60 } };

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

int m[2][] = { { 10, 20, 30 }, { 40, 50, 60 / / ошибка

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

Для доступа к элементам многомерного массива нужно несколько индексов — по одному на ка>кдую размерность. Аналогично одномерным массивам, каждый индекс представляет смещение элемента, а следовательно, начинается с О и закан­ чивается значением, на единицу меньшим числа элементов массива. Например, первый элемент второй строки матрицы т[][] обозначается как т[1][0]. Это обо­ значение можно использовать как г-значение (операнд выражения), а также как 1-значение (цель присваивания).

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

В листинге 5.4 показан вложенный цикл, который построчно выводит на экран каждый элемент матрицы т[ ][ ]. Внутренний цикл изменяет индекс j от О до 2 для каждого значения внешнего цикла i (который меняется от О до 1). Вывод программы показан на рис. 5.4.

Листинг 5.4. Пример операций с двумерным массивом

#inclucle <iostream.h>

 

void mainO

 

{ const int ROWS = 2, COLS = 3;

40, 50, 60 } };

int m[ROWS][COLS] = { { 10, 20. 30 },

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

// один раз для каждой строки

{ for(int j=0; j<COLS; j++)

// для каждого индекса i

cout « " " « m[i][j];

// конец строки: один раз для каждого индекса i

cout « endl; }

}

 

Рис. 5.5.
Вывод программы
из листинга 5.4 с fn[i][j], записанным как m[i,j]

Глава 5 • Агрегирование с помощью типов, определяемых программистом

153

Программисты, знакомые с другими языками, считают это обозначе­

10

20

30

 

ние многомерных массивов трудноватым (или, по крайней мере, новым).

 

Иногда они ошибочно используют только один набор квадратных скобок

40

50

60

 

 

 

 

 

и разделяют индексы запятыми. Например, вместо т[1][0] программист

 

 

 

 

может использовать т[1,0]. К сожалению, компилятор в этом случае

Рис. 5.4.

массив:

не сообщает об ошибке. Вместо этого он спокойно принимает данное вы­

Двумерный

ражение с разделителями-запятыми и предоставляет программисту само­

построчный

вывод

му разбираться в некорректности программы. На рис. 5.5 показан вывод

 

 

 

 

программы из листинга 5.4, где m[i][j] ошибочно записано как m[i, j ] .

 

 

 

 

 

 

 

Причины здесь две, и обе унаследованы из языка С. Одна из них

0х34С4

0х34СА

0x3400

в том, что запятая — полноправная операция С-1-+. Когда компилятор

0х34С4

0х34СА

0x3400

вычисляет разделенное запятыми индексное выражение [i, j ] , он сна­

чала вычисляет i (или 1), затем находит запятую, опускает значение i и вычисляет следуюидее выражение (т. е. 0). Затем компилятор возвра- ш,аетданное значение в качестве индекса. Для различных целей m[i][ j ] можно записать как m[j]. Вторая причина в том, что m[j] с одним индексом — допустимое обозначение строки со сменлением j . В много­ мерном массиве не обязательно указывать все индексы.

Внимание Для ссылки на компонент многомерного массива используйте форму с двумя наборами квадратных скобок: a [ i ] [ j ] .

Не применяйте разделитель-запятую, как в a [ i , j ] . Это ведет к проблемам.

Многомерные массивы поддерживаются в языке C + + как синтаксическое излишество — только для удобства программиста. "Внутри" они реализуются как одномерные массивы. Некоторые программисты предпочитают использовать одномерные массивы с компонентами ROWS*COLS и вычислять индекс элемента

в i-той строке и j-той строке как i*COLS+j. (Не забывайте: индексы начинаются

сО и заканчиваются значением R0WS*C0LS-1). Листинг 5.5 показывает программу из листинга 5.4, где массив явным образом интерпретируется как одномерный. Вывод данной программы (см. рис. 5.5) будет таким же, как на рис. 5.4.

Листинг 5.5. Использование одномерного массива для реализации матрицы

#inclucle <iostream> using namespace std;

int mainO

const int

ROWS = 2,

COLS = 3;

 

 

int

m[ROWS * COLS] = {

10, 20, 30, 40, 50, 60

/ /

TOT же размер

for

(int

i=0;

i < ROWS; i++)

 

 

{ for (int j=0; j

< COLS; j++)

 

 

 

cout

«

"

"

«

m[i*COLS + j ] ;

/ /

трудный способ

 

cout

«

endl;

}

 

 

/ /

конец строки (один раз для каждого i )

return 0;

 

 

 

 

 

 

 

 

Какой способ обработки индексов лучше? Это зависит от приложения и лич­ ных предпочтений, поэтому рекомендации дать трудно.

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

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

Определение символьных массивов

Текст представляется в C++ в виде массивов символов (они часто называются строками), поэтому символьные массивы имеют здесь особое значение. Все, что говорится в данной главе о массивах (одно- и многомерных) применимо и к мас­ сивам символов.

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

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

Чтобы отличить этот код от символа 'О' (в коде ASCII это 48 в десятичном представлении и 0x30 в шестнадцатеричном), контрольное значение часто пред­ ставляется как ESC-последовательность '\0' (О в десятичном представлении и 0x0 в шестнадцатеричном).

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

char

t [ 4 ]

= {

' Н',

' i ' , ' ! ' , ' \ 0 ' );

/ /

четыре

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

cout

« t

«

endl;

 

/ /

выводит

" H i ! "

Здесь массив t[] инициализируется с помощью стандартного синтаксиса для одномерных массивов и передается функции-операции « как аргумент. Эта функция продолжает вывод строки символов, пока не встретит нулевой код. Тогда она прекраш^ает работу.

В строковых литералах завершаюш,ий ESC-символ обязателен, но во многих контекстах можно использовать числовой 0. В следуюш^ем примере он применяет­ ся вместо ESC-символа '\0'. Некоторые программисты предпочитают символь­ ную запись.

char t [ 4 ] = { ' Н ' , ' i ' , ' ! ' , 0 } ;

//некоторые предпочитают ' \ 0 '

Такая запись не совсем обычна для инициализации символьных массивов боль­ шой длины. На этот случай C++ предусматривает специальное решение: вместо набора символьных значений можно использовать символьный литерал. Компи­ лятор поймет, что вы имеете в виду, поместит каждый символ в соответствующую позицию массива и добавит в конец терминатор:

char

t [ 4 ]

= 'Hi! ';

/ / t [ 0 ]

- это 'Н' , t [ 1 ] - это ' i ' , и т.д.

char

u[]

= "Сегодня прекрасный день"

/ /

24 символа с нулевым завершителем

Строковый литерал "Hi!" содержит 4 символа. Четвертый — это код 0. Стро­ ковый литерал "Сегодня прекрасный день" содержит 24 символа, включая код 0. Следовательно, в массиве и[ ] не 23 компонентов, а 24. Для хранения контроль­ ного символа нужен дополнительный элемент массива. Если не зарезервировать для него место, возникнет проблема. Например, такая запись даст синтаксическую ошибку:

char v[3] = " H i ! " ;

/ / Четыре начальных значения для трех элементов

Глава 5 • Агрегирование с помощью типов, определяе^гых програм1^истог^

| 155 |

Можно определить массив символов, где зарезервированного места больше, чем начальных символов, а можно определить такой массив и оставить его содер­ жимое неопределенным:

char last[30]="Jones", f i r s t [ 3 0 ] ;

/ / имеется место

Доступ к обычным строковым компонентам аналогичен обычному массиву. Каждый компонент массива имеет тип char. Первый индекс равен 0.

t [ 0 ] = ' N ' ; t [ 1 ] = ' о ' ;

/ / t [ ] содержит "No!", а не " H i ! "

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

Операции с символьными массивами

Отдельные символы можно присваивать один другому или сравнивать друг

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

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

Функция strcpyO реализует присваивание массивов символов. Она воспри­ нимает два аргумента-массива и копирует компоненты второго аргумента в соответствуюш,ие компоненты первого. Нулевой терминатор также копируется. В результате получается нормально сформированный целевой массив, который можно использовать в аргументах других функций:

strcpy(u,t); / / Теперь и[] также содержит "No!"

Поскольку ни одна функция не просматривает содержимое строки за преде­ лами контрольного значения, очиш.ать остаток строки нет необходимости. Перед вызовом этой функции строка и[] содержала "Сегодня прекрасный день\0", где "\0" — контрольный символ. После вызова функции она будет содержать "Мо!\Одня прекрасный день\0", но все, что содержится после "\0", теперь уже нерелевантно. (Здесь использование ESC-символа обязательно.) Для любых целей содержимым данной строки будет просто "No!".

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

strcpy(t, "Yes");

/ /

теперь t [ ] содержит

"Yes", плюс ноль

strcpy("Yes", t ) ;

/ /

этого делать нельзя

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

Функция strcatO реализует для массивов символов операцию +=. Она также воспринимает два символьных массива как аргумент и копирует второй аргумент в первый. В отличие от strcpyO она не заменяет содержимое первого параметра новым содержимым, а просто добавляет его к текуидему. Результатом будет конка­ тенация двух строк:

strcat(u, " means No!");

/ / u[] содержит "No! means No!"

Добавление символов начинается с позиции, где раньше был нулевой термина­ тор. Этот терминатор и то, что может находиться за ним, затирается — туда поме- ш,ается новое содержимое. Нулевой терминатор вставляется после добавленных символов, в результате в массиве будет "No! means No! \Оный день\0". Поскольку функции C++ не просматривают то, что находится за терминатором, для всех целей это будет просто строка "No! means No!".

I 156

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

 

Функция St гетр () реализует операцию сравнения двух символьных элементов-

 

массивов. Она поочередно сравнивает соответствующие символы, пока не найдет

 

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

 

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

 

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

 

в коде ASCII. Когда строки упорядочены и символ в первом аргументе предше­

 

ствует символу во втором аргументе, strcmpO возвращает - 1 . Если строки не

 

упорядочены и символ во втором аргументе предшествует символу в первом

 

аргументе, strcmpO возвращает 1. При достижении контрольных значений в од­

 

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

 

Например, strcmpC'Hi",

"Hello") вернет 1. Строки не упорядочены. С другой

 

стороны, strcmpC'Handler",

"Hello")

вернет - 1 , как и strcmp("Hell", "Hello"),

 

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

 

второй. Так как в коде ASCII символы в нижнем регистре следуют за символами

 

в верхнем регистре, strcmp("hello",

"Hello") возвращает 1 и выполняет ветвь

 

true следующего оператора:

 

 

 

i f (strcmp("hello", "Hello")) cout «

"He упорядочены\п";

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

предшествующих завершающему нулю, используйте функцию s t r l e n ( ) .

Еще одна полезная библиотечная функция — strlenO, воспринимающая сим­ вольный массив в качестве аргумента и возвращающая число символов в строке {предшествующих нулевому терминатору). Например, strlen("Hello") возвра­ щает 5, а strlen(t) — 3 (t содержит "Hi!"). Все библиотечные функции прекра­ щают просмотр строки, как только встречают контрольный символ.'

Строковые функции и порча содержимого памяти

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

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

В листинге 5.6 приведена программа, непрерывно работающая с доступной об­ ластью памяти. Что может быть проще считывания двух элементов и отображения их на экране? Программа передает функции >> массивы first[ ] и last[ ], предва­ рительно заполнив их вводимыми символами и добавив нулевые терминаторы.

Вывод программы показан на рис. 5.6. Если данных достаточно

Введите имя: John

мало, проблем не будет. Если данные длиннее, то проблема (в этом

Введите фамилию: Johnson

тривиальном примере) становится очевидной. Вряд ли можно считать,

п Johnson

 

что 6 символов для имени и фамилии достаточно. Более того, до­

 

ступны только 5 знакомест, так как шестое занимает терминатор.

Рис 5 6

А 20 символов достаточно? Моя подруга Галина Белосельская-Бело-

Переполнепие массива

зерская столкнулась бы с проблемой — в ее имени-фамилии 32 сим-

при вводе приводит

вола, включая пробел и завершающий 0.

к порче данных

 

^ Глава 5 • Агрегирование с помощью типов, ооределяе^лых nporpa^i^ncTOi^

[ 157 |

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

Листинг 5.6. Простой пример переполнения массива

#inclucle

<iostream>

 

 

using namespace std;

 

 

int mainO

 

 

 

 

{

 

 

 

 

 

 

char

f i r s t [ 6 ] ,

l a s t [ 6 ] ;

/ /

не слишком ли коротки эти массивы?

cout

«

"Введите

имя: ";

/ /

я ввел "John\0" (5 символов)

cin

»

f i r s t ;

 

 

/ /

нет защиты от переполнения

cout

«

"Введите фамилию: "

/ /

я ввел "Johnson\0" (8 символов)

cin

»

last;

 

 

/ /

нет защиты от переполнения

cout

«

f i r s t

«

" " last « endl;

/ /

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

return о;

Здесь first[] содержит строку "John\0" (последние два символа '\0' представля­ ют ячейку памяти с нулевым содержимым). Массив last[] содержит "Johnson\0" (8 символов с завершаюш,им нулем). Между тем, массив last[] содержит только 6 символов. Куда денутся два символа "п\0"? На моей машине массив first[] фактически следует в памяти за массивом last[] . Почему это так — не важно. Но очевидно, два лишних символа куда-то попадут. После ввода фамилии массив first[] содержит два символа "п\0", плюс то, что осталось от "John\0", т. е. "n\Ohn\0". При попытке вывести содержимое first[] с помош,ью cout вывод прекращается после первого ' \ 0 ' , т. е. сразу после ' п'.

Это очень интересно и довольно опасно. На вашей машине программа может работать несколько по-другому. Некоторые компиляторы выделяют место фраг­ ментами по 4 байта, так что массивы будут, фактически, содержать 8 символов, а потому для демонстрации порчи содержимого памяти потребуется более длинные имя/фамилия. Другие компиляторы не размеш.ают массив first[] после массива last[] . Как бы то ни было, важно, что при обработке строк возможна порча содержимого памяти.

Хорошим решением проблемы является динамическое распределение памяти, но пока у нас нет необходимых для этого инструментальных средств. Другое практическое решение — ограничение числа помеш,аемых в массив символов. Функция ввода get() позволяет программисту задать ограничение на число считы­ ваемых символов:

c i n . g e t ( f i r s t , 6 ) ; / / считывает до 5 символов + ноль

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

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

158

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

Использование функции get() порождает две проблемы. Допустим, имя содер­ жит только три символа (например, "Amy"). Далее вводится фамилия:

cin get(last, 6) / / ввод прекращается по концу строки

Первое, что находит данный оператор в буфере ввода — символ новой строки, оставшийся от предыдущего вызова функции get() для чтения "Amy", поэтому он считывает символ новой строки и завершает работу. В результате в массиве last[] оказывается пустая строка. Уловили? То, что набирает пользователь, не попадает в массив last[] и остается в буфере ввода. Если, первый ввод окажется слишком длинным ("Владимир"), то в first[] помеилаются только пять символов с завершаюш,им нулем. Остаток ввода ("мир") будет находиться в буфере, ожидая следуюш^его запроса ввода. Что бы пользователь ни набрал в фамилии, это не попадет в массив last[]. Программа считает символы из буфера ввода ("мир") и остановится, встретив в буфере символ новой строки (он там остается).

Как видно, одна лишь функция get() не в состоянии выполнить задачу. Нужна функция ignoreO, считывающая символы и отбрасывающая их. Для нее необхо­ димо задать верхний предел — число отбрасываемых символов и ограничитель, который прекращает отбрасывание символов (здесь используется символ новой строки).

Введите

имя: John

|

Данное решение проблемы переполнения ввода показано в лис­

тинге 5.7. Эта программа демонстрирует также следующую проблему,

Введите

фамилию: Smith

 

на сей раз относящуюся к копированию и конкатенации. Она форми­

John Smith

 

 

рует имя заказчика из фамилии, запятой, пробела и имени. Если число

Заказчик: Smith. John

 

John n

 

 

 

скопированных в массив name[ ] символов превышает размер массива,

 

 

 

 

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

Рис. 5.7.

 

 

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

 

 

Хотя массив name[] содержит "корректные" данные, массив last[]

Переполнение массива при

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

конкатенации

приводит

 

к порче других

данных

 

грамме нет никаких операторов, явно изменяющих его содержимое.

Листинг 5.7.

Пример переполнения массива при конкатенации

#inclucle

<iostream>

 

 

 

«include

<cstring>

 

 

 

using namespace std;

 

 

 

int mainO

 

 

 

 

char first[6], last[6]; char name[10];

cout «

"Введите имя: ";

 

cin.get(first,6); cin. ignore(2000,'\n');

cout «

"Введите фамилию: ";

 

cin.get(last,6); cin. ignore(2000,'\n');

cout «

first «

"

'«

last «

endl;

strcpy(name,last);

 

 

 

strcat(name,",

" ) ;

 

 

strcat(name,first);

 

 

 

cout «

"Заказчик:

«

name «

endl;

cout «

first «

"

"«

last < endl;

return 0;

//или«include <iostream.h>

//или «include <string.h>

/ /

name = имя, фамилия

/ /

для удаления CR

 

/ /

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

на первом CR

/ /

копирует l a s t [ ]

в name[]

/ /

добавляет запятую и пробел

/ /

добавляет f i r s t [ ] к name[]

/ /

просто для данного примера

Здесь массив first[] содержит "John", а массив last[] — "Smith". Эти мас­ сивы защищены от переполнения при вводе. Затем выполняется конкатенация компонентов имени в массиве name[], например "Smith, John". Данная строка

Глава 5 • Агрегирование с помощью типов, определяемых профоммисто^^

[ 159 |

содержит 12 символов, включая завершающий 0. Так как в массиве name[ ] только 10 символов, последние два символа ("п\0") попадают в другое место. На моей машине они оказались в массиве last[] . В результате массив last[] содержит эти два символа и то, что осталось от "Smith", т. е. "n\Oitn\0". Когда последний оператор программы выводит имя, оно оказывается корректным, а при выводе фамилии появляется только "п".

Чтобы устранить проблему, C++ предлагает библиотечные функции strncpyO

иSt rncat () из заголовочного файла st ring. h. Они аналогичны функциям st ropy()

иstrcatO, но имеют третий аргумент, ограничиваюш,ий число копируемых сим­ волов.

Программа в листинге 5.8 показывает их использование. Функция st rncat О завершает копирование при получении заданного числа символов (или достижении конца второго аргумента) и добавляет нулевой терминатор. Все надежно. Функция strncpyO добавляет ну­ левой терминатор, только когда длина второй строки меньше указан­ ного предела. Если достигается предел, она завершает копирование без добавления контрольного символа. Следовательно, strncpyO не всегда создает правильно сформированную строку. Использовать ее небезопасно. Рис. 5.8 демонстрирует, что применение функции st rncat () не заш.иидает целевой массив от переполнения. В массиве name[] со­ держатся усеченные данные ("Smith, Joh" вместо "Smith, John"), но они сформированы правильно.

Введите имя: John Введите фамилию: Smith

John Smith

После копирования: Smith Заказчик: Smith, Joh John

Рис. 5.8.

Усечение данных предотвращает порчу содержимого памятки

Листинг 5.8. Пример предотвращения переполнения массива при конкатенации строк

ttinclude

<iostream>

 

 

#inclucle

<cstring>

 

 

using namespace std;

 

 

int mainO

 

 

 

{

 

last[6]; name[10];

char first[6],

cout «

"Введите имя: ";

 

cin.get(last,6); cin. ignore(200, '\n'

cout «

"Введите фамилию: ";

 

cin.get(last,6); cin.ignore(200, '\n'

cout «

first «

" " «

last «

endl;

// strncpy(name, last, 4);

 

strcpy(name,last);

 

name « endl;

cout «

"После копирования: " «

strcat(name ",

" ) ;

 

 

strncat(name,first,3);

name «

endl;

cout «

"Заказчик: " «

cout «

first «

" " «

last <endl;

return 0;

}

//для удаления CR

//останавливается на первом CR

//нет нуля, если длина>=счетчика

// просто для данного примера

Похоже, принцип усечения выбран неверно. Массив name[] содержит 10 сим­ волов, а строка "Smith, Joh" — 11 символов, включая нулевой терминатор. Куда денется этот О? На моей машине он оказывается первым символом в массиве last[] . Вместо "Smith" там оказывается "\Omith". Когда последний оператор cout выводит массив last[ ], он находит там нулевой символ и завершает работу, ниче­ го не отображая.

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