Вход/Регистрация
Программирование. Принципы и практика использования C++ Исправленное издание
вернуться

Страуструп Бьерн

Шрифт:

double x = *p; // читаем объект, на который ссылается указатель p

*p = 8.9; // записываем объект, на который ссылается указатель p

Когда оператор

[]
применяется к указателю
p
, он интерпретирует память как последовательность объектов (имеющих тип, указанный в объявлении указателя), на первый из который ссылается указатель
p
.

double x = p[3]; // читаем четвертый объект, на который ссылается p

p[3] = 4.4; // записываем четвертый объект, на который

// ссылается p

double y = p[0]; // p[0] - то же самое, что и *p

Вот и все. Здесь нет никаких проверок, никакой тонкой реализации — простой доступ к памяти.

Именно такой простой и оптимально эффективный механизм доступа к памяти нам нужен для реализации класса

vector
.

17.4.3. Диапазоны

Основная проблема, связанная с указателями, заключается в том, что указатель не знает, на какое количество элементов он ссылается. Рассмотрим пример.

double* pd = new double[3];

pd[2] = 2.2;

pd[4] = 4.4;

pd[– 3] = – 3.3;

Может ли указатель
pd
ссылаться на третий элемент
pd[2]
? Может ли он ссылаться на пятый элемент
pd[4]
? Если мы посмотрим на определение указателя
pd
, то ответим “да” и “нет” соответственно. Однако компилятор об этом не знает; он не отслеживает значения указателя. Наш код просто обращается к памяти так, будто она распределена правильно. Компилятор даже не возразит против выражения
pd[–3]
, как будто можно разместить три числа типа
double
перед элементом, на который ссылается указатель
pd
.

Нам не известно, что собой представляют ячейки памяти, на которые ссылаются выражения

pd[–3]
и
pd[4]
. Однако мы знаем, что они не могут использоваться как часть нашего массива, в котором хранятся три числа типа
double
, на которые ссылается указатель
pd
. Вероятнее всего, они являются частью других объектов, и мы просто заблудились. Это плохо. Это катастрофически плохо. Здесь слово “катастрофически” означает либо “моя программа почему-то завершилась аварийно”, либо “моя программа выдает неправильные ответы”. Попытайтесь произнести это вслух; звучит ужасно. Нужно очень многое сделать, чтобы избежать подобных фраз. Выход за пределы допустимого диапазона представляет собой особенно ужасную ошибку, поскольку очевидно, что при этом опасности подвергаются данные, не имеющие отношения к нашей программе. Считывая содержимое ячейки памяти, находящегося за пределами допустимого диапазона, получаем случайное число, которое может быть результатом совершенно других вычислений. Записывая в ячейку памяти, находящуюся за пределами допустимого диапазона, можем перевести какой-то объект в “невозможное” состояние или просто получить совершенно неожиданное и неправильное значение. Такие действия, как правило, остаются незамеченными достаточно долго, поэтому их особенно трудно выявить. Что еще хуже: дважды выполняя программу, в которой происходит выход за пределы допустимого диапазона, с немного разными входными данными, мы можем прийти к совершенно разным результатам. Ошибки такого рода (неустойчивые ошибки) выявить труднее всего.

Мы должны гарантировать, что выхода за пределы допустимого диапазона не будет. Одна из причин, по которым мы используем класс
vector
, а не выделяем память непосредственно с помощью оператора
new
, заключается в том, что класс
vector
знает свой размер и поэтому выход за пределы допустимого диапазона можно предотвратить.

Предотвратить выход за пределы допустимого диапазона сложно по многим причинам. Одна из них заключается в том, что мы можем присваивать один указатель

double*
другому указателю
double*
независимо от количества элементов, на которые они ссылаются. Указатель действительно не знает, на сколько элементов он ссылается. Рассмотрим пример.

double* p = new double; // разместить переменную типа double

double* q = new double[1000]; // разместить тысячи переменных double

q[700] = 7.7; // отлично

q = p; // пусть указатель q ссылается на то же, что и p

double d = q[700]; // выход за пределы допустимого диапазона!

Здесь всего три строки кода, в которых выражение

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

Теперь мы надеемся, что вы спросите: “А почему указатель не может помнить размер памяти?” Очевидно, что можно было бы разработать указатель, который помнил бы, на какое количество элементов он ссылается, — в классе

vector
это сделано почти так. А если вы прочитаете книги, посвященные языку С++, и просмотрите его библиотеки, то обнаружите множество “интеллектуальных указателей”, компенсирующих этот недостаток встроенных низкоуровневых указателей. Однако в некоторых ситуациях нам нужен низкоуровневый доступ и понимание механизма адресации объектов, а машина не знает, что она адресует. Кроме того, знание механизма работы указателей важно для понимания огромного количества уже написанных программ.

17.4.4. Инициализация

Как всегда, мы хотели бы, чтобы объект уже имел какое-то значение, прежде чем мы приступим к его использованию; иначе говоря, мы хотели бы, чтобы указатели и объекты, на которые они ссылаются, были инициализированы. Рассмотрим пример.

double* p0; // объявление без инициализации:

// возможны проблемы

double* p1 = new double; // выделение памяти для переменной

  • Читать дальше
  • 1
  • ...
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • ...

Ебукер (ebooker) – онлайн-библиотека на русском языке. Книги доступны онлайн, без утомительной регистрации. Огромный выбор и удобный дизайн, позволяющий читать без проблем. Добавляйте сайт в закладки! Все произведения загружаются пользователями: если считаете, что ваши авторские права нарушены – используйте форму обратной связи.

Полезные ссылки

  • Моя полка

Контакты

  • chitat.ebooker@gmail.com

Подпишитесь на рассылку: