действия, как правило, остаются незамеченными достаточно долго, поэтому их особенно трудно выявить. Что еще хуже: дважды выполняя программу, в которой происходит выход за пределы допустимого диапазона, с немного разными входными данными, мы можем прийти к совершенно разным результатам. Ошибки такого рода (неустойчивые ошибки) выявить труднее всего.
Мы должны гарантировать, что выхода за пределы допустимого диапазона не будет. Одна из причин, по которым мы используем класс 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; // выделение памяти для переменной
// типа double
// без инициализации
double* p2 = new double(5.5); // инициализируем переменную типа double
// числом 5.5
double* p3 = new double[5]; // выделение памяти для массива
// из пяти чисел
// типа double без инициализации
Очевидно, что объявление указателя p0 без инициализации может вызвать проблемы. Рассмотрим пример.
*p0 = 7.0;
Эта инструкция записывает число 7.0 в некую ячейку памяти. Мы не знаем, в какой части памяти расположена эта ячейка. Это может быть безопасно, но рассчитывать на это нельзя. Рано или поздно мы получим тот же результат, что и при выходе за пределы допустимого диапазона: программа завершит работу аварийно или выдаст неправильные результаты. Огромное количество серьезных проблем в программах, написанных в старом стиле языка С, вызвано использованием неинициализированных указателей и выходом за пределы допустимого диапазона. Мы должны делать все, чтобы избежать таких проблем, частично потому, что наша цель — профессионализм, а частично потому, что мы не хотим терять время в поисках ошибок такого рода.
Выявление и устранение таких ошибок — ужасно нудное и неприятное дело. Намного приятнее и продуктивнее предотвратить такие ошибки, чем вылавливать их.
Память, выделенная оператором new встроенных типов, не инициализируется. Если хотите инициализировать указатель, задайте конкретное значение, как это было сделано при объявлении указателя p2: *p2 равно 5.5. Обратите внимание на круглые скобки, (), используемые при инициализации. Не перепутайте их с квадратными скобками, [], которые используются для индикации массивов.
В языке С++ нет средства для инициализации массивов объектов встроенных типов, память для которых выделена оператором new. Для массивов работу придется проделать самостоятельно. Рассмотрим пример.
double* p4 = new double[5];
for (int i = 0; i<5; ++i) p4[i] = i;
Теперь указатель p4 ссылается на объекты типа double, содержащие числа 0.0, 1.0, 2.0, 3.0 и 4.0.
Как обычно, мы должны избегать неинициализированных объектов и следить за тем, чтобы они получили значения до того, как будут использованы. Компиляторы часто имеют режим отладки, в котором они по умолчанию инициализируют все переменные предсказуемой величиной (обычно нулем). Это значит, что, когда вы отключаете режим отладки, чтобы отправить программу заказчику, запускаете оптимизатор или просто компилируете программу на другой машине, программа, содержащая неинициализированные переменные, может внезапно перестать работать правильно. Не используйте неинициализированные переменные. Если класс X имеет конструктор по умолчанию, то получим следующее:
X* px1 = new X; // один объект класса Х, инициализированный
// по умолчанию
X* px2 = new X[17]; // 17 объектов класса Х, инициализированных
// по умолчанию
Если класс Y имеет конструктор, но не конструктор по умолчанию, мы должны выполнить явную инициализацию
Y* py1 = new Y; // ошибка: нет конструктора по умолчанию
Y* py2 = new Y[17]; // ошибка: нет конструктора по умолчанию
Y* py3 = new Y(13); // OK: инициализирован адресом объекта Y(13)
17.4.5. Нулевой указатель
Если в вашем распоряжении нет другого указателя, которым можно было бы инициализировать ваш указатель, используйте 0 (нуль).
double* p0 = 0; // нулевой указатель
Указатель, которому присвоен нуль, называют нулевым (null pointer). Корректность указателя (т.е. ссылается ли он на что-либо) часто проверяется с помощью сравнения его с нулем. Рассмотрим пример.
if (p0 != 0) // проверка корректности указателя p0
Этот тест неидеален, поскольку указатель p0 может содержать случайное ненулевое значение или адрес объекта, который был удален с помощью оператора