10.16 – Переменные-ссылки

Добавлено 9 июня 2021 в 05:06
Глава 10 – Массивы, строки, указатели и ссылки  (содержание)

До сих пор мы обсуждали два основных типа переменных:

  • Обычные переменные, которые напрямую хранят значения.
  • Указатели, которые содержат адрес другого значения (или nullptr), которое можно получить путем косвенного обращения через адрес, на который указывает указатель.

Ссылки – это третий основной тип переменных, поддерживаемый C++. Ссылка – это тип переменной C++, которая действует как псевдоним для другого объекта или значения.

C++ поддерживает три вида ссылок:

  1. ссылки на неконстантные значения (обычно называемые просто «ссылками» или «неконстантными ссылками»), которые мы обсудим в этом уроке;
  2. ссылки на константные значения (часто называемые «константными ссылками»), которые мы обсудим в следующем уроке;
  3. в 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-значениями. После инициализации ссылки нельзя переназначить.

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

Теги

C++ / Cppl-value / l-значениеLearnCppr-value / r-значениеДля начинающихОбучениеПрограммированиеСсылка / Reference (программирование)

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.