В C++ добавился новый тип, который поначалу ввёл меня в ступор. Это ссылка (reference).
Предыдущая часть:
Я потратил довольно много времени, чтобы понять сущность ссылок, но в итоге оказалось, что я сильно переоценил их, считая чем-то особенным. Ссылки это просто указатели в другом синтаксисе, и на этом можно было закончить.
Но ведь их придумали зачем-то.
Давайте повторим прошлые материалы про указатели. Каждая переменная программы, вроде такой:
foo = 5
обладает следующими свойствами:
- у неё есть имя foo
- у неё есть значение 5
Переменная это хранилище, где хранится значение, а для доступа к хранилищу нужно имя. Но что такое имя? Ничто иное, как адрес. Чтобы получить значение, нужно обратиться по адресу.
Таким образом, если в адрес 1000 записать 5 и назвать этот адрес foo, то мы получим переменную с именем foo, со значением 5 и с адресом 1000.
Если бы язык C был абсолютно свободным и прямолинейным, нам вообще были бы не нужны типы-указатели. С любой переменной можно было бы работать так, как будто она одновременно является указателем.
Например,
foo = 5
это записать по адресу foo значение 5,
bar = &foo
это записать по адресу bar значение адреса foo,
*bar = 5
это записать по адресу, который хранится по адресу bar, значение 5.
Переменная bar в данном случае работает как указатель. Её можно было бы не объявлять как специальный тип-указатель. Достаточно было бы написать *bar.
Но тип-указатель объявлять приходится, поэтому создание переменной bar будет выглядеть так:
int* bar;
Где int* это указатель на адрес с размером данных int. Заметим, что часто тип-указатель пишут вот так:
int *bar;
Разницы нет никакой, но *bar это, как мы ранее видели, содержимое памяти, на которую указывает значение bar. Просто в этом случае запись трактуется как тип-указатель. Синтаксис неоднозначен, и я предлагаю угадать, что получится в этом примере:
int* foo;
int* bar = (int)foo**foo;
Чтобы не было разночтений, я рекомендую использовать именно такое объявление типа:
int* bar;
Ссылки
Ссылка и указатель в C это синонимы. Указатель ссылается на какой-то адрес, а ссылка указывает на какой-то адрес. Это технически полностью одно и то же.
Тип-ссылка объявляется так:
int& bar;
Просто другой символ. Который опять же не надо приписывать к имени переменной вот так:
int &bar;
Потому что &bar это адрес переменной bar и синтаксис опять неоднозначен.
Первое отличие ссылки от указателя в том, что ей обязательно надо присвоить значение:
int& bar = foo;
Здесь ссылке (указателю) bar присваивается адрес переменной foo. Ранее я говорил, что адрес переменной foo это &foo. Но когда мы работаем со ссылками, обычные правила синтаксиса уже не действуют. Когда мы присваиваем значение ссылке, то по умолчанию считается, что это уже адрес, поэтому & писать не надо.
То есть, вот эти две записи эквивалентны:
int* bar = &foo;
int& bar = foo;
И в том и в другом случае мы получаем адрес foo, записанный в переменную bar.
Также эквивалентны эти записи:
int bar = *foo; // если foo – указатель
int bar = foo; // если foo – ссылка
И в том и в другом случае мы получаем значение по ссылке/указателю foo.
Работа со ссылками
Сcылки работают точно так же, как указатели, просто синтаксис у них устроен наоборот. Как мы уже видели раньше, если указателю присваивается адрес переменной foo, то нужно писать &foo, а для ссылки не нужно. Если для доступа к полям структуры, расположенной по указателю, нужно использовать стрелки:
bar->x = 5;
То для ссылки нужно использовать точку:
bar.x = 5;
Вот программа с двумя способами передачи параметра в функцию – по ссылке и по указателю:
Функции area_ref() и area_ptr() абсолютно идентичны друг другу:
То есть разница между указателем и ссылкой исключительно в синтаксисе.
Ссылка это такой способ замаскировать указатель под обычную переменную. То есть не писать & при получении адреса, не писать * при получении значения по указателю, и не ставить стрелочек при работе с полями структуры.
Для чего это надо – сказать затрудняюсь. Два вида указателей с одним и тем же функционалом только лишь вносят путаницу и неоднозначность в трактовке синтаксиса.
Но у ссылки есть дополнительные правила – она должна быть обязательно инициализирована и ей нельзя присвоить NULL. Это может пригодиться для создания более надёжного кода.
Но случаев, когда именно ссылка может сделать что-то, чего не может указатель, я не нашёл. Точнее, эти случаи, предположительно, относятся к перегрузке операторов и каким-то особым конструкторам, и базируются исключительно на синтаксисе, но до этого ещё надо дойти.
Пока же просто – когда вы видите ссылку, это на самом деле обычный указатель.
Поправки из комментариев
Я упустил один момент с присваиванием ссылок, на который мне указали более умные комментаторы :)
Попробуем переприсвоить значения указателя и ссылки:
После того как мы присвоили указателю ptr адрес переменной &a, мы можем присвоить тому же указателю адрес &b. Это выльется в то, что собственное значение ptr будет равно адресу a, а затем изменится на адрес b. При этом значения переменных a и b останутся как и были.
В случае со ссылкой ref вначале всё так же: в ref сохраняется значение адреса a. Но когда мы пытаемся записать в уже инициализированную ref адрес переменной b, это сделать уже невозможно. Потому что такая запись:
ref = b;
С точки зрения указателя равносильна такой:
*ptr = b;
То есть мы не в ref записываем адрес b, а в адрес, находящийся в ref, записываем значение b.
В результате значение ref остаётся равным 1000, и по этому адресу, то есть в переменную a, записывается значение переменной b. То есть теперь a = 6.
Это чисто синтаксическая ловушка. Физически мы ничем не ограничены, но нет таких синтаксических средств, чтобы поменять адрес в ref, который был однажды задан при инициализации. Поменять можно только значение по адресу.