02 мая 2012

#2 Константные указатели и указатели на константу

... или «Обрезаем права на работу с указателями по самые... указатели»

Не будем делать долгих и лирических вступлений. Мы же все тут профессионалы. Поговорим о спецификаторе const при объявлении указателей:
1: int* pi               = new int(5);
2: const int* pci        = new int(5);
3: int const* pci2       = new int(5);
4: int* const cpi        = new int(5);
5: const intconst cpci = new int(5);
6: int constconst cpci = new int(5);
...


 В первой строчке описан указатель pi, который ссылается на область памяти, в которой находится объект типа int. Тут всё понятно.
 Во второй строчке объявляется указатель на константу типа int и инициализируется адресом памяти, который вернул оператор new. Это значит, что компилятор не даст модифицировать значение переменной по этому адресу, используя этот указатель. Но обратите внимание, что сам временный объект не константный. И считать значение переменной, естественно, тоже получится.
 В третьей строчке делается всё абсолютно то же самое, что и во второй. Синтаксис языка позволяет поместить const после типа данных. Главное, чтобы он стоял до «звёздочки».
 В четвертой строчке объявляется константный указатель на тип int и инициализируется значением. Это делает указатель псевдонимом объекта в динамической памяти (очень напоминает ссылки). Переназначить указатель на другую область памяти не получится. Зато можно легко изменить значение переменной, разыменовав указатель.
 В пятой строчке мы имеем константный указатель на константу. Тут, как вы, наверное, догадались, не переназначить ни адрес указателя, ни значение переменной, на которую он ссылается.
— Шестая строчка ещё раз демонстрирует, что можно по-другому объявить константным значение, а не указатель. Смысл её объявления абсолютно такой же, как и у строчки №5.

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

Как обычно, ссылки более просты в обращении. Вместо указателя
intconst cpi = &i;
можно объявить ссылку
int& ri = i;

А вместо громоздкой структуры
int constconst cpci = &i;
написать
const int& ri = i;

Далее, следует вспомнить, что можно преобразовать указатель на неконстанту в указатель на константу. Таким образом, мы просто урезаем права для нового указателя в рамках его области видимости (часто функции объявляют свои параметры как константные указатели, показывая при этом, что они не будут модифицировать объект, а не требуют при этом, чтобы он был обязательно константным).

Обратное же преобразование (из константы в неконстанту) невозможно — мы не можем присвоить значение константного указателя обычному указателю, способному вносить изменения в объект. Компилятор просто не позволит сделать это напрямую.

Можно применить оператор приведения типов const_cast<>. И он сработает, однако последствия в 50% случаев будут фатальными.

1 случай: фактически, указатель на константу может ссылаться не на константный объект (строчка 2 из примера объявления указателей). Тогда const_cast сработает нормально и инициализированный указатель на неконстанту не будет опасным:

const int* pci = new int(5);int* pi = const_cast<int*>(pci);*pi = 6;

2 случай: указатель на константу и вправду ссылается на константный объект. В этом случае опять же const_cast сработает, но модификация объекта через неконстантный указатель вызовет #неопределённое поведение.
const int ci;const int* pci = &ci;int* pi = const_cast<int*>(pci);*pi = 6; // Процитирую Джесса Либерти:         // «Мина замедленного действия         // уже запустилась и ждёт
         // своего часа

  #неопределённое поведение  


Об этом говорят специалисты: