Этот указатель можно индексировать и разыменовывать.
*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
, в котором проверка выхода за пределы допустимого диапазона осуществляется проще.
Отметим, что в большинстве реальных программ арифметика указателей связана с передачей указателя в качестве аргумента функции. В этом случае компилятор не знает, на сколько элементов ссылается указатель, и вы должны следить за этим сами. Этой ситуации необходимо избегать всеми силами.
Почему в языке C++ вообще разрешена арифметика указателей? Ведь это так хлопотно и не дает ничего нового по сравнению с тем, что можно сделать с помощью индексирования. Рассмотрим пример.
double* p1 = &ad[0];
double* p2 = p1+7;
double* p3 = &p1[7];
if (p2 != p3) cout << "impossible!\n";
В основном это произошло по историческим причинам. Эти правила были разработаны для языка C несколько десяткой лет назад, и отменить их невозможно, не выбросив в мусорную корзину огромное количество программ. Частично это объясняется тем, что арифметика указателей обеспечивает определенное удобство в некоторых низкоуровневых приложениях, например в механизме управления памятью.
18.5.2. Указатели и массивы
Имя массива относится ко всем элементам массива. Рассмотрим пример.
char ch[100];
Размер массива
ch
, т.е.
sizeof(ch)
, равен 100. Однако имя массива без видимых причин превращается в указатель.
char* p = ch;
Здесь указатель
p
инициализируется адресом
&ch[0]
, а размер
sizeof(p)
равен 4 (а не 100). Это свойство может быть полезным. Например, рассмотрим функцию
strlen()
, подсчитывающую количество символов в массиве символов, завершающимся нулем.
int strlen(const char* p) // аналогична стандартной
// функции strlen()
{
int count = 0;
while (*p) { ++count; ++p; }
return count;
}
Теперь можем вызвать ее как с аргументом
strlen(ch)
, так и с аргументом
strlen(&ch[0]
). Возможно, вы заметили, что такое обозначение дает очень небольшое преимущество, и мы с вами согласны. Одна из причин, по которым имена массивов могут превращаться в указатели, состоит в желании избежать передачи большого объема данных по значению. Рассмотрим пример.