Ссылки в С++ — это отдельный (невстроенный) тип данных. В большинстве случаев ссылочные переменные работают как автоматически разыменовываемые указательные переменные. Из-за очень сильной похожести между двумя разными типами (ссылка и указатель) новичкам трудно, очень трудно уловить тонкое различие. Если бы ссылке дали название ярлык, то, возможно, проблем с пониманием было намного меньше: ссылка напоминает ярлыки программ. Большинство из вас, мои читатели, прекрасно знает, что такое ярлык к документу, к программе, к игре. Основной файл один, а ссылающихся на него ярлыков может быть бесконечно много. Такие вот ссылающиеся ярлыки, только ссылающиеся не на программы, а на какие-то именованные переменные, называются ссылками.
Ссылка — это второе имя объекта. Псевдоним объекту.
Навешивание ссылок на переменные == навешивание переменным личных ярлыков.
Часто говорят, что ссылка — это саморазыменовывающийся указатель. Это близко к правде, но неправда. Говорят так только для того, чтобы спросивший смог быстрее понять суть-существование ссылки. Опытные программисты, умеющие читать документацию, прекрасно знают, что в разных компиляторах ссылки могут иметь отличающиеся реализации. На выходе-то одно, но устройство разное.
На самом деле увидеть пример, где ссылочная переменная не ведёт себя как саморазыменованный указатель очень просто:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
//Листинг #1 Ссылка на массив Не ведёт себя как саморазыменовывающийся указатель
#include <iostream>
usingnamespacestd;
intmain(){
intP[10]{1,2,3};
int(&ref)[10] = P;
cout<<ref<<'\n';//Вывод чего-то одного
cout<<*P<<'\n';//Разыменованный указатель здесь является первым элементом массива
}
Но в целом это допущение, что ссылка — саморазыменовывающийся указатель, помогает многим новичкам. Чем же ссылка принципиально отличается от указателя? — этот вопрос тревожит армию новичков постоянно.
Синтаксис. Ссылка по свойствам напоминает указатель, но при написании ссылочные переменные не требуют принудительного разыменования.
Ссылка есть только в C++. Указатель есть и в C++, и в C.
В отличие от указателя ссылку нельзя перенаправить ни на какой новый адрес, ссылку нельзя переназначить. В этом плане ссылка эквивалентна константному указателю.
В отличие от указателя, который может быть не инициализирован, ссылка требует обязательной инициализации.
Ссылка может указывать на указатель. Указатель на ссылку указывать не может.
Уровень косвенности у указателя может быть очень большим (указатели на указатели). Ссылка допускает только один уровень косвенности: нельзя иметь ссылку на ссылку.
const-ссылка может продлевать время жизни временного объекта, на который она указывает. У указателя такая особенность отсутствует.
Нельзя иметь массив ссылок. Массив указателей — можно.
В отличие от указательной переменной, которая всегда занимает место в памяти, ссылочная переменная может не занимать место в памяти вообще (зависит от реализации компилятора).
Ссылка не встронный тип данных, поэтому говорить, что ссылка указывает на адрес или говорить, что ссылка указывает на название привязанной к себе переменной — некорректно.
Мне никогда не нравилось определение ссылки: второе имя переменной. Оно, считаю, откровенно выбрано неудачно и приводит к абсолютному непониманию большинством новичков. Я могу обозначить ссылку как представителя переменной, и такое определение будет намного больше соответствовать действительности, чем даваемое нам и поныне. Указатель больше как агент: способен менять клиентов и даже место жительства (смещение указателя, если неконстантный). Под клиентами понимаем переменные и значения, хранимые в памяти, на которые заточен указатель в какой-то момент времени.
Ссылки использовать удобнее указателей, потому что не приходится задумываться, где разыменовывать переменную, где не разыменовывать. Использование ссылок, как и использование указателей, зачастую помогает ускорить выполнение функции, избегая накладных расходов на копирование. Принимать приходящие вовнутрь функций аргументы вовнутрь константных ссылок, если приходящие аргументы представляют собой большие структуры данных, считается хорошим тоном.
Если прикинуть какие-нибудь аналогии, то ссылки подобны агентам-посредникам в то время, когда указатели больше похожи на курьеров. К переменной можно обратиться напрямую, а можно через любой из, возможно, множества псевдонимов, т. е. задействовать ссылку, отчего получается, что между программистом и переменной есть третья сторона, напоминающая агента-посредника. В случае работы с указателем указателю задаётся адрес и даётся план работ: или измени значение внутри этого адреса, или сравни значения, или даётся указание смены адреса. Так получается, что мне указатель напоминает курьера с правами давать задачи рабочему классу (изменение значений не сам же указатель делает, а, условно, рабочий класс: операционная система).
На самом деле без ссылок вполне можно обойтись, но коды будут сложнее: одни константные указатели, указатели на константы и константные указатели на константы чего стоят! А тут константную ссылку обозначил и живёшь, не путаешься, не горюешь. В этом плане использование ссылок безопаснее использования указателей. Вдобавок, свойство продлевать жизнь временной переменной находит себе применение при написании шаблонов.
Одно из очень замечательных свойств ссылок — сохранять данные об объекте. Например, если ссылка на массив, то при передаче ссылки на массив в функцию, самой функции необязательно передавать данные о размере массива, эта информация будет сохранена ссылкой:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Листинг #2.1 Параметр функции ссылка на массив clang
#include <iostream>
usingnamespacestd;
voidfoo(int(&arr)[10]){ //Если в параметре ссылка на массив
unsigned MAX_LEN = sizeof(arr) / sizeof(*arr);//вполне себе узнаётся
for(unsignedi=0;i<MAX_LEN;i++)cout<<arr[i]<<'\t';//и всё работает
cout<<'\n';
}
intmain(){
intP[10]{1,2,3};
int(&ref)[10] = P;
foo(ref);//передача непосредственно самой ссылки теперь не даёт скомпилировать код
foo(P);//передача имя массива воспринимается как значение, а не как указатель
}
Одно из неудобств — нужно задействовать точный тип. В отличие от указателя, где в параметре функции указатель напоминает массив, сколько элементов в массиве есть, столько и надо сообщать ссылке. Посмотрим пример с параметром функции название массива:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Листинг #2.2 Параметр функции название массива clang
#include <iostream>
usingnamespacestd;
voidfoo(intarr[5]){//Если в параметре название массива
unsignedMAX_LEN=sizeof(arr)/sizeof(*arr);//информация о типе потеряна, не опознаётся. обслуживается указатель (имя массива приведено к указателю)
for(unsignedi=0;i<MAX_LEN;i++)cout<<arr[i]<<'\t';//теперь это неправильно
cout<<'\n';
}
intmain(){
intP[10]{1,2,3};
int(&ref)[10] = P;
foo(ref);//передача непосредственно самой ссылки теперь не даёт скомпилировать код
foo(P);//передача имемни массива воспринимается как значение цельный массив, а не как указатель
}
Оттого, что у нас внутри функции обслуживается указатель, операция sizeof применяется не к целому массиву, а к указателю на первый элемент массива. Но если поменять параметр функции в круглых скобках на ссылку, то пример компилироваться не станет:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Листинг #2.3 Параметр функции ссылка clang
#include <iostream>
usingnamespacestd;
voidfoo(int(&arr)[5]){ //Если в параметре ссылка на массив, то ожидается точно такой массив, какой привязан к ссылке
for(unsignedi=0;i<MAX_LEN;i++)cout<<arr[i]<<'\t';//теперь это правильно
cout<<'\n';
}
intmain(){
intP[10]{1,2,3};
int(&ref)[10] = P;
foo(ref);//функция foo ожидает массив int[5], Но уходит int[10], ошибка компиляции
foo(P);//функция foo ожидает массив int[5], Но уходит int[10], ошибка компиляции
}
В общем, смысл таков, что ссылка представляет собой малюсенькую сущность, берущую на себя обязанности агента оригинальной переменной и не требует к себе повышенного внимания. Указатель же представляет собой своего рода свободолюбивую переменную и контроллировать указательные переменные программистам сложнее.
Из-за неявных приведений названий массивов к указательным переменным мы иногда не можем взять и просто перегрузить функции:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Листинг #3.1 Перегрузка функций со ссылкой и названием массива в параметрах clang
#include <iostream>
usingnamespacestd;
voidfoo(int(&arr)[5]){ //Если в параметре ссылка
unsigned MAX_LEN = sizeof(arr) / sizeof(*arr);//информация о типе сохранена. обслуживается непосредственно массив
for(unsignedi=0;i<MAX_LEN;i++)cout<<arr[i]<<'\t';//теперь это правильно
cout<<'\n';
}
voidfoo(intarr[]){//Если в параметре имя массива
unsignedMAX_LEN=sizeof(arr)/sizeof(*arr);//информация о типе теряется. обслуживается указатель
for(unsignedi=0;i<MAX_LEN;i++)cout<<arr[i]<<'\t';//теперь это неправильно
cout<<'\n';
}
intmain(){
intP[5]{1,2,3};
int(&ref)[5] = P;
foo(P);//ошибка компиляции, компилятор не может выбрать функцию
}
Проблему перегрузки можно решить на уровне шаблонов, но я не смогу объяснить показываемый пример. Оставляю его здесь, чтобы не затерять и не забыть:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Листинг #3.2 Перегрузка функций со ссылкой и указателем clang C++11
Выбор отображения различия ссылки от указателя на примере ссылки на массив мне показался достаточно привлекательным, поэтому примеры выбраны именно показанные. Вы должны чётко понимать, что ссылка на самом деле никакой не саморазыменовывающийся указатель, хотя очень его напоминает. Также должны понимать, что ссылки в разных компиляторах могут реализовываться по-разному, поэтому вполне возможно, что в каких-то реализациях компиляторов они и есть обычные константные указатели, обладающие умением саморазыменовываться, но разработчикам компиляторов правила не запрещают реализовывать ссылки другим образом.
Статья полностью переписана 28.03.2018
Один комментарий на «“Ссылки в C++ для начинающих. Повторение и продолжение знакомства”»
Ссылок на самом деле в программе не существует, также как и имен переменных. Используются они только при компиляции и линковке. А вот указатель — это вполне реальная ячейка памяти, содержащая адрес.
Проще эти различия понять тем, кто хоть немного знаком с любым ассемблером. Ссылка — это когда адрес обрабатываемой ячейки памяти дан непосредственно в команде, а указатель — это когда адрес мы сначала загружаем из другой ячейки памяти.
Ссылок на самом деле в программе не существует, также как и имен переменных. Используются они только при компиляции и линковке. А вот указатель — это вполне реальная ячейка памяти, содержащая адрес.
Проще эти различия понять тем, кто хоть немного знаком с любым ассемблером. Ссылка — это когда адрес обрабатываемой ячейки памяти дан непосредственно в команде, а указатель — это когда адрес мы сначала загружаем из другой ячейки памяти.