int* pi = new int; // выделяем память для одной переменной int
int* qi = new int[4]; // выделяем память для четырех переменных int
// (массив)
double* pd = new double; // выделяем память для одной переменной
// double
double* qd = new double[n]; // выделяем память для n переменных
// double
Обратите внимание на то, что количество объектов может задаваться переменной. Это важно, поскольку позволяет нам выбирать, сколько массивов можно разместить в ходе выполнения программы. Если
n
равно
2
, то произойдет следующее.
Указатели на объекты разных типов имеют разные типы. Рассмотрим пример.
pi = pd; // ошибка: нельзя присвоить указатель double* указателю int*
pd = pi; // ошибка: нельзя присвоить указатель int* указателю double*
Почему нельзя? В конце концов, мы же можем присвоить переменную типа
int
переменной типа
double
, и наоборот. Причина заключается в операторе
[]
. Для того чтобы найти элемент, он использует информацию о размере его типа. Например, элемент
qi[2]
находится на расстоянии, равном двум размерам типа
int
от элемента
qi[0]
, а элемент
qd[2]
находится на расстоянии, равном двум размерам типа
double
от элемента
qd[0]
. Если размер типа
int
отличается от размера типа
double
, как во многих компьютерах, то, разрешив указателю
qi
ссылаться на память, выделенную для адресации указателем
qd
, можем получить довольно странные результаты.
Это объяснение с практической точки зрения. С теоретической точки зрения ответ таков: присваивание друг другу указателей на разные типы сделало бы возможными ошибки типа (type errors).
17.4.2. Доступ с помощью указателей
Кроме оператора разыменования
*
, к указателю можно применять оператор индексирования
[]
. Рассмотрим пример.
double* p = new double[4]; // выделяем память для четырех переменных
// типа double в свободной памяти
double x = *p; // читаем (первый) объект, на который
// ссылается p
double y = p[2]; // читаем третий объект, на который
// ссылается p
Так же как и в классе
vector
, оператор индексирования начинает отсчет от нуля. Это значит, что выражение
p[2]
ссылается на третий элемент;
p[0]
— это первый элемент, поэтому
p[0]
означает то же самое, что и
*p
. Операторы
[]
и
*
можно также использовать для записи.
*p = 7.7; // записываем число в (первый) объект, на который
// ссылается p
p[2] = 9.9; // записываем число в третий объект, на который
// ссылается p
Указатель ссылается на объект, расположенный в памяти. Оператор разыменования (“contents of” operator, or dereference operator) позволяет читать и записывать объект, на который ссылается указатель
p
.
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
.