Ссылки. Константные ссылки

Практический курс по C/C++: https://stepik.org/course/193691

Смотреть материал на YouTube | RuTube

Следующее важное нововведение языка С++ - это ссылки. Объявляются они так же, как и обычные переменные, только после типа следует поставить символ амперсанда. Например:

int d = 10;
int& lnk_d = d;  // ссылка с именем lnk_d на переменную d

или, что то же самое:

int& lnk_d2 {d};
int& lnk_d3 (d);

В итоге, с ячейками памяти, где хранится значение переменной d, связано теперь два имени: d и lnk_d. Ссылку можно воспринимать, как неявный указатель, который хранит адрес переменной, записанной при инициализации. В результате, мы через ссылку lnk_d можем выполнять все те же самые действия, что и с переменной d. Например:

lnk_d = 5;  // переменная d = 5
d = -1;     // ссылка lnk_d связана со значением -1
lnk_d *= 10; // значение d увеличено в 10 раз
lnk_d++;  // инкремент переменной d

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

double& alias_d;

При этом в инициализаторе можно прописывать любое допустимое lvalue выражение. Например:

int a = 10;
int *ptr = &a;
int ar[] = {1, 2, 3};
 
int& lnk_1 = a;        / / ok
int& lnk_2 = *ptr;    // ok
int& lnk_3 = ar[1];   // ok
int& lnk_4 = 10;  // ошибка
int& lnk_5 = ptr;  // ошибка

То есть, ссылка должна быть связана с областью памяти, где хранятся данные того же типа, который указан при объявлении ссылки.

Некоторые из вас, возможно, задаются вопросом, зачем вообще нужны ссылки, когда с переменной (данными) можно работать напрямую через их имена? На самом деле, польза ссылок проявляется там же, где и польза указателей. Например, в классической задаче обмена значениями двух переменных. Без использования ссылок мы могли бы для этой цели объявить следующую функцию:

void swap_d(double* x, double* y)
{
    double t = *x;
    *x = *y;
    *y = t;
}

И, затем, в функции main() вызвать ее для переменных типа double:

double a{1.2}, b{-3.4};
swap_d(&a, &b);
std::cout << a << " " << b << std::endl;

Как видите, при вызове в аргументах функции swap_d() дополнительно приходится указывать операцию взятия адреса переменных a и b. Это не очень удобно и красиво. Куда лучше было бы записать эту же функцию с использованием ссылок:

void swap_d(double& x, double& y)
{
    double t = x;
    x = y;
    y = t;
}

И ее последующий вызов:

swap_d(a, b);

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

Также обратите внимание, что при объявлении параметров функции в виде ссылок их не нужно инициализировать. Компилятор поставит им в соответствие те переменные, которые мы укажем при вызове этой функции. Причем, сами переменные при этом не копируются, а лишь формируется связь между параметрами-ссылками и переменными, указанными при вызове функции. В частности, это означает, что если нужно передать в функцию какие-либо объемные данные, например, большие структуры, то часто целесообразно это сделать через ссылки.

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

short p[] = {1, 2, 3, 4};

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

for (short& x : p)
    x *= 2;

И выведем полученный массив p в консоль:

for (int i = 0; i < sizeof(p)/sizeof(*p); ++i)
    std::cout << p[i] << " ";

Увидим:

2 4 6 8

Почему здесь в первом цикле for нужно прописывать переменную x в виде ссылки? Думаю, вы уже догадались, что в противном случае:

for (short x : p)
    x *= 2;

мы получим копирование текущего значения массива p в переменную x и изменение (увеличение в 2 раза) коснется этой новой переменной, а не элемента массива. Ссылка же решает нашу задачу.

Константные ссылки

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

int s = 0;
const int& ls = s;
 
int x = ls;  // чтение данных разрешено
ls = 5; // ошибка, запись нового значения невозможна

Соответственно, константные ссылки могут быть инициализированы, как обычными переменными, так и константными:

int s = 0;
const int d = -2;
 
const int& ls = s;
const int& ld = d;

А вот обычная ссылка может вести только на такую же обычную переменную:

int s = 0;
const int d = -2;
 
int& ls = s;    // ok
int& ld = d;    // ошибка

Это вполне очевидное ограничение, т.к. иначе бы мы смогли изменить константную переменную d через ссылку ld.

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

struct point {
    char name[50];      // название точки
    double x, y;        // координаты точки
};

Затем, объявим функцию, которая вычисляет длину такого радиус-вектора:

double length(const point& p)
{
    return sqrt(p.x * p.x + p.y * p.y);
}

И вызовем ее в функции main():

int main()
{
    point p2 {"first", 10.0, 20.0};
    double len = length(p2);
    std::cout << len << std::endl;
 
    return 0;
}

Смотрите, в момент вызова функции length(p2), структура p2 не копируется в параметр p, копируется только ее адрес. В результате ссылка p связывается со структурой p2. А дополнительное ключевое слово const гарантирует, что структура p2 никак не будет менять свое состояние внутри функции length(). Соответственно, программист, который использует эту функцию, понимает, что если какой-либо ее параметр помечен как const, то он может быть уверенным в неизменности передаваемого аргумента.

И еще раз отмечу, что если бы в функции length() передача аргумента происходила бы не по ссылке, а по значению:

double length(const point p) ...

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме