Menu

Указатели в C++: оператор адреса, разыменование и nullptr

Указатели в C++ с нуля: как объявить указатель, операторы & (взятие адреса) и * (разыменование), nullptr, указатели на массивы и ловушки висячих и неинициализированных указателей, приводящие к падениям.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Переменная, хранящая адрес

Каждая переменная находится где-то в памяти, в пронумерованном месте, которое называют её адресом. Чаще всего вам всё равно где — вы просто используете имя переменной. Указатель переворачивает это: это переменная, значением которой является адрес. Вместо 42 он хранит «место, где хранится 42».

Именно эта косвенность делает указатели мощными. Функции могут через них менять переменную вызывающей стороны, структуры данных вроде связных списков сцепляют ими свои узлы, а (как вы увидите в разделе динамическая память) именно так вы добираетесь до памяти, выделенной во время выполнения.

& в &score — это оператор взятия адреса: он даёт местоположение score. * в *p — это оператор разыменования: он идёт по адресу к значению, которое там живёт.

Два оператора: & и *

Самое запутанное для новичков — то, что * означает две разные вещи в зависимости от того, где он стоит. Держите это в голове чётко:

int* p;     // ОБЪЯВЛЕНИЕ: "p — это указатель на int"
p = &x;     // & = взятие адреса: сохранить адрес x в p
int y = *p; // * = разыменование: прочитать значение, на которое указывает p
*p = 99;    // разыменование слева: запись через указатель

В объявлении * — часть типа. В выражении * выполняет работу. Как только указатель настроен, его разыменование даёт вам полный доступ на чтение и запись к исходной переменной:

Обратите внимание: после первой строки вы ни разу не обращались к health по имени, а его значение всё равно менялось. В этом весь смысл: hp — это псевдоним того же хранилища. Расстановка пробелов (int* p, int *p, int*p) — лишь косметика и для компилятора одинакова; в этом руководстве используется int* p.

nullptr: указывать в никуда

Указатель, который никуда не указывает, следует устанавливать в nullptr (C++11). Это понятный и типобезопасный способ сказать «цели пока нет», и он даёт вам то, что можно проверить перед разыменованием.

Предпочитайте nullptr устаревшему макросу NULL или голому 0. Поскольку nullptr имеет настоящий тип указателя, при разрешении перегрузки его никогда не примут за целое 0 — тонкая ошибка, которую мог вызывать старый стиль.

Ловушка — разыменование null. Чтение или запись через нулевой (или неинициализированный) указатель — это неопределённое поведение, обычно мгновенное падение:

int* p = nullptr;
cout << *p;   // ПАДЕНИЕ - разыменование null — это неопределённое поведение

Всегда защищайтесь проверкой if (p) (или if (p != nullptr)) перед разыменованием всего, что может быть null.

Указатели и массивы

Имя массива распадается (decay) в указатель на его первый элемент, поэтому указатели и массивы тесно переплетены. Прибавление 1 к указателю не добавляет один байт — оно сдвигает на один элемент, и именно это заставляет работать арифметику указателей:

p[i] и *(p + i) — буквально одно и то же выражение; именно эта эквивалентность объясняет, почему массивы индексируются с нуля. Классическая ошибка здесь — выйти за конец: nums + 4 — допустимый маркер «на один за концом» для сравнения, но разыменование *(nums + 4) читает за границами. Ошибки на единицу (off-by-one) с указателями — одна из главных причин падений и тихой порчи данных, поэтому будьте внимательны к условию остановки.

const и указатели

const может относиться к тому, на что указывает указатель, к самому указателю или к обоим. Чтобы расшифровать объявление, читайте его справа налево:

const int* p;        // указатель на const int  - нельзя менять *p, можно перенаправлять p
int* const p = &x;   // const-указатель на int  - можно менять *p, нельзя перенаправлять p
const int* const p = &x; // заблокировано и то, и другое

В реальном коде это важно постоянно. Функция, которая обещает не изменять ваши данные, принимает указатель на const:

Пометка указуемого как const документирует намерение и позволяет компилятору пресекать случайные записи — бесплатная безопасность без затрат во время выполнения.

Главная ловушка: висячие указатели

Висячий указатель указывает на память, которая больше не содержит ожидаемого значения — переменная вышла из области видимости или память была освобождена. Его разыменование — это неопределённое поведение, и самое неприятное в том, что часто всё как будто работает, пока не перестанет.

int* makeBad() {
    int local = 5;
    return &local;   // ОШИБКА: local умирает, когда функция возвращается
}                    // возвращённый указатель теперь висячий

Адрес по-прежнему остаётся допустимым числом, но он указывает на ячейку стека, которая была переиспользована — её чтение даёт мусор или падение. То же самое происходит, если вы держите указатель на delete-нутый объект в куче или на элемент vector, который позже перевыделяет память.

Три правила уберегут вас:

  • Никогда не возвращайте адрес локальной переменной. Возвращайте по значению или пусть вызывающая сторона владеет хранилищем.
  • Устанавливайте указатель в nullptr после того, как то, на что он указывал, исчезло, и проверяйте перед использованием.
  • Для владения и времён жизни обращайтесь к умным указателям вместо сырых new/delete — они освобождают память автоматически и сводят на нет весь этот класс ошибок.

Далее: ссылки против указателей

Указатели — не единственный способ косвенно сослаться на другую переменную. В C++ есть ещё и ссылки, которые ощущаются похоже, но не могут быть null, не могут быть переназначены и используют более чистый синтаксис. Дальше мы поставим их рядом в разделе ссылки против указателей, чтобы вы точно знали, какой инструмент выбрать — и почему большая часть современного C++ предпочитает ссылки, когда их можно использовать.

Часто задаваемые вопросы

Что такое указатель в C++?

Указатель — это переменная, которая хранит адрес в памяти другого значения, а не само значение. Его объявляют с * (например, int* p), берут адрес оператором & (p = &x), а читают или записывают значение, на которое он указывает, разыменовывая его через *p.

В чём разница между & и * в указателях C++?

В контексте указателей & — это оператор взятия адреса: &x даёт адрес x. * выполняет две роли: в объявлении (int* p) он помечает переменную как указатель, а в выражении (*p) разыменовывает указатель, чтобы добраться до значения, хранящегося по этому адресу.

Что такое nullptr в C++ и почему его использовать вместо NULL?

nullptr — это типобезопасный литерал нулевого указателя, добавленный в C++11. Он означает «не указывает ни на что». Предпочитайте его старому NULL или голому 0, потому что nullptr имеет настоящий тип указателя, поэтому при разрешении перегрузки его никогда не примут за целое число. Всегда проверяйте if (p) перед разыменованием — разыменование нулевого указателя — это неопределённое поведение.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ