Указатели против ссылок
Второй по частоте вопрос после стека и кучи: «В чём разница между указателем и ссылкой, если оба как будто дают доступ к чужой переменной?»
Ссылка — постоянный псевдоним уже существующей переменной, привязывается один раз и навсегда. Указатель — отдельная переменная, которая хранит адрес в памяти и может быть переназначена или обнулена.
Что выведет этот код?
#include <iostream>
int main() {
int a = 10;
int b = 20;
int& ref = a; // ссылка на a
ref = b; // это НЕ переназначение ссылки!
std::cout << "a = " << a << std::endl;
std::cout << "ref = " << ref << std::endl;
return 0;
}Вывод:
a = 20
ref = 20Вот она, главная ловушка вопроса. Строка ref = b; выглядит так, будто теперь ref ссылается на переменную b. На самом деле нет: ссылку нельзя «перепривязать» после создания. Строка ref = b; означает «скопировать значение b в ту переменную, на которую указывает ref» — то есть фактически это a = b;. Именно поэтому a тоже стало равно 20: ссылка ref — это просто другое имя для a, и присвоение через это имя меняет саму a.
Тот же трюк с указателем — другое поведение
#include <iostream>
int main() {
int a = 10;
int b = 20;
int* p = &a; // указатель на a
p = &b; // а вот это — настоящее переназначение
std::cout << "a = " << a << std::endl;
std::cout << "*p = " << *p << std::endl;
return 0;
}Вывод:
a = 10
*p = 20Здесь картина другая: p = &b; — это именно переназначение указателя, теперь он хранит адрес b, а не a. Переменная a при этом никак не пострадала и осталась равна 10. Указатель — самостоятельная переменная (он хранит число — адрес), поэтому его можно менять когда угодно, в отличие от ссылки, которая жёстко привязывается один раз при объявлении.
Как это работает под капотом
Указатель int* p в памяти занимает реальное место (обычно 8 байт на 64-битной системе) и хранит там число — адрес другой ячейки памяти. Чтобы получить значение, на которое он указывает, нужно явно «разыменовать» его звёздочкой: *p. Указатель можно не инициализировать сразу, можно обнулить через nullptr, можно переприсвоить сколько угодно раз, можно даже сделать арифметику вроде p + 1 (сдвинуть на следующий элемент массива).
Ссылка же на уровне компилятора почти всегда реализуется как тот же самый указатель «под капотом», но с ограничениями, которые компилятор соблюдает уже на этапе компиляции: она обязана быть инициализирована сразу при объявлении, не может быть nullptr и не может быть переприсвоена на другой объект. Из-за этих гарантий работать со ссылкой в коде проще и безопаснее — не нужно писать *, не нужно бояться нулевого указателя.
Таблица-памятка для собеседования
| Признак | Указатель | Ссылка |
| Может быть null | да (nullptr) | нет, обязана ссылаться на объект |
| Можно переназначить | да, в любой момент | нет, привязывается один раз |
| Нужна инициализация сразу | нет | да, обязательно |
| Синтаксис доступа | через *p | как у обычной переменной |
| Поддерживает арифметику | да (p + 1) | нет |
Частые ошибки на собеседовании
- Говорят, что ссылку «можно переназначить, как указатель» — нет, присвоение ссылке всегда меняет объект, на который она ссылается, а не саму привязку.
- Путают
nullptrс пустой ссылкой — ссылок без объекта в корректном C++ не существует в принципе. - Не могут объяснить, зачем вообще нужны указатели, если есть более безопасные ссылки — ответ: указатели нужны там, где требуется «может отсутствовать» (null), «можно переприсвоить» или арифметика адресов, например при обходе массива или в реализации собственных структур данных.
Итог
- Ссылка — второе имя для уже существующей переменной, привязка происходит один раз и навсегда.
- Указатель — самостоятельная переменная-адрес, которую можно переназначать, обнулять и использовать в арифметике.
- Присвоение ссылке (
ref = b) меняет значение исходного объекта, а не «перепривязывает» ссылку.