Ссылки // FAQ C++

Добавлено 24 сентября 2020 в 21:53

Что такое ссылка?

Псевдоним (альтернативное имя) для объекта.

Ссылки часто используются для передачи по ссылке:

void swap(int& i, int& j)
{
  int tmp = i;
  i = j;
  j = tmp;
}

int main()
{
  int x, y;
  // ...
  swap(x,y);
  // ...
}

Здесь i и j – псевдонимы соответственно для x и y из main. Другими словами, i – это x, не указатель на x и не копия x, а сам x. Всё, что вы делаете с i, происходит с x, и наоборот. Это включает в себя взятие адреса. Значения &i и &x идентичны.

Вот как программист должен думать о ссылках. Рискую сбить вас с толку, представив вам другую точку зрения, как реализуются ссылки. В конечном итоге ссылка i на объект x обычно является машинным адресом объекта x. Но когда программист говорит i++, компилятор генерирует код, увеличивающий x. То есть биты адреса, которые компилятор использует для нахождения x, не изменяются. Программист на C будет думать об этом так, как если бы вы использовали указатель передачи в стиле C, с синтаксическим вариантом (1) перемещения & от вызывающего объекта к вызываемому и (2) удаления *s. Другими словами, программист на C будет думать о i как о макросе для (*p), где p – указатель на x (например, компилятор автоматически разыменовывает базовый указатель; i++ меняется на (*p)++; i = 7 автоматически заменяется на (*p) = 7.

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


Что произойдет, если вы выполните присваивание ссылке?

Вы измените состояние референта (референт - это объект, на который ссылается ссылка).

Помните: ссылка – это референт, поэтому изменение ссылки изменяет состояние референта. На жаргоне разработчика компилятора ссылка – это "lvalue" (то, что может появиться слева от оператора присваивания).


Что будет, если вы вернете ссылку?

Вызов функции может появляться слева от оператора присваивания.

Поначалу эта способность может показаться странной. Например, никто не считает, что выражение f() = 7 имеет смысл. Тем не менее, если a является объектом класса Array, большинство людей думает, что a[i] = 7 имеет смысл, даже если a[i] – это на самом деле просто замаскированный вызов функции (он вызывает метод Array::operator[](int), который является оператором индекса для класса Array).

class Array {
public:
  int size() const;
  float& operator[] (int index);
  // ...
};

int main()
{
  Array a;
  for (int i = 0; i < a.size(); ++i)
    a[i] = 7;    // Эта строка вызывает Array::operator[](int)
  // ...
}

Что означает object.method1().method2()?

Данная строка связывает вызовы этих методов, поэтому это называется цепочкой вызовов методов.

Первое, что выполняется, – это object.method1(). Он возвращает некий объект, который может быть ссылкой на объект (т.е. method1() может заканчиваться с return *this;), или это может быть какой-то другой объект. Назовем возвращенный объект objectB. Затем objectB становится объектом this для method2().

Чаще всего цепочки методов используются в библиотеке iostream. Например, cout << x << y работает, потому что cout << x – это функция, возвращающая cout.

Менее распространенное, но всё же довольно изящное использование цепочки методов - это идиома именованного параметра.


Как можно изменить ссылку так, чтобы она ссылалась на другой объект?

Никак.

Ссылку нельзя отделить от референта.

В отличие от указателя, ссылка, привязанная к объекту, не может быть «переустановлена» на другой объект. Ссылка не является отдельным объектом. У нее нет идентичности. Взяв адрес ссылки, вы получите адрес референта. Помните: ссылка – это ее референт.

В этом смысле ссылка похожа на константный указатель, такой как int* const p (в отличие от указателя на константу, такого как const int* p). Но, пожалуйста, не путайте ссылки с указателями; они очень разные с точки зрения программиста.


Почему в C++ есть и указатели, и ссылки?

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

void f1(const complex* x, const complex* y) // без ссылок
{
  complex z = *x+*y;  // некрасиво
  // ...
}

void f2(const complex& x, const complex& y) // со ссылками
{
  complex z = x+y;    // уже лучше
  // ...
}   

В более общем плане, если вы хотите иметь как функциональность указателей, так и функциональность ссылок, вам нужны либо два разных типа (как в C++), либо два разных набора операций с одним типом. Например, с одним типом вам нужны как операция для присваивания объекту, на который ссылаетесь, так и операция для присваивания ссылке/указателю. Это можно сделать с помощью отдельных операторов (как в Simula). Например:

Ref<My_type> r :- new My_type;
r := 7;            // присваиваем объекту
r :- new My_type;  // присваиваем ссылке

В качестве альтернативы вы можете полагаться на проверку (перегрузку) типа. Например:

Ref<My_type> r = new My_type;
r = 7;            // присваиваем объекту
r = new My_type;  // присваиваем ссылке

Когда мне следует использовать ссылки, а когда – указатели?

Используйте ссылки, когда можете, и указатели, когда вам нужно.

Ссылки обычно предпочтительнее указателей, если вам не нужна «переустановка». Обычно это означает, что ссылки наиболее полезны в общедоступном интерфейсе класса. Ссылки обычно появляются на поверхности объекта, а указатели – внутри.

Исключением из вышеизложенного является ситуация, когда параметру функции или возвращаемому значению требуется «контрольная» ссылка – ссылка, которая не ссылается на объект. Обычно это лучше всего сделать, возвращая/принимая указатель и придавая этот особый смысл значению nullptr (ссылки всегда должны быть псевдонимами объектов, а не разыменованного нулевого указателя).

Примечание. Старомодные C-программисты иногда не любят ссылки, поскольку они предоставляют семантику ссылок, которая не является явной в коде вызывающего. Однако после некоторого опыта работы с C++ можно быстро понять, что ссылки – это форма сокрытия информации, которая скорее предоставляет удобства, чем является помехой. То есть программисты должны писать код на языке задачи, а не на языке машины.


Что означает, что ссылка должна ссылаться на объект, а не на разыменованный нулевой указатель?

Это значит, что следующий код нелегален:

T* p = nullptr;
T& r = *p;  // нелегально

ПРИМЕЧАНИЕ: Пожалуйста, не пишите нам, говоря, что приведенный код работает с вашей конкретной версией вашего конкретного компилятора. Это всё еще нелегально. Язык C++, как он определен стандартом C++, говорит, что это незаконно. Стандарт C++ не требует диагностики для этой конкретной ошибки, что означает, что ваш конкретный компилятор не обязан замечать, что p имеет значение nullptr, и выдавать сообщение об ошибке, но это всё равно незаконно. Язык C++ также не требует, чтобы компилятор генерировал код, который взорвался бы во время выполнения. Фактически, ваша конкретная версия вашего конкретного компилятора может генерировать, а может и не генерировать код, который приведен выше, и который, по вашему мнению, имеет смысл. Но суть в следующем: поскольку от компилятора не требуется генерирование разумного кода, вы не знаете, что компилятор сделает. Поэтому, пожалуйста, не пишите, что ваш конкретный компилятор генерирует хороший код; нам все равно. Это всё еще незаконно. Дополнительные сведения смотрите в стандарте C++, например, C++ 2014 в разделе 8.3.2 [dcl.ref] p5.

В качестве примера, а не в качестве ограничения, некоторые компиляторы оптимизируют тесты nullptr, поскольку они «знают», что все ссылки относятся к реальным объектам, и что ссылки никогда (легально) не являются разыменованными nullptr. Это может заставить компилятор оптимизировать следующий тест:

// ...код выше...
T* p2 = &r;
if (p2 == nullptr) {
  // ...
}

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

Пациент: «Доктор, доктор, у меня болит глаз, когда тыкаю его ложкой».

Врач: "Тогда не тыкайте".


Что такое дескриптор (handle) объекта? Это указатель? Это ссылка? Это указатель на указатель? Что это?

Термин дескриптор (handle) используется для обозначения любого метода, который позволяет вам перейти к другому объекту, - обобщенного псевдоуказателя. Термин (намеренно) двусмысленный и расплывчатый.

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

Итак, если ваша конечная цель – позволить куску кода однозначно идентифицировать/найти конкретный объект некоего класса Fred, вам необходимо в этот кусок кода передать дескриптор Fred. Дескриптор может быть строкой, которая может использоваться в качестве ключа в какой-нибудь известной таблице поиска (например, ключ в std::map<std::string,Fred> или std::map<std::string,Fred*>), или это может быть целое число, которое будет индексом в некотором известном массиве (например, Fred* array = new Fred[maxNumFreds]), или это может быть просто Fred*, или это может быть что-то другое.

Новички часто вспоминают об указателях, но на самом деле использование сырых указателей чревато рисками. Например, что, если объект Fred нужно переместить? Как узнать, безопасно ли удалять объекты Fred? Что, если объект Fred необходимо (временно) сериализовать на диске? И т.д. и т.д. В большинстве случаев для управления подобными ситуациями мы добавляем дополнительные уровни косвенного обращения. Например, дескрипторы могут быть Fred**, где указывающие на Fred* указатели гарантированно никогда не перемещаются; но когда объекты Fred необходимо переместить, вы просто обновляете указывающие на Fred* указатели. Или вы делаете дескриптор целым числом, а затем ищете объекты Fred (или указатели на объекты Fred) в таблице / массиве / где угодно.

Дело в том, что мы используем слово «дескриптор» (Handle), когда еще не знаем деталей того, что мы собираемся делать.

В другой ситуации мы используем слово «дескриптор», когда хотим говорить размыто о том, что уже сделали (иногда для этого также используется термин «волшебное печенье» (magic cookie), например, «Программа передает волшебное печенье (куки), которое используется для уникального определения и поиска соответствующего объекат Fred»). Причина, по которой мы (иногда) хотим говорить размыто о том, что уже сделали, состоит в том, чтобы минимизировать волновой эффект, если/когда конкретные детали/представление дескриптора изменятся. Например, если/когда кто-то изменит дескриптор со строки, которая используется в таблице поиска, на целое число, которое ищется в массиве, мы не хотим идти и обновлять миллион строк кода.

Чтобы еще больше упростить поддержку, если/когда детали/представление дескриптора изменятся (или в целом для облегчения чтения/записи кода), мы часто инкапсулируем дескриптор в класс. Этот класс часто перегружает операторы operator-> и operator* (поскольку дескриптор действует как указатель, то он может и выглядеть как указатель).


Что следует использовать, вызов по значению или вызов по ссылке?

(Примечание: этот ответ необходимо обновить для C++ 11.)

Это зависит от того, чего вы пытаетесь достичь:

  • если вы хотите изменить переданный объект, вызовите его по ссылке или используйте указатель; например, void f(X&); или void f(X*);
  • если вы не хотите изменять переданный объект, и он большой, вызовите по константной ссылке; например, void f(const X&);
  • в противном случае вызовите по значению; например void f(X);

Что значит «большой»? Всё, что больше пары слов.

Зачем вам изменять аргумент? Что ж, нам часто приходится делать подобное, но часто у нас есть и альтернатива: создать новое значение. Рассмотрим:

void incr1(int& x); // инкремент
int incr2(int x);   // инкремент

int v = 2;
incr1(v);       // v становится 3
v = incr2(v);   // v становится 4

Читающему, вероятно, легче понять incr2(). То есть incr1() с большей вероятностью приведет к ошибкам. Поэтому, если создание и копирование нового значения не являются дорогостоящими, мы должны предпочесть стиль, который возвращает новое значение, а не стиль, который изменяет существующее значение.

Если вы действительно хотите изменить аргумент, что в этом случае следует использовать, указатель или ссылку? Если возможна передача «не объекта» (например, нулевого указателя), имеет смысл использование указателя. Предпочтительный стиль, когда вы хотите изменить объект, – использовать указатель потому, что в некоторых контекстах это позволяет проще дать знать, что модификация возможна.

Также обратите внимание, что вызов функции-члена по сути является вызовом по ссылке на объект, поэтому мы часто используем функции-члены, когда хотим изменить значение/состояние объекта.


Почему this – это не ссылка?

Потому что this был введен в C++ (на самом деле в C с классами) до добавления ссылок. Кроме того, Страуструп выбрал this, чтобы следовать примеру Simula, а не (позднее) использованию self в Smalltalk.

Теги

C++ / CppFAQВысокоуровневые языки программированияСсылка / Reference (программирование)Указатель / Pointer (программирование)Языки программирования

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

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