Указатели
Указатели
Указатель в С++ — это переменная, содержащая не сам объект, а адрес памяти, где располагается объект. Java и C# имеют аналогичную концепцию «ссылки» при другом синтаксисе. Мы начнем с рассмотрения придуманного нами примера, иллюстрирующего применение указателей:
01 #include "point2d.h"
02 int main()
03 {
04 Point2D alpha;
05 Point2D beta;
06 Point2D *ptr;
07 ptr = α
08 ptr->setX(1.0);
09 ptr->setY(2.5);
10 ptr = β
11 ptr->setX(4.0);
12 ptr->setY(4.5);
13 ptr = 0;
14 return 0;
15 }
В этом примере используется класс Point2D из предыдущего подраздела. В строках 4 и 5 определяется два объекта типа Point2D. Эти объекты инициализируются в значение (0, 0) стандартным конструктором Point2D.
В строке 6 определяется указатель на объект Point2D. Для обозначения указателя здесь используется звездочка перед именем переменной. Поскольку мы не инициализируем указатель, он будет содержать произвольный адрес памяти. Эта ситуация изменяется в строке 7, в которой адрес alpha присваивается этому указателю. Унарный оператор & возвращает адрес памяти, где располагается объект. Адрес обычно представляет собой 32-битовое или 64-битовое целое число, задающее смещение объекта в памяти.
В строках 8 и 9 мы обращаемся к объекту alpha с помощью указателя ptr. Поскольку ptr является указателем, а не объектом, необходимо использовать оператор -> (стрелка) вместо оператора . (точка).
В строке 10 указателю присваивается адрес beta. С этого момента любая выполняемая нами операция с этим указателем будет воздействовать на объект beta.
В строке 13 указатель устанавливается в нулевое значение. С++ не имеет ключевого слова для представления указателя, который не ссылается ни на один объект; вместо этого мы используем значение 0 (или символическую константу NULL, которая разворачивается в 0). Попытка применения нулевого указателя приведет к краху приложения с выводом такого сообщения об ошибке, как «Segmentation fault» (ошибка сегментации), «General protection fault» (общая ошибка защиты) или «Bus error» (ошибка шины). Применяя отладчик, можно найти строку программного кода, которая приводит к краху.
В конце функции объект alpha содержит пару координат (1.0, 2.5), а объект beta — (4.0,4.5).
Указатели часто используются для хранения объектов, память для которых выделяется динамически с помощью оператора new. Используя жаргон С++ можно сказать, что эти объекты распределяются в «куче», в то время как локальные переменные (т.е. переменные, определенные внутри функции) хранятся в «стеке».
Ниже приводится фрагмент программного кода, иллюстрирующий динамическое распределение памяти при помощи оператора new:
01 #include "point2d.h"
02 int main()
03 {
04 Point2D *point = new Point2D;
05 point->setX(1.0);
06 point->setY(2.5);
07 delete point;
08 return 0;
09 }
Оператор new возвращает адрес памяти для нового распределенного объекта. Мы сохраняем адрес в переменной указателя и обращаемся к объекту через этот указатель. Поработав с объектом, мы возвращаем занимаемую им память, используя оператор delete. В отличие от Java и C#, сборщик мусора отсутствует в С++; динамически распределяемые объекты должны явно освобождать занимаемую ими память при помощи оператора delete, когда они становятся больше ненужными. В главе 2 описывается механизм родственных связей Qt, который значительно упрощает управление памятью в программах, написанных на С++.
Если не вызвать оператор delete, память остается занятой до тех пор, пока не завершится программа. Это не создаст никаких проблем в приведенном выше примере, потому что память выделяется только для одного объекта, однако в программе, в которой постоянно создаются новые объекты, это может привести к нехватке машинной памяти. После удаления объекта переменная указателя по-прежнему будет хранить адрес объекта. Такой указатель является «повисшим указателем» и не должен использоваться для обращения к объекту. Qt предоставляет «умный» указатель QPointer<T>, который автоматически устанавливает себя в 0, если удаляется объект QObject, на который он ссылается.
В приведенном выше примере мы вызывали стандартный конструктор и функции setX() и setY() для инициализации объекта. Вместо этого можно было использовать конструктор с двумя параметрами:
Point2D *point = new Point2D(1.0, 2.5);
Кроме того, мы могли бы распределить объект в стеке следующим образом:
Point2D point;
point.setX(1.0);
point.setY(2.5);
Распределенные таким образом объекты автоматически освобождаются в конце блока, в котором они появляются.
Если мы не собираемся модифицировать объект при помощи указателя, можно объявить указатель как константный. Например:
const Point2D *ptr = new Point2D(1.0, 2.5);
double x = ptr->x();
double у = ptr->y();
// НЕ БУДЕТ КОМПИЛИРОВАТЬСЯ
ptr->setX(4.0);
*ptr = Point2D(4.0, 4.5);
Константный указатель ptr можно использовать лишь для вызова константных функций-членов, например x() и y(). Признаком хорошего стиля является объявление указателей константными, когда нет намерения модификации объекта с их помощью. Более того, если сам объект является константным, ничего не остается, кроме использования константного указателя для хранения его адреса. Применение ключевого слова const предоставляет компилятору информацию, позволяющую обнаруживать ошибки на ранних этапах и повысить производительность. C# имеет ключевое слово const с очень похожими свойствами. Ближайшим эквивалентом в Java является ключевое слово final, однако оно лишь защищает переменные от операций присваивания, но не от вызова «неконстантных» функций—членов объекта.
Указатели могут использоваться со встроенными типами так же, как с классами. Используемый в выражении унарный оператор * возвращает значение объекта, на который ссылается указатель. Например:
int i = 10;
int j = 20;
int *p = &i;
int *q = &j;
cout << *p << " equals 10" << endl;
cout << *q << " equals 20" << endl;
*p = 40;
cout << i << " equals 40" << endl;
p = q;
*p = 100;
cout << i << " equals 40" << endl;
cout << j << " equals 100" << endl;
Оператор ->, который можно использовать для обращения к членам объекта через указатель, является чисто синтаксическим приемом. Вместо ptr->member можно также написать (*ptr).member. Скобки обязательны, потому что оператор . (точка) имеет более высокий приоритет, чем унарный оператор *.
Указатели имели плохую репутацию в С и С++, причем доходило до того, что рекламировалось отсутствие указателей в языке Java. На самом деле указатели С++ концептуально аналогичны ссылкам в Java и C#, за исключением того, что указатели можно использовать для прохода по памяти, как мы это увидим позже в данном разделе. Более того, включение в Qt классов—контейнеров, использующих метод «копирования при записи» вместе со способностью С++ инстанцировать любой класс в стеке, означает возможность во многих случаях обойтись без указателей.