Урок по треку C++ 5 мин

Указатели и ссылки без висячих концов

Адреса, арифметика указателей, const в разных позициях, висячие ссылки, nullptr, массивы и их распад.

Указатель хранит адрес объекта, ссылка является другим именем уже существующего объекта. Вопросы о них звучат на собесе почти всегда, причём часто в виде «что выведет» или «где здесь 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.
  • Что такое висячая ссылка и когда временный объект продлевается.
  • Распад массива и как передать массив с размером.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.