Указатель (pointer) — тип данных, хранящий адрес другого объекта в памяти. Если T — некоторый тип, то T* — указатель на T. Поэтому указатели — не один тип, а целое семейство: int*, float*, const char**, double***.

Зачем нужно

Память в C++ — нумерованная последовательность байт, номер байта — его адрес. Указатель позволяет обратиться к объекту не по имени, а по адресу: передать объект куда-то, не копируя его, дотянуться до объекта, у которого имени нет вовсе, — например, созданного в динамической памяти. С указателями работают и массивы, см. Арифметика указателей.

Как работает

Взятие адреса

Адрес объекта даёт унарная операция &. Её результат — указатель на тип объекта:

int x;
const float y = 0;
 
std::cout << &x << '\n';  // адрес в 16-ричном виде
&x;                       // int*
&y;                       // const float*

Брать адрес можно только у lvalue — возможность взять адрес и служит критерием lvalue, см. Категории значения:

&x, &(x = 8), &(++x);   // можно
&5, &(x + 8), &(x++);   // нельзя

Разыменование

Унарная * применяется к указателю и возвращает lvalue того объекта, на который он указывает. Через него объект читают и меняют:

int x = 1;
float y = 2.5;
 
int* px = &x;
float* py = &y;
 
*px = 11;
*py = 3.5;
 
std::cout << x << ' ' << y;  // 11 3.5

Тип указателя должен соответствовать типу объекта: int* py = &y при float y не компилируется.

Звёздочка в объявлении (int* px) — часть типа, звёздочка в выражении (*px) — операция. Это разные вещи с одинаковым знаком.

Указатели и const

Константным бывает объект, на который указывают, и сам указатель:

int x = 0;
int y = 0;
 
const int* pc = &x;     // указатель на константу: *pc менять нельзя
int* const cp = &x;     // константный указатель: cp менять нельзя
const int* const cc = &x;  // и то и другое
 
std::cin >> *pc;  // CE
pc = &y;          // OK
*cp = 5;          // OK
cp = &y;          // CE
++cp;             // CE

const int* и int const* — одно и то же. Читайте тип справа налево: int const* const — константный указатель на константу int.

Указатель на константу можно направить и на неконстантный объект — тогда объект просто нельзя изменить через этот указатель. Обратное запрещено: float* py = &y при const float y — ошибка, иначе через py можно было бы изменить константу. См. const и constexpr.

Указатель на указатель

Указатель — тоже тип, на него можно сделать указатель:

float x;
float* px = &x;
float** ppx = &px;
float*** pppx = &ppx;
 
const float* const** const p = ...;
// константный указатель на указатель на константный указатель на константу float

Нулевой указатель

Неинициализированный указатель читать нельзя — это Неопределённое поведение, как и для любой переменной. Присвоить указателю произвольное число тоже нельзя: int* r = 10; — ошибка. Нужен способ сказать «указатель никуда не указывает».

Исторически для этого годился литерал 0: по стандарту по нулевому адресу ничего не лежит. Современный способ — литерал nullptr типа std::nullptr_t, неявно приводимый к нулевому указателю любого типа:

int* p = nullptr;
const float* pp = nullptr;

Правило: нулевые указатели обозначаются только nullptr. Разыменовать нулевой указатель — UB; удалить через delete — можно, это ничего не делает.

Указатель на void

void* хранит адрес без информации о типе. Напечатать такой адрес можно, разыменовать — нельзя: неизвестно, что там лежит и сколько байт читать.

int x;
void* p = &x;
 
std::cout << p;   // Ok
std::cout << *p;  // CE

Вернуть тип можно через static_cast: static_cast<int*>(p). Так работают с результатом malloc, см. Динамическая память.

Тонкости

  • Указатели одного типа можно вычитать и сдвигать на целое число — см. Арифметика указателей. Остальные арифметические операции к ним неприменимы.
  • Указатель может пережить объект, на который указывает, — тогда он становится висячим.
  • Исключение «0 можно присвоить указателю» касается только литерала 0. int z = 0; int* p = z; не компилируется.

Не из лекции

Последний пункт проверен на GCC 16.2 (-std=c++20): invalid conversion from 'int' to 'int*'.

Частые ошибки

  • Объявить указатель без инициализации и прочитать его.
  • Перепутать const int* (нельзя менять объект) и int* const (нельзя менять указатель).
  • Писать 0 вместо nullptr.
  • Сохранить адрес локальной переменной и пользоваться им после выхода из блока.