Привет, Хабр!
Сегодня разбираем &&* неувядающую классику C++ — ссылки & и указатели *. Казалось бы, два оператора, делов-то, но стгоит нырнуть под крышку — и выясняется: тут и разное время жизни, и несменяемость адреса, и прочие вещички. Разберемся в статье подробнее.
Как устроена ссылка
В C++ ссылка reference
— это псевдоним для существующего объекта. Синтаксически она похожа на указатель, но ведёт себя по-другому: у ссылки всегда есть цель, и её нельзя переназначить после инициализации. Это своего рода другой способ обратиться к тому же самому объекту, без лишней обвязки.
Объявляется ссылка просто:
int a = 42;
int& ref = a; // ref — это "второе имя" для a
Теперь ref
и a
— это одно и то же. Измените ref
— изменится a
. Копии не происходит, память не дублируется, всё работает напрямую. По сути, это способ писать код чуть проще, но с теми же самыми адресами под капотом.
Ссылки чаще всего используют:
когда вы хотите передать объект в функцию без копии, но не хотите заниматься проверкой
nullptr
;когда хотите показать, что параметр обязателен (в отличие от указателя);
и — чаще всего — когда пишете перегрузки, шаблоны и интерфейсы, где нужна категория значения.
Reference is just a const T* (но не всегда)
Математика уровня ABI
ABI / компилятор | Что лежит в памяти | Можно ли вычислить offset до объекта? |
|
---|---|---|---|
Itanium (GCC/Clang на *nix) | ровно | да, это прямой адрес |
|
MSVC (x86-64) | тот же | да |
|
ARM AAPCS |
| да |
|
Почему иногда пишут, что sizeof(int&) == sizeof(int)
?
На платформах ILP32 int
и void*
по 4 байта утверждение банально истинно. На LP64 будет 4 vs 8. Проверьте сами:
static_assert(sizeof(int) == 4);
static_assert(sizeof(int*) == 8);
static_assert(sizeof(int&) == 8); // LP64: совпало с указателем
Почему нельзя переназначить?
Компилятор генерирует скрытый T* const ref = &obj;
. Модифицировать ref
— значит сломать const
. Undef-Behaviour, до свидания переносимость.
int a=10, b=42;
int& ref=a; // mov edi, offset a
// ...
ref = b; // mov eax, DWORD PTR [b]
// mov DWORD PTR [a], eax
Никакого отдельного объекта для ref
в asm уже нет: оптимизатор подставил адрес напрямую.
Null-reference: формально UB, фактически можно, но не надо
Стандарт (C++26 [dcl.ref]/1) жёстко: «A reference shall be bound to an object» → никакого «ну пусть будет null».
Тем не менее, на машинном уровне это реально 0
в регистре — компилятор просто предполагает, что так не случится. Нарушаем — получаем:
int& bad = *static_cast<int*>(nullptr); // UB
std::cout << bad; // -fsanitize=null отловит
Почему потусторонний код иногда так делает?
Бриджи к старому C API: приходится передать отсутствие как ссылку, чтобы не менять сигнатуру. Правило команды: закрываем пробел фабрикой-обёрткой:
std::optional<std::reference_wrapper<T>> make_ref(T* p) {
if (p) return std::ref(*p);
return std::nullopt;
}
Rvalue- и forwarding-ссылки
Категории значений refresher
Категория | Пример | Что значит |
---|---|---|
lvalue |
| есть имя, есть адрес |
xvalue |
| ресурс можно украсть |
prvalue |
| безадресное временное |
Каллиграфия коллапса
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
Когда шаблонный параметр T = Widget&
, ваша универсальная T&&
раскладывается в Widget&
. Поэтому forwarding-ссылка = «T deduced + &&
».
template<class T>
void sink(T&& val) {
sink_impl(std::forward<T>(val)); // краеугольный камень perfect forwarding
}
Ограничения ABI
Выравнивание alias-объекта
Reference обязана уважать alignof(T). На 64-битахlong double&
может потребовать 16-байтовый aligned move, и тогда__ref
всё равно останется указателем, иначе архитектура ломается.Exceptions &
catch(T&)
Грубо говоря, в Itanium-ABI параметр-catch передаётся как ссылочный alias, MSVC — как копия. Поэтому одна и та же.dll
/.so
может ловить чужие C++-исключения по-разному. Поэтму не кидаем эксепшены через бинарный шов.Member pointer vs reference-to-member
int T::*
— это offset, аint& T::
недопустим вовсе. Почему? Потому что reference всегда привязан к объекту — без полного адреса она не имеет смысла.
Указатели
Если ссылка — это псевдоним объекта, то указатель — это переменная, которая содержит адрес объекта. Указатель — это буквально указка: он не владеет значением сам по себе, он лишь показывает, где это значение лежит в памяти. В отличие от ссылок, указатель можно не только переназначить, но и сделать пустым — т.е нулевым nullptr
.
Объявляется указатель просто:
int a = 5;
int* p = &a; // p указывает на a
Через *p
можно разыменовать указатель и прочитать значение, которое по этому адресу лежит.
Базовые аксиомы
int a = 5;
int* p = &a; // A. захват адреса
*p = 7; // B. разыменование (read-modify-write)
p = nullptr; // C. переназначение (nullable by design)
Вопрос | Краткий ответ | Последствия |
---|---|---|
Что лежит в | Машинный адрес + provenance-тег | АСan/TSan отслеживают к чьей зоне принадлежит |
Можно ли писать через | Нельзя, тк | Протягивайте |
Почему | В ABI pointer размер не зависит от | Можно |
Арифметика, массивы, SIMD и alias-клятва компилятору
Pointer + loop == самый дешёвый итератор
std::array<float, 1024> samples;
const float* in = samples.data();
float* out = scratchpad.data();
for (size_t i = 0; i < samples.size(); ++i, ++in, ++out)
*out = *in * window[i];
Каждый инкремент — это LEA rsi, [rsi+4]
на x86-64 (если float
). Создать такой же tight-loop на std::vector<float>::iterator
можно, но придётся довериться inlining; сырой pointer снимает вопрос.
restrict (или restrict/__restrict) — пароль от векторного ускорения
void saxpy(size_t n,
float __restrict__ * __restrict__ y,
const float __restrict__ * __restrict__ x,
float a)
{
for (size_t i = 0; i < n; ++i)
y[i] += a * x[i];
}
Даем майку-обещание «x
и y
не пересекаются». Оптимизатор перестаёт держать y[i]
в регистре на случай alias и сверяет память реже -> GCC/Clang свободно разворачивают петлю и склеивают в 256-/512-битный vfmadd231ps
.
Alignment: страшилка про 16-байтные границы
AVX-512 потребует, чтобы ptr % 64 == 0
для unaligned-free загрузки. Если не уверены — вызывайте:
float* ptr = std::bit_cast<float*>(std::aligned_alloc(64, N * sizeof(float)));
или используйте std::aligned_alloc
/std::pmr::new_delete_resource
.
Обертка std::span: zero-cost, но с контрактом
void convolve(std::span<const float> in,
std::span<float> out,
std::span<const float> kernel);
std::span
— это простая, но мощная обёртка над «указатель + размер». Внутри он реализован как struct { T* data; std::size_t size; }
, и на большинстве архитектур занимает всего 16 байт. Те можно передавать миллионы спанов между функциями без малейшего давления на кеши или стек — overhead практически нулевой.
Частая ошибка: дожить дольше, чем владелец
std::span<int> leak_span;
{
std::vector<int> v = {1,2,3};
leak_span = v; // span на вектор
} // v уничтожен — UB при доступе
span — не владелец. Жизнь span ≤ жизнь данных. Для статического анализа включайте -fanalyzer
и clang-tidy
check bugprone-dangling-handle
.
Вместо вывода
Ссылки и указатели — это не «выбрать один раз и забыть». Это инструментальный сет: как молоток и отвёртка. Нужно ли прибивать гвоздь болгаркой? Скорее нет. Нужна ли на стройке только отвёртка? Тоже нет. В 2025 г. мы живём в раю span
, expected
, unique_ptr
, но по-прежнему встречаем коды, где raw-pointer обязателен, а reference делает интерфейс выразительным.
Берём чек-лист выше, погружаем в clang-tidy, добавляем санитайзеры -fsanitize=address,undefined, и спим спокойнее.
Увидимся в комментариях, коллеги — с удовольствием обсудим любые edge-кейсы, которые остались за кулисами. Happy coding!
Когда ты уверен в разнице между &
и *
, кажется, всё под контролем. Но если баги упрямо прячутся до релиза, а любой рефакторинг превращается в минное поле — значит, пришло время не просто знать синтаксис, а понимать, что происходит под капотом. Эти два урока — про то, как писать код на C++, который не пугает ни других разработчиков, ни отладчик.
9 июня в 20:00
Отлаживаем C++: от printf до asan и зеленых тестов
Как находить ошибки до продакшена — с помощью address sanitizer, assert'ов, логирования и core dump. Всё на живом коде.19 июня в 20:00
Разделяй и абстрагируй: как создавать понятный C++ код
Практика рефакторинга: превращаем сложный код в чистую, тестируемую архитектуру без потери производительности.
А освоить базовые навыки IT, необходимые C++ разработчику для успешного старта, можно на курсе "C++ Developer. Basic" под руководством преподавателей-практиков.