Иначе говоря, мы можем индексировать указатель с помощью как положительных, так и отрицательных чисел. Поскольку результаты не выходят за пределы допустимого диапазона, эти выражения являются правильными. Однако выход на пределы допустимого диапазона является незаконным (аналогично массивам, размещенным в свободной памяти; см. раздел 17.4.3). Как правило, выход за пределы массива компилятором не распознается и (рано или поздно) приводит к катастрофе.
Если указатель ссылается на элемент внутри массива, то для его переноса на другой элемент можно использовать операции сложения и вычитания. Рассмотрим пример.
p += 2; // переносим указатель p на два элемента вправо
Итак, приходим к следующей ситуации.
Аналогично,
p –= 5; // переносим указатель p на пять элементов вправо
В итоге получим следующее.
+
, –
, +=
и –=
для переноса указателей называется
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";
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]
). Возможно, вы заметили, что такое обозначение дает очень небольшое преимущество, и мы с вами согласны. Одна из причин, по которым имена массивов могут превращаться в указатели, состоит в желании избежать передачи большого объема данных по значению. Рассмотрим пример.