10.16 – Переменные-ссылки
До сих пор мы обсуждали два основных типа переменных:
- Обычные переменные, которые напрямую хранят значения.
- Указатели, которые содержат адрес другого значения (или
nullptr
), которое можно получить путем косвенного обращения через адрес, на который указывает указатель.
Ссылки – это третий основной тип переменных, поддерживаемый C++. Ссылка – это тип переменной C++, которая действует как псевдоним для другого объекта или значения.
C++ поддерживает три вида ссылок:
- ссылки на неконстантные значения (обычно называемые просто «ссылками» или «неконстантными ссылками»), которые мы обсудим в этом уроке;
- ссылки на константные значения (часто называемые «константными ссылками»), которые мы обсудим в следующем уроке;
- в C++11 добавлены ссылки на r-значения (r-value), которые мы подробно рассмотрим в главе о семантике перемещения.
Ссылки на неконстантные значения
Ссылка (на неконстантное значение) объявляется с помощью амперсанда (&
) между типом ссылки и именем переменной:
int value{ 5 }; // обычная переменная int
int &ref{ value }; // ссылка на переменную value
В этом контексте амперсанд означает не «адрес», а «ссылка на».
Ссылки на неконстантные значения часто для краткости называют просто «ссылками».
Как и положение звездочки у указателей, не имеет значения, помещаете ли вы амперсанд рядом с типом или рядом с именем переменной.
int value{ 5 };
// эти два объявления одинаковы
int& ref1{ value };
int &ref2{ value };
Лучшая практика
При объявлении переменной-ссыдки ставьте амперсанд рядом с типом, чтобы его было легче отличить от оператора адреса.
Ссылки как псевдонимы
Ссылки обычно действуют идентично значениям, на которые они ссылаются. В этом смысле ссылка действует как псевдоним для объекта, на который ссылается. Например:
int x{ 5 }; // обычная переменная int
int& y{ x }; // y - ссылка на x
int& z{ y }; // z - тоже ссылка на x
В приведенном выше фрагменте установка или получение значения x
, y
или z
будет делать одно и то же (установка или получение значения x
).
Давайте посмотрим на использование ссылок:
#include <iostream>
int main()
{
int value{ 5 }; // обычная переменная int
int& ref{ value }; // ссылка на переменную value
value = 6; // value теперь равна 6
ref = 7; // value теперь равна 7
std::cout << value << '\n'; // выводит 7
++ref;
std::cout << value << '\n'; // выводит 8
return 0;
}
Этот код печатает:
7
8
В приведенном выше примере ref
и value
рассматриваются как синонимы.
Использование оператора адреса для ссылки возвращает адрес значения, на которое она ссылается:
cout << &value; // выводит 0012FF7C
cout << &ref; // выводит 0012FF7C
Что и следовало ожидать, если ref
действует как псевдоним для value
.
l-значения и r-значения
В C++ переменные представляют собой тип l-значения (l-value). l-значение (l-value) – это значение, имеющее адрес (в памяти). Поскольку все переменные имеют адреса, все переменные являются l-значениями. Название l-значение появилось потому, что l-значения – это единственные значения, которые могут быть в левой части оператора присваивания. Когда мы выполняем присваивание, левая часть оператора присваивания должна быть l-значением. Следовательно, инструкция вроде 5 = 6; вызовет ошибку компиляции, потому что 5 не является l-значением. Значение 5 не имеет памяти, поэтому ему ничего не может быть присвоено. 5 означает 5, и его значение нельзя переназначить. Когда l-значению присваивается значение, текущее значение по этому адресу памяти перезаписывается.
Противоположностью l-значений являются r-значения (r-value). r-значение (r-value) – это выражение, которое не является l-значением. Примерами r-значений являются литералы (например, 5, которое вычисляется как 5) и выражения, отличные от l-значений (например, 2 + x).
Вот несколько примеров операторов присваивания, показывающих, как вычисляются r-значения:
int y; // определяем y как переменную int
y = 4; // 4 вычисляется как 4, которое затем присваивается y
y = 2 + 5; // 2 + 5 вычисляется как 7, которое затем присваивается y
int x; // определяем x как переменную int
x = y; // y вычисляется как 7 (было раньше), которое затем присваивается x
x = x; // x вычисляется как 7, которое затем присваивается x (бесполезно!)
x = x + 1; // x + 1 вычисляется как 8, которое затем присваивается x
Давайте подробнее рассмотрим последнюю инструкцию присваивания выше, поскольку она вызывает наибольшую путаницу.
x = x + 1;
В этом инструкции переменная x
используется в двух разных контекстах. Слева от оператора присваивания x
используется как l-значение (переменная с адресом). В правой части оператора присваивания x
используется в r-значении и будет вычислено для получения значения (в данном случае 7). Когда C++ вычисляет показанную выше инструкцию, он делает это так:
x = 7 + 1;
Это делает очевидным, что C++ присвоит значение 8 переменной x
.
Ключевой момент заключается в том, что в левой части присваивания должно быть что-то, что представляет адрес памяти (например, переменную). Всё, что находится справа от присваивания, будет вычислено для получения значения.
Примечание: константные переменные считаются неизменяемыми l-значениями.
Ссылки должны быть инициализированы
Ссылки должны быть инициализированы при создании:
int value{ 5 };
int& ref{ value }; // допустимая ссылка, инициализирована переменной value
int& invalidRef; // недопустимо, нужно на что-то ссылаться
В отличие от указателей, которые могут содержать нулевое значение, нулевых ссылок не существует.
Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-значениями. Они не могут быть инициализированы константными l-значениями или r-значениями.
int x{ 5 };
int& ref1{ x }; // ok, x - неконстантное l-значение
const int y{ 7 };
int& ref2{ y }; // недопустимо, y - константное l-значение
int& ref3{ 6 }; // недопустимо, 6 - r-значение
Обратите внимание, что в среднем случае вы не можете инициализировать неконстантную ссылку с помощью константного объекта – в противном случае вы сможете через ссылку изменить значение константного объекта, что нарушит константность объекта.
Ссылки нельзя переназначать
После инициализации ссылку нельзя изменить для ссылки на другую переменную. Рассмотрим следующий фрагмент:
int value1{ 5 };
int value2{ 6 };
int& ref{ value1 }; // ok, ref теперь псевдоним для value1
ref = value2; // присваивает 6 (значение value2) значению value1 - НЕ меняет ссылку!
Обратите внимание, что вторая инструкция может делать не то, что вы ожидаете! Вместо изменения ref
для ссылки на переменную value2
она присваивает значение value2
значению value1
.
Ссылки как параметры функций
Чаще всего ссылки используются как параметры функций. В этом контексте параметр-ссылка действует как псевдоним для аргумента, и в параметр не делается копия аргумента. Это может привести к повышению производительности, если аргумент большой или копирование дорого.
В предыдущем уроке мы говорили о том, как передача аргумента-указателя в функцию позволяет функции выполнять косвенное обращение через указатель для непосредственного изменения значения аргумента.
Ссылки в этом отношении работают аналогично. Поскольку параметр-ссылка действует как псевдоним для аргумента, функция, использующая параметр-ссылку, может изменять переданный аргумент:
#include <iostream>
// ref - это ссылка на переданный аргумент, а не копия
void changeN(int& ref)
{
ref = 6;
}
int main()
{
int n{ 5 };
std::cout << n << '\n';
changeN(n); // обратите внимание, что этот аргумент не обязательно должен быть ссылкой
std::cout << n << '\n';
return 0;
}
Эта программа печатает:
5
6
Когда аргумент n
передается функции, параметр функции ref
устанавливается как ссылка на аргумент n
. Это позволяет функции изменять значение n
через ref
! Обратите внимание, что n
не обязательно должно быть ссылкой.
Лучшая практика
Когда функция должна изменить аргумент, передавайте его по неконстантной ссылке (а не по указателю).
Основным недостатком использования неконстантных ссылок в качестве параметров функции является то, что аргумент должен быть неконстантным l-значением. Это может ограничивать. Мы поговорим об этом подробнее (и о том, как это обойти) в следующем уроке.
Использование ссылок для передачи функциям массивов в стиле C
Одна из самых неприятных проблем с массивами в стиле C заключается в том, что в большинстве случаев при вычислении они превращаются в указатели. Однако если массив в стиле C передается по ссылке, этого разложения не происходит.
Вот пример:
#include <iostream>
#include <iterator>
// Примечание: вам нужно указать размер массива в объявлении функции
void printElements(int (&arr)[4])
{
// теперь мы можем это сделать, так как массив не раскладывается в указатель
int length{ static_cast<int>(std::size(arr)) };
for (int i{ 0 }; i < length; ++i)
{
std::cout << arr[i] << '\n';
}
}
int main()
{
int arr[]{ 99, 20, 14, 80 };
printElements(arr);
return 0;
}
int main ()
{
int arr [] {99, 20, 14, 80};
printElements (обр);
возврат 0;
}
Обратите внимание, что для того, чтобы это работало, вам необходимо явно указать размер массива в параметре.
Ссылки как ярлыки
Вторичное (гораздо менее частое) использование ссылок – облегчение доступа к вложенным данным. Рассмотрим следующую структуру:
struct Something
{
int value1;
float value2;
};
struct Other
{
Something something;
int otherValue;
};
Other other;
Допустим, нам нужно было работать с полем value1
структуры Something
в other
. Обычно мы получаем доступ к этому члену как other.something.value1
. Если к этому члену есть много разных обращений, код может стать беспорядочным. Ссылки позволяют облегчить доступ к члену:
int& ref{ other.something.value1 };
// ref теперь можно использовать вместо other.something.value1
Таким образом, следующие две инструкции идентичны:
other.something.value1 = 5;
ref = 5;
Это поможет сохранить ваш код более чистым и читабельным.
Ссылки против указателей
Ссылки и указатели имеют интересную взаимосвязь – ссылка действует как указатель, который при доступе неявно выполняет косвенное обращение через себя (внутри компилятора ссылки обычно реализуются с помощью указателей). Таким образом, в следующем коде:
int value{ 5 };
int* const ptr{ &value };
int& ref{ value };
*ptr
и ref
вычисляются одинаково. В результате следующие две инструкции производят одинаковый эффект:
*ptr = 5;
ref = 5;
Поскольку ссылки должны быть инициализированы, чтобы быть допустимыми (не могут быть нулевыми), и не могут быть изменены после установки, их, как правило, использовать намного безопаснее, чем указатели (поскольку нет риска косвенного обращения через нулевой указатель). Однако, соответственно, они несколько более ограничены в функциональности.
Если какая-либо задача может быть решена с помощью ссылки или указателя, ссылка обычно должна быть предпочтительнее. Указатели следует использовать только в ситуациях, когда ссылки недостаточны (например, при динамическом распределении памяти).
Резюме
Ссылки позволяют нам определять псевдонимы для других объектов или значений. Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-значениями. После инициализации ссылки нельзя переназначить.
Ссылки чаще всего используются в качестве параметров функций, либо когда мы хотим изменить значение аргумента, либо когда мы не хотим выполнять дорогостоящее копирование аргумента.