Загружаем лабораторные…
// C++ · Лекция 08

Ссылки и сложные объявления

От чтения вложенных типов к главной идее ссылки: это другое имя того же объекта. Передача по ссылке, категории значений, время жизни и допустимые комбинации типов.

Ссылка — второе имя

Ссылка (reference) — другое имя уже существующего объекта или функции. Запись T& r = x; связывает r с x без создания нового объекта типа T.

Для обычного объекта int y = x; создаёт отдельный объект с копией значения, а int& y = x; делает второе имя того же объекта. Изменение через y тогда меняет именно x.

Идентичность объекта

Ссылка и исходное имя ведут к одному объекту, а не к двум объектам, которые «синхронизируются».

Это же работает со строкой, вектором, словарём: std::string s2 = s; создаёт отдельную строку, а std::string& s2 = s; — второе имя.

copy_or_alias.cpp
#include <iostream>
int main() {
    int x = 5;
    int copy = x;       // отдельная ячейка
    int& alias = x;     // то же самое x
    ++alias;
    copy = 100;
    std::cout << x << ' ' << copy; // 6 100
}
Изменение alias изменяет исходный объект x; copy независим.
Визуализация / создание копии и второго имени

Присваивание при создании копии и связывание ссылки выглядят похоже, но результат разный. Копия получает собственный объект со значением, а ссылка даёт ещё один способ обратиться к исходному объекту. В визуализации меняйте обе стороны и проверяйте, сколько независимых значений получилось.

x и y — два имени5

один объект int

Значения совпадают не из-за синхронизации: обе подписи относятся к одной ячейке.
Модель различает копирование и связывание ссылки, не моделируя машинные адреса.
Ссылка не переназначается

Если после int& y = x; написать y = z;, изменится значение x. Ссылка y не начнёт обозначать z.

Разобранный пример и самостоятельная проверка

Сравним две похожие записи. Первая, int copy = original, создаёт новый объект и переносит в него текущее значение. После этого объекты независимы. Вторая, int& alias = original, связывает новое имя с уже существующим объектом. Дополнительного int не появляется. Поэтому изменение через alias сразу является изменением original. В правильном рисунке это одна ячейка с двумя подписями, а не две ячейки, которые обмениваются обновлениями. Если позже написать alias = other, ссылка не станет обозначать other: в исходный объект будет записано значение other. Ссылка должна получить объект при инициализации. Она не управляет его временем жизни и не удерживает локальную переменную после выхода из функции. Поэтому ссылку на обычную локальную переменную нельзя возвращать для дальнейшего использования. Для первого применения достаточно параметра int&, который изменяет живой объект вызывающего кода. Возврат значений остаётся обычным безопасным выбором.

code-guided-alias.cpp
#include <iostream>

// int& bad() { int local = 7; return local; } // Не возвращать local.

void increment(int& value) {
    ++value;
}

int main() {
    int original = 5;
    int copy = original;
    int& alias = original;
    increment(alias);
    std::cout << original << ' ' << copy << '\n';
    std::cout << std::boolalpha << (&original == &alias) << '\n';
}
Один объект с двумя именами; copy остаётся независимым.
Результат6 5 true

Ссылка и указатель

Указатель int* p — самостоятельный объект, хранящий адрес. Ссылка int& r — имя уже существующего объекта. Указатель можно направить на другой объект и можно сделать nullptr. Ссылка должна быть связана с объектом при инициализации.

Разные роли символа &

&x — оператор взятия адреса, результат int*. int& r — объявление ссылочного типа. Это разные конструкции.

pointer_vs_reference.cpp
#include <iostream>
int main() {
    int x = 5, z = 9;
    int* p = &x;
    int& r = x;
    p = &z;            // указатель сменил адрес
    r = z;             // x = z, не смена ссылки
    std::cout << x << ' ' << *p; // 9 9
}
p перенаправляется; r остаётся вторым именем x.
Визуализация / два объекта, переназначаемый указатель и постоянная ссылка

Указателю можно присвоить другой адрес; уже связанную ссылку переназначить нельзя. Выражение r = z меняет значение объекта, который именуется r. Поэтому сначала проследите, какой объект обозначает каждое имя, и только потом выполняйте присваивание.

x (r)5
z9
p → x5
r всегда именует x; p сейчас хранит адрес x
r = z копирует значение z в x. Лишь p можно переориентировать присваиванием адреса.
r связан с x раз и навсегда, пока существование ссылки и объекта корректно.
ДействиеУказатель int*Ссылка int&
Объявление
int* p = &x;
int& r = x;
Использование
*p = 7;
r = 7;
Можно перенаправить
Да · p = &z;
Нет · r = z; копирует значение в x
Адрес объекта
p
&r
sizeof
Размер указателя
Размер int через r
Ссылка — не «указатель, который всегда разыменован»

Внутренняя реализация параметра-ссылки может использовать адрес, но программа опирается на правила ссылок, а не на устройство машинного кода. sizeof(r) измеряет тип объекта, а не возможный внутренний адрес.

Ссылки в функциях

Передача по значению создаёт отдельный параметр функции. Передача по ссылке позволяет функции обращаться к исходному объекту вызывающего кода без ручных & и *.

Функция swap(int& a, int& b) работает с исходными переменными. Внутри a и b — просто имена соответствующих объектов; можно использовать обычные присваивания.

Параметр по ссылке

Тип T& в списке параметров позволяет изменять объект, переданный аргументом. Для чтения крупных объектов обычно используют const T& (подробнее — в «Дополнительно»).

swap_refs.cpp
#include <iostream>
void swap_values(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}
int main() {
    int x = 3, y = 8;
    swap_values(x, y);
    std::cout << x << ' ' << y; // 8 3
}
Указатели для изменения исходных аргументов не требуются.
Визуализация / передача по значению и по ссылке

Параметр-ссылка позволяет функции работать с объектом вызывающего кода. Параметр по значению создаёт самостоятельный объект: его изменение само по себе не меняет исходную переменную. Ссылка не означает, что изменение обязательно произойдёт; const-ссылка часто используется только для чтения.

x = 3main
swap(x, y)a ↔ x, b ↔ y
y = 8main
Выберите способ передачи и нажмите «Вызвать swap».
В настоящем C++ стандартная библиотека уже предоставляет std::swap.

Почему f(int) и f(int&) конфликтуют при вызове?

Это две разные допустимые перегрузки, но при вызове с изменяемым int-lvalue обе подходят одинаково хорошо. Поэтому компилятор сообщает о неоднозначности — не важно, передали исходное имя или ссылку на него.

Визуализация / выбор перегрузки

Наличие ссылки у аргумента не даёт автоматического преимущества перегрузке с int&: само выражение всё равно обозначает int-lvalue. Пара f(int) и f(int&) делает обычный вызов неоднозначным. В примерах рассмотрены явный выбор через тип указателя и другая пара перегрузок, которая различает доступ для изменения и чтения.

void f(int)кандидат по значению
void f(int&)кандидат по ссылке
f(x) → CE: ambiguous call
Обе перегрузки подходят для изменяемого int-lvalue. Приоритет не определяется тем, передано имя x или r.
r — это int& r = x. Оба выражения f(x) и f(r) ведут себя одинаково.

Связывание и категории значений

Правило

Неконстантная lvalue-ссылка T& должна быть инициализирована при создании и обычно связывается с подходящим изменяемым lvalue. С временным результатом 5 или x++ напрямую её связать нельзя.

Запись int& r; запрещена: без объекта у ссылки нет смысла. Но int& r = x; корректна. ++x возвращает lvalue, в то время как x++ выдаёт старое значение как prvalue.

Lvalue и prvalue

Lvalue обозначает существующий объект (например, x или *p). Prvalue — выражение-значение (например, 5 или x++). Более полная система категорий — в отдельной теме.

reference_binding.cpp
int main() {
    int x = 5;
    int* p = &x;
    int& r = x;           // OK
    int& s = ++x;         // OK: lvalue
    int& t = *p;          // OK: lvalue
    // int& bad = 5;      // CE: prvalue
    // int& bad2 = x++;   // CE: prvalue
    const int& read = 5;  // OK: временный int
    (void)r; (void)s; (void)t; (void)read;
}
Неконстантные lvalue-ссылки не связываются с обычными временными значениями.
Визуализация / какое выражение может стать источником int&

При связывании важны тип и категория выражения. Для обычной int& нужен подходящий изменяемый lvalue; это может быть имя переменной, разыменование указателя или результат префиксного инкремента. Временное число связывается с const int&, но такая ссылка уже не даёт изменять число.

инкремент, разыменование и T&&

✓ корректно
x — изменяемый int-lvalue.
Для примеров: int x=5; const int cx=5; int* p=&x;. Моделируется проверка типа, не выполнение.
Исключение для const и &&

const int& r = 5; корректна: допускается привязка к временному объекту с продлением времени жизни в этом случае. А int&& — другой вид ссылки; он не означает «ссылку на ссылку». Подробности — в закрытом «Дополнительно».

Возврат по ссылке

По умолчанию возвращайте результат по значению: например, int sum(int a,int b) возвращает a+b. Возврат T& нужен, когда вызывающий должен получить доступ к уже существующему объекту, который переживёт вызов. Не возвращайте ссылку на локальную переменную.

Возврат по ссылке — функция с возвращаемым типом T&, результат которой обозначает уже существующий объект. Такой результат обычно является lvalue, поэтому ему можно присвоить значение.

Именно поэтому допустимы v[i] = 42; и *p = 42;: доступ возвращает сам элемент, а не отдельную копию его значения. Операция operator[] у изменяемого std::vector<int> возвращает int& Что с const vector?У const std::vector<int> операция индексации возвращает const int&, так что прочитать элемент можно, а изменить через такую ссылку — нет..

Lvalue-to-rvalue conversion

Когда требуется именно значение, C++ может неявно прочитать lvalue-объект. В int y = x; читается значение x и инициализируется новый объект y.

reference_result.cpp
#include <iostream>
int& identity(int& x) { return x; }
int main() {
    int x = 5;
    identity(x) = 12;   // изменили сам x
    int& r = identity(x);
    int y = identity(x); // создана копия
    r = 8;
    std::cout << x << ' ' << y; // 8 12
}
Возвращённая ссылка может использоваться как lvalue или читаться как значение.
Визуализация / тот же объект или считанное значение

Возврат int& позволяет продолжить работать с исходным объектом после завершения вызова. Но тип переменной, принимающей результат, всё ещё важен: int& сохраняет связь, а int создаёт копию прочитанного значения. Объект, на который возвращена ссылка, обязан оставаться живым.

x / r — один объект5
g(x) возвращает int&. r привязан к тому же x: изменение r меняет x.
В модели g возвращает ссылку на x без иных побочных эффектов.
Тип выражения и категория значения — разные вещи

Выражение *p имеет тип int и категорию lvalue. В частности, decltype(*p) даёт int&. Не нужно утверждать, что обычный тип каждого такого выражения буквально становится int&.

Висячие ссылки и время жизни

Висячая ссылка (dangling reference) — ссылка, объект назначения которой уже не существует. Попытка читать или изменять через неё уничтоженный объект приводит к неопределённому поведению (UB).

Функция может вернуть ссылку на объект, который переживёт её вызов. Но нельзя использовать ссылку на обычную локальную переменную после завершения функции: срок её жизни закончился.

Время жизни объекта (object lifetime)

Промежуток, в течение которого объект существует согласно правилам C++. У автоматических локальных объектов срок обычно заканчивается при выходе из соответствующего блока.

dangling.cpp
int& bad() {
    int local = 7;
    return local;          // опасно: ссылка переживёт local
}
int& good() {
    static int value = 7;
    return value;          // объект продолжает жить
}
int main() {
    // int& r = bad();    // не используем висячую ссылку
    int& r = good();
    r = 42;
}
Возврат ссылки на обычную локальную переменную приводит к висячей ссылке.
Визуализация / какая ссылка переживает возврат из функции

Ссылка не владеет объектом и не делает обычную локальную переменную бессмертной. Возврат из функции уничтожает её локальные объекты, но не уничтожает статические объекты и не освобождает память, выделенную new. Важно отдельно определить, кто отвечает за время жизни и где ссылка перестаёт быть пригодной для обращения.

Дополнительный разбор плохого API

локальная int yобъект внутри f()
int& refпередали ссылку наружу
f() выполняетсявремя жизни
Чтение через ref: допустимо
Пока f() выполняется, локальная переменная жива.
Модель срока жизни. Для heap предполагается, что new выполнился успешно, а delete ещё не вызван.
UB не обязано проявиться сразу

Даже если компилятор выводит ожидаемое число, использование висячей ссылки всё равно некорректно. Не полагайтесь на случайное сохранение байтов в памяти.

Необычный, но допустимый пример: ссылка на динамический объект

В лекции рассмотрен случай, когда функция возвращает int& к объекту, созданному через new. После возврата объект остаётся жив, а по &r можно получить исходный адрес для delete. Однако такой API трудно сопровождать: ответственность за освобождение памяти неочевидна. Лучше использовать RAII.

heap_reference.cpp
int& create() { return *new int(7); }
int main() {
    int& r = create();
    r = 9;
    delete &r;             // допустимо, но плохой дизайн
}
Необычный, но корректный пример лекции: объект создан new и освобождён delete.

Ссылки на другие типы

Правило

Ссылки можно создавать на объекты и функции, включая указатели и массивы. Но не существуют типы «указатель на ссылку», «массив ссылок» и обычная «ссылка на ссылку».

int*& rp = p; — ссылка на сам указатель: при присваивании rp = &z; перенаправится p. А int (&r)[5] = a; — ссылка на целый массив: размер 5 сохраняется в типе.

Ссылка на массив

Не преобразуется в указатель при связывании: int (&r)[5] = a; означает, что r — другое имя всего массива.

reference_forms.cpp
#include <iostream>
void print(int x) { std::cout << x; }
int main() {
    int x = 5, z = 9;
    int* p = &x;
    int*& rp = p;          // ссылка на указатель
    rp = &z;               // теперь p указывает на z
    int a[5]{};
    int (&ra)[5] = a;      // ссылка на массив
    void (&rf)(int) = print; // ссылка на функцию
    ra[0] = *p;
    rf(ra[0]);
}
Допустимые виды ссылок: на указатель, на массив и на функцию.
Визуализация / разрешённые комбинации * & [] ()

Ссылка может именовать указатель, массив или функцию — нужно лишь правильно сгруппировать объявление. При этом массивы ссылок и указатели на ссылки напрямую не объявляются. Для коллекций связанных объектов используют подходящие обёртки, а ссылку на массив удобно применять там, где нужно сохранить размер в типе.

✓ допустимая форма
Ссылка на переменную-указатель int*.
Контекст для примеров: int x; int a[5]; int* p=&x; void f(int); подключён <vector>.
std::vector<int&> не подходит

Контейнеры вроде std::vector не поддерживают элементы-ссылки. Для коллекции невладеющих ссылок возможен std::reference_wrapper<int>, но это дополнительная тема.

Сложные объявления

Декларатор (declarator) — часть объявления, которая описывает имя и форму типа: указатель, ссылка, массив или функция. Скобки определяют порядок связывания этих конструкций.

Объявление читают от имени, учитывая вложенность и приоритет: сначала ближайшие [] и (), затем * / &, после чего выходят из скобок. Тип результата находится слева.

Объявление функции и указатель на функцию

void f(int) объявляет функцию; void (*pf)(int) — переменную-указатель на такую функцию.

declarations.cpp
#include <type_traits>
int a[5]{};                   // массив из 5 int
int* ptrs[5]{};               // массив из 5 int*
int (*pa)[5] = &a;            // указатель на int[5]
void f(int);
void (*pf)(int) = &f;         // указатель на функцию
static_assert(std::is_same_v<decltype(pa), int (*)[5]>);
void f(int) {}
Синтаксические различия: массив, указатель на массив, указатель на функцию.
Визуализация / разбор типа от имени наружу

Читать длинное объявление легче по шагам: найдите имя, рассмотрите ближайшие скобки и только затем переходите к окружающим конструкциям. Сравните исходную запись с псевдонимами типов. Псевдонимы не создают новые виды объектов, но позволяют обсуждать сложный тип небольшими понятными частями.

Углублённый разбор: фабрика указателей на функции

Упростите объявление псевдонимами: using Callback=void(*)(int); using Factory=Callback(*)(int); Factory factory[10]{};

1. p
2. [5]
3. *
4. int
массив из пяти указателей на int
Читай от имени и переходи к ближайшей подходящей конструкции, выходя из скобок по мере необходимости.
Слова в цепочке — порядок разбора объявления, а не порядок выполнения программы.
Скобки меняют смысл

int* a[5] — массив указателей. int (*a)[5] — указатель на целый массив. Аналогично void (*fa[10])(int) — массив указателей на функции.

Ещё один сложный уровень — указатель на функцию, возвращающую указатель на функцию

В лекции автор курса исправляет пример с вложенными объявлениями. Это необязательно запоминать наизусть: важно научиться разбирать его слоями. В современном C++ аналогичную запись часто упрощают с помощью using.

nested_declaration.cpp
#include <type_traits>
void target(int) {}
void (*make_target(int))(int) { return &target; }
void (*(*factory[10])(int))(int){};
// factory: массив из 10 указателей на функции,
// возвращающие указатели на функции
int main() {
    factory[0] = &make_target;
    factory[0](1)(2);
}
Один из сложных деклараторов лекции; скобки читаются изнутри наружу.

Дополнительно

Для расширения кругозора · необязательно для этой лекции
const-ссылки

const T& подходит для чтения объекта без копирования и может связываться с некоторыми временными значениями. Например, const std::string& s = "hello"; даёт время жизни созданной временной строке по правилам такой инициализации.

Rvalue-ссылки

T&& — отдельная категория ссылочного типа, активно используемая в перемещении и perfect forwarding. Это не «ссылка на ссылку».

Reference collapsing

При выводе типов и подстановке шаблонов формы ссылок могут «схлопываться»: например, T& & в соответствующем контексте приводит к T&. Непосредственно объявить обычную ссылку на ссылку нельзя.

additional.cpp
#include <functional>
#include <vector>
int main() {
    int a = 1, b = 2;
    const int& read = 5;
    int&& temporary = 9;
    std::vector<std::reference_wrapper<int>> refs{a, b};
    refs[0].get() = 10;
    (void)read; (void)temporary;
}
Раздел «Дополнительно»: const-ссылки, rvalue-ссылки и reference_wrapper.
Практическое правило

Если объект должен жить дольше функции — используйте ясный способ владения: возвращайте значение, используйте std::unique_ptr или документируйте, кто управляет временем жизни. Не возвращайте ссылку на локальный автоматический объект.

Справочник терминов

Главные определения расположены внутри глав. Здесь они собраны в алфавитном поиске для повторения.

declarator

Декларатор

Часть объявления, связывающая имя с конструкцией типа: *, &, [], ().

Примеры кода

Обмен исходных значений через ссылки

reference-cases.cpp
#include <iostream>
void swap_values(int& left, int& right) {
    const int saved = left;
    left = right;
    right = saved;
}
int main() {
    int a = 0, b = 0;
    if (!(std::cin >> a >> b)) {
        std::cerr << "Invalid numbers\n";
        return 1;
    }
    swap_values(a, b);
    std::cout << a << ' ' << b << '\n';
}

Один объект с двумя именами; copy остаётся независимым.

code-guided-alias.cpp
#include <iostream>

// int& bad() { int local = 7; return local; } // Не возвращать local.

void increment(int& value) {
    ++value;
}

int main() {
    int original = 5;
    int copy = original;
    int& alias = original;
    increment(alias);
    std::cout << original << ' ' << copy << '\n';
    std::cout << std::boolalpha << (&original == &alias) << '\n';
}

Обсудить лекцию с ИИ

Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.

Посмотреть промпт