Указатель хранит адрес объекта, ссылка является другим именем уже существующего объекта. Вопросы о них звучат на собесе почти всегда, причём часто в виде «что выведет» или «где здесь UB».
Указатель
int x = 5;
int* p = &x; // адрес x
*p = 7; // меняем x через указатель
p = nullptr; // указатель можно перенаправить или обнулить
Указатель может быть нулевым, может меняться, у него есть своя память (обычно 8 байт на 64-битной платформе). nullptr (тип std::nullptr_t) заменил NULL и 0: он неявно приводится к любому указателю, но не к int, поэтому перегрузки f(int) и f(char*) выбираются однозначно.
Ссылка
int x = 5;
int& r = x; // r и x один объект
r = 7; // x == 7
Ссылка обязана быть инициализирована, не бывает «нулевой» по правилам языка и не может быть перенаправлена: r = y присвоит значение y в x, а не перепривяжет ссылку. Отдельной памяти у ссылки может не быть вовсе.
Когда что: ссылка для обязательного параметра (const T& для чтения больших объектов, T& для выходного параметра), указатель, когда значения может не быть или его нужно перенаправлять. Для «может не быть» в современном коде есть ещё std::optional.
const в объявлениях
Читайте справа налево:
const int* p(илиint const* p): указатель на константныйint, менять*pнельзя,pможно;int* const p: константный указатель,*pменять можно,pнельзя;const int* const p: нельзя ни то, ни другое.
Арифметика указателей
Внутри массива указатель можно сдвигать: p + i указывает на i-й элемент дальше, разность q - p даёт число элементов между ними (тип std::ptrdiff_t). Разрешено получить адрес на один за последним элементом (a + n), но разыменовывать его нельзя. Вычисление адреса дальше a + n или раньше a уже UB, даже без разыменования. a[i] это по определению *(a + i), поэтому i[a] тоже компилируется.
Распад массива
Массив при передаче в функцию превращается в указатель на первый элемент:
void f(int arr[10]) { sizeof(arr); } // == sizeof(int*), размер потерян
Чтобы передать размер, используйте std::span<int> (C++20), std::array или ссылку на массив int (&arr)[10].
Висячие указатели и ссылки
Самая частая ошибка с памятью: объект умер, а адрес остался.
const std::string& name() { std::string s = "x"; return s; } // ссылка на умерший локальный объект
std::vector<int> v = {1, 2, 3};
int& first = v[0];
v.push_back(4); // возможна реаллокация: first висит
Исключение, которое любят спрашивать: временный объект, привязанный к const T& (или T&&) напрямую, живёт столько же, сколько ссылка:
const std::string& s = std::string("a") + "b"; // ок, временный объект продлён
Продление не работает через функцию: const auto& r = std::max(std::string("a"), std::string("b")); висит, потому что max возвращает ссылку на свой аргумент-временный объект.
Частые ошибки
- Путать
const int*иint* const. - Возвращать ссылку или указатель на локальную переменную.
- Хранить указатель или ссылку на элемент вектора и потом добавлять элементы.
- Ждать, что
sizeofмассива внутри функции вернёт размер массива. deleteдля памяти изnew[](нуженdelete[]), а лучше вообще контейнеры и умные указатели.
Что спросят на собесе
- Отличия указателя от ссылки и когда что выбирать.
- Где
constвconst int* const pи что можно менять. - Почему
nullptrлучшеNULL. - Что такое висячая ссылка и когда временный объект продлевается.
- Распад массива и как передать массив с размером.