Другое имя для той же сущности
На странице о параметрах функций каждый аргумент копировался в функцию. Именно из-за этой копии функция не может изменить переменную вызывающего кода — она всегда видит лишь свой дубликат. Ссылка разрушает эту стену. Это псевдоним: второе имя, привязанное к существующей переменной и разделяющее ровно ту же память.
Ссылку создают с помощью & в объявлении. После привязки ссылка и оригинал неразличимы:
Два правила делают ссылки безопасными и предсказуемыми: ссылка должна быть инициализирована в момент объявления (int& r; — ошибка компиляции), и её нельзя переназначить на что-то другое впоследствии. Присваивание ссылке всегда пишет в то, к чему она была изначально привязана.
Передача по ссылке: позвольте функции дотянуться обратно
Настоящая выгода — в функциях. Поставьте & у параметра, и функция получит псевдоним аргумента вызывающего кода вместо копии. Теперь изменения внутри функции видны и снаружи:
Уберите &, и addBonus увеличил бы одноразовую копию, оставив total равным 100. Этот единственный символ и есть вся разница. Это канонический способ написать функцию, которая возвращает более одного результата или изменяет свои входные данные на месте. Классический пример — обмен двух переменных:
Без ссылок swapValues обменял бы только локальные копии, и x/y остались бы 1 2. (В стандартной библиотеке уже есть std::swap, но написать его самому полезно: это наглядно показывает, что даёт параметр-ссылка.)
Ссылки на const: читай быстро, обещай не трогать
Передача по ссылке также избегает копирования — а для большого объекта эта копия может быть дорогой. Но простой параметр T& сигнализирует «я могу это изменить», что вводит в заблуждение, когда вы хотите только читать. Решение — const T&: вы получаете скорость без копирования, как у ссылки, и обещание, гарантированное компилятором, что функция не изменит аргумент.
Не-const-ссылка может привязаться только к изменяемой переменной, но const-ссылка может также привязаться к литералам и временным объектам — вот почему greet("literal works too") компилируется. Практическое правило выбора типа параметра:
void f(int x) // дешёвый тип, только чтение -> просто копируй
void f(const string& s) // тяжёлый тип, только чтение -> const-ссылка
void f(string& s) // ты намерен изменить объект вызывающего кода
По умолчанию используйте const T& для любого классового типа, который вы только читаете (string, vector, ваши собственные структуры), и оставляйте не-const-ссылку для случаев, когда вы действительно хотите записать обратно.
Возврат ссылки
Функция также может возвращать ссылку, передавая вызывающему коду псевдоним на нечто уже существующее. Это распространено в коде контейнеров — именно это позволяет работать v[i] = 5 и именно это делает operator[] под капотом:
Поскольку at возвращает int&, выражение вызова at(data, 1) само является lvalue, которому можно присваивать. Верните вместо этого простой int, и at(data, 1) = 42 не скомпилируется — вы бы присваивали временной копии.
Главная ловушка: висячие ссылки
Ссылка ничем не владеет; она лишь указывает на память, которая живёт где-то ещё. Если эта память умирает, пока ссылка ещё используется, у вас висячая ссылка, и чтение через неё — неопределённое поведение: может вывести мусор, упасть или казаться работающим, пока не испортит вам день в продакшене. Классическая ошибка — вернуть ссылку на локальную переменную:
int& broken() {
int local = 42;
return local; // BUG: local уничтожается, когда broken() возвращает управление
} // возвращённая ссылка повисает
int main() {
int& r = broken();
cout << r << "\n"; // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ - читает мёртвую память
}
Переменная local исчезает в тот момент, когда broken возвращает управление, поэтому ссылка указывает на уже освобождённое пространство стека. Возвращайте ссылку только на то, что переживёт вызов — параметр, переданный по ссылке, элемент данных или static. Если значение вычисляется внутри функции, возвращайте по значению и позвольте компилятору оптимизировать копию. Та же ловушка подстерегает циклы на основе диапазона и любую ссылку, привязанную к временному объекту: никогда не удерживайте ссылку дольше времени жизни того, что она именует.
Далее: перегрузка функций
Ссылки дают вам второй регулятор у каждого параметра — копия или псевдоним, изменяемый или const — и этот регулятор напрямую связан со следующей темой. Далее перегрузка функций позволяет определить несколько функций с одним именем, но разными списками параметров, и компилятор выбирает нужную, сопоставляя типы аргументов — в том числе передаются ли они по значению, по ссылке или по const-ссылке.
Часто задаваемые вопросы
Что такое ссылка в C++?
Ссылка — это псевдоним существующей переменной, другое имя для той же памяти. Её создают с помощью & в объявлении: int& r = x;. После этого r и x взаимозаменяемы; изменение одного меняет другое. Ссылки должны быть инициализированы при объявлении и никогда не могут быть переназначены на другую переменную.
В чём разница между передачей по значению и передачей по ссылке в C++?
Передача по значению (void f(int x)) копирует аргумент, поэтому функция работает со своей копией, а переменная вызывающего кода остаётся нетронутой. Передача по ссылке (void f(int& x)) даёт функции прямой доступ к переменной вызывающего кода, поэтому изменения видны после вызова — и копия не создаётся, что важно для крупных объектов.
Когда следует использовать параметры-ссылки на const в C++?
Используйте const T&, когда функции нужно только читать параметр, но тип дорого копировать (string, vector, большие структуры). Вы получаете скорость без копирования, как у ссылки, плюс гарантию компилятора, что функция не изменит значение вызывающего кода. Для дешёвых типов вроде int или double обычная передача по значению проще и так же быстра.