Указатель решает две разные задачи: «объект где-то там, в куче» и «дай функции доступ к моей
переменной». Для второй он избыточен – адрес приходится брать, потом разыменовывать, и всюду
возможен nullptr. Ссылка закрывает ровно эту задачу и делает это без синтаксического шума.
Псевдоним
int value = 42;
int& ref = value; // ref – другое имя для value
ref = 100; // value == 100
std::cout << value; // 100
std::cout << &ref; // тот же адрес, что и &value
ref – не копия и не отдельный объект. Это второе имя для той же ячейки памяти.
Три правила, из которых следует всё остальное:
- Ссылку обязательно инициализировать при объявлении.
int& r;не скомпилируется. - Привязку нельзя изменить.
ref = otherприсваивает значениеotherвvalue, а не перенаправляет ссылку. - Ссылка не может быть пустой. Проверять её на
nullptrбессмысленно – такого состояния нет.
Занимает ли ссылка память? Стандарт не обязывает. На практике компилятор либо полностью её устраняет (просто работает с исходной переменной), либо реализует как указатель – например, когда ссылка является параметром функции, которую не удалось встроить. Считать её «указателем с приятным синтаксисом» – рабочая модель, но помнить про пункт 3 всё равно нужно.
Ссылка против указателя
| Указатель | Ссылка | |
|---|---|---|
| Может быть пустым | да, nullptr |
нет |
| Можно переприсвоить | да | нет |
| Синтаксис доступа | *p, p->field |
как у обычной переменной |
| Арифметика | да | нет |
| Когда брать | объект может отсутствовать; нужно перемещаться по памяти | объект точно есть и один |
Практическое правило: ссылка по умолчанию, указатель – когда нужна пустота или
переприсваивание. Функция, принимающая Widget&, самой сигнатурой говорит «объект
обязателен»; принимающая Widget* – «может быть и nullptr, я проверю».
Ссылки в параметрах
Это главное применение. Три варианта и когда какой:
void ByValue(std::string s); // копия: функция получает свой экземпляр
void ByRef(std::string& s); // без копии, функция может менять
void ByConstRef(const std::string& s);// без копии, менять нельзя
std::string text = "hello";
ByValue(text); // скопирована вся строка
ByRef(text); // text может измениться
ByConstRef(text); // text не изменится, копии нет
Для int, double, указателя копия дешевле ссылки – их передают по значению. Для строки,
вектора, любого объекта с полями – const T&.
Читая чужой код, обращайте внимание на & без const в сигнатуре: это единственный внешний признак того, что функция изменит ваш объект. У вызова Update(config) по виду не отличить, передаётся копия или оригинал, – смотреть надо в объявление.
Константная ссылка и временные объекты
У const T& есть особенность: к ней можно привязать временный объект, и время его жизни
продлевается до конца жизни ссылки.
void Print(const std::string& s);
Print("literal"); // создан временный std::string, привязан, уничтожен после вызова
const std::string& r = std::string("temp"); // временный живёт, пока жива r
С неконстантной ссылкой так нельзя – std::string& r = std::string("temp"); не скомпилируется.
Логика простая: изменять то, что вот-вот исчезнет, почти наверняка ошибка.
Продление жизни работает только для прямой привязки. Если функция вернула ссылку на временный объект, продлевать уже нечего:const std::string& bad = MakeName().substr(0, 3); – временный уничтожается в конце выражения, bad становится висячей. Ссылка так же легко превращается в мусор, как и указатель, просто выглядит безобиднее.
Ссылка на массив
Обычная передача массива теряет его размер. Ссылка – нет:
void Foo(int (&a)[3]) { /* размер известен и проверяется */ }
int good[3] = {};
int bad[1] = {};
Foo(good); // ок
Foo(bad); // ошибка компиляции: invalid initialization of reference of type 'int (&)[3]'
Скобки вокруг &a обязательны: без них int& a[3] читалось бы как массив ссылок, которого в
C++ не бывает.
Конструкция выглядит громоздко и в чистом виде используется редко. Но в связке с шаблонами она даёт способ узнать размер массива на этапе компиляции – к этому мы вернёмся в модуле про шаблоны.
Возврат по ссылке
Функция может вернуть ссылку – если возвращаемый объект переживёт вызов.
class Buffer {
public:
int& At(size_t i) { return data_[i]; } // объект живёт в *this, всё честно
private:
int data_[128];
};
Buffer b;
b.At(3) = 42; // работает: слева от = стоит ссылка на элемент
И чего делать нельзя:
int& Broken() {
int x = 42;
return x; // кадр умрёт на выходе, ссылка станет висячей
}
Правило то же, что и для указателей: не возвращайте ссылку на то, что живёт в кадре функции. Возвращать по значению безопасно и почти всегда бесплатно – компилятор строит результат прямо там, где он нужен вызывающему.