Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

Итак, что такое массив? Как его определить и как использовать? Массив — это однородная последовательность объектов, расположенных в смежных ячейках памяти; иначе говоря, все элементы массива имеют один и тот же тип, и между ними нет пробелов. Элементы массива нумеруются, начиная с нуля в возрастающем порядке. В объявлении массив выделяется квадратными скобками.

const int max = 100;

int gai[max]; // глобальный массив (из 100 чисел типа int);

// "живет всегда"

void f(int n)

{

char lac[20]; //
локальный массив; "живет" до конца области

// видимости

int lai[60];

double lad[n]; // ошибка: размер массива не является константой

// ...

}

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

vector
с массивами элементов.

Как и к элементам массивов, размещенных в свободной области, доступ к элементам именованных массивов осуществляется с помощью операторов индексирования и разыменования (

[ ]
и
*
). Рассмотрим пример.

void f2

{

char lac[20]; // локальный массив; "живет" до конца области

// видимости

lac[7] = 'a';

*lac = 'b'; // эквивалент инструкции lac[0]='b'

lac[–2] = 'b'; // ??

lac[200] = 'c'; // ??

}

Эта функция компилируется, но, как мы знаем, не все скомпилированные функции работают правильно. Использование оператора
[ ]
очевидно, но проверка выхода за пределы допустимого диапазона отсутствует, поэтому функция
f2
компилируется, а результат записи
lac[–2]
и
lac[200]
приводит к катастрофе (как всегда, при выходе за пределы допустимого диапазона). Не делайте этого. Массивы не проверяют выход за пределы допустимого диапазона. И снова здесь нам приходится непосредственно работать с физической памятью, так как на системную поддержку рассчитывать не приходится.

А не мог ли компилятор как-то увидеть, что массив
lac
содержит только двадцать элементов, так что выражение
lac[200]
— это ошибка? В принципе мог бы, но, как нам известно, в настоящее время не существует ни одного такого компилятора. Дело в том, что отследить границы массива на этапе компиляции невозможно в принципе, а перехват простейших ошибок (таких как приведены выше) не решает всех проблем.

18.5.1. Указатели на элементы массива

Указатель может ссылаться на элемент массива. Рассмотрим пример.

double ad[10];

double* p = &ad[5]; // ссылается на элемент ad[5]

Указатель

p
ссылается на переменную типа
double
, известную как
ad[5]
.

Этот указатель можно индексировать и разыменовывать.

*p =7;

p[2] = 6;

p[–3] = 9;

Теперь

ситуация выглядит следующим образом.

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

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

p += 2; // переносим указатель p на два элемента вправо

Итак, приходим к следующей ситуации.

Аналогично,

p –= 5; // переносим указатель p на пять элементов вправо

В итоге получим следующее.

Использование операций
+
,
,
+=
и
–=
для переноса указателей называется арифметикой указателей (pointer arithmetic). Очевидно, поступая так, мы должны проявлять большую осторожность, чтобы не выйти за пределы массива.

p += 1000; // абсурд: p ссылается на массив, содержащий

// только 10 чисел

double d = *p; // незаконно: возможно неправильное значение

// (совершенно непредсказуемое)

*p = 12.34; // незаконно: можно задеть неизвестные данные

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

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

++
) для ссылки на следующий элемент и декрементация указателя (с помощью оператора
––
) для ссылки на предыдущий элемент. Например, мы могли вы вывести элементы массива ad следующим образом:

for (double* p = &ad[0]; p<&ad[10]; ++p) cout << *p << '\n';

И в обратном порядке:

for (double* p = &ad[9]; p>=&ad[0]; ––p) cout << *p << '\n';

Это использование арифметики указателей не слишком широко распространено. Однако, по нашему мнению, последний (“обратный”) пример небезопасен. Почему

&ad[9]
, а не
&ad[10]
? Почему
>=
, а не
>
? Эти примеры были бы одинаково хороши (и одинаково эффективны), если бы мы использовали индексацию. Кроме того, они были бы совершенно эквивалентны в классе
vector
, в котором проверка выхода за пределы допустимого диапазона осуществляется проще.

Поделиться с друзьями: