Ссылка (C++)

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

В языке программирования C++ ссылка (англ. reference) — это простой ссылочный тип, менее мощный, но более безопасный, чем указатель.

Введение[править | править вики-текст]

Если мы объявили переменную без спецификатора extern, то для хранения значений выделяется память. Чтобы изменить или прочитать значение переменной (то есть значение, находящееся в этой области памяти), мы обращаемся по имени этой переменной. В языке C имя сущности (переменной, типа, функции и т.д.) — это идентификатор. С точки зрения программиста, объявляя ссылку (или же указывая, что она будет возвращаемым значением или аргументом функции), мы задаём альтернативный идентификатор для уже созданного объекта. В языке C ссылок нет. С точки зрения реализации, ссылка — это, по сути, указатель, который жёстко привязан к области памяти, на которую он указывает, и который автоматически разыменовывается, когда мы обращаемся по имени ссылки (это легко проверить, дизассемблируя простой пример). Однако это совсем не обязательно так, например, если создать локальную ссылку на другую локальную переменную, указатель не возникает, а все обращения к ссылке компилируются как обращения к первоначальной переменной.

Например:

        /* конкретные адреса переменных могут быть другими */
	int a;       //переменная с именем "a" типа int размещена по адресу 0xbfd86d6c
	int &ra = a; //задано альтернативное имя (ra) для переменной по адресу 0xbfd86d6c
        
        /* символ "&", используемый для уже созданного объекта, является операцией взятия адреса *
         * (и эта операция не есть ссылка), то есть &a тут означает получить адрес переменной,   *
         * к которому привязано имя "a"                                                          */
	cout << &a << '\n' << &ra << '\n';

В stdout будет записано:

0xbfd86d6c
0xbfd86d6c

То есть оба имени "a" и "ra" привязаны к одному и тому же адресу.

Ссылки нельзя объявлять без привязки к переменной (то есть не инициализировав при объявлении). После объявления ссылки её невозможно привязать к другой переменной.

Важно отличать ссылки от оператора взятия адреса & (address of). Оператор взятия адреса используется для уже созданного объекта с целью получить его адрес (то есть адрес области памяти, где хранятся значения), а ссылка это только задание альтернативного имени объекта (с точки зрения программиста, а не реализации). Например:

        int a;       //переменная типа int размещена по адресу 0xbfd86d6c с именем "a"
        int b = 3;

        /* создан указатель с именем "p" по адресу 0xbf971c4c, значение этого указателя *
         * адрес объекта с именем "a" - 0xbfd86d6c (это значение можно будет менять)    */
	int *p = &a;

        p = &b;      //присваиваем указателю новое значение, соответствующее адресу переменной "b"

Отличие указателя от ссылки в том, что получить само значение переменной, на которую указывает указатель, можно только выполнив операцию разыменовывания * (символ "*" в объявлении является объявлением указателя, а при применении к уже созданной переменной является оператором разыменовывания). Например:

        int a = 3;
	int *p = &a; //объявили, создали и инициализировали объект
        
        // здесь к уже созданному объекту с именем "p" применяется оператор "*", который означает
        // считать значение из "p", которое является адресом, и далее считать данные по этому адресу
	cout << *p << '\n';

В stdout будет записано:

3

Синтаксис и терминология[править | править вики-текст]

Объявление вида:

<Type> & <Name>

где <Type>тип и <Name>идентификатор, указывает идентификатор, чьим типом является ссылка на <Type>.

Примеры:

  1. int A = 5;
  2. int& rA = A;
  3. extern int& rB;
  4. int& foo ();
  5. void bar (int& rP);
  6. class MyClass { int& m_b; /* ... */ };
  7. int funcX() { return 42 ; }; int (&xFunc)() = funcX;

Здесь, rA и rB являются типами «ссылок на int», foo() — функция, возвращающая ссылку на int, bar() — функция с ссылкой в качестве параметра, которая ссылается на int, MyClass — класс (class) с членом, ссылающимся на int, funcX() — функция, возвращающая int, xFunc() — псевдоним для funcX.

Типы, относящиеся к «ссылка на <Type>», иногда называются ссылочными типами. Идентификаторы ссылочного типа называются ссылочными переменными. Называть их переменными в строгом смысле будет неправильно (показано дальше).

В языке не существует значений ссылочных типов, ни лево-, ни правосторонних. Из не-существования левосторонних значений ссылочных типов следует, что не бывает массивов ссылок и указателей на ссылки.

Ссылка обязана быть инициализирована (если она не extern. Также отметим, что передача параметров в вызов и возврат значения из вызова понимаются как инициализация). Инициализатором ссылки T& может быть только левостороннее значение типа T или (недавнее нововведение) типа, унаследованного от T. Однако для константных ссылок сделано исключение: инициализатором может быть и правостороннее значение T (при необходимости создается невидимая переменная, значение укладывается в нее, ссылка ссылается на нее), и всегда с начала существования языка допускались типы, унаследованные от T.

В стандарте C++11 также добавлена возможность указания атрибутов ссылки <Type> &[[<Attr>]] <Name>[1].

Связь с указателями[править | править вики-текст]

C++ ссылки отличаются от указателей несколькими особенностями:

  • Невозможно ссылаться напрямую на объект ссылочного типа после его определения; каждое упоминание его имени напрямую представляет объект, на который он ссылается. Это происходит потому, что ссылочные типа являются "не совсем полноценными" в системе типов C++ - не существует значений ссылочных типов, причем ни лево-, ни правосторонних значений. Не-существование левосторонних значений ссылочных типов Страуструп и Эллис формулировали как "ссылка не есть объект".
  • В качестве результата первого указания не могут быть выполнены никакие арифметические вычисления, приведения типов, взятия адреса и т.п. Дело в том, что результаты всех этих операций есть правосторонние значения (в случае классовых типов - временные объекты), а не-константная ссылка не может быть инициализирована правосторонним значением (константная - может, при необходимости создается невидимая переменная, значение укладывается туда, ссылка указывает на невидимую переменную).
  • После создания ссылки её нельзя перевести на другой объект; в таких случаях говорят, что ссылка не может быть переопределена. Это часто делают с указателями.
  • Ссылки не могут быть null (т.е. указывать в никуда), тогда как указатели - могут; каждая ссылка ссылается на некий объект, вне зависимости от его корректности.
  • Ссылки не могут быть неинициализированными. Так как невозможно переинициализировать ссылку, она должна быть инициализирована сразу после создания. В частности, локальные и глобальные переменные должны быть проинициализированы там же, где они определены, а ссылки, которые являются данными-членами сущностей класса, должны быть инициализированы в списке инициализатора конструктора класса. Важное замечание: класс, в котором есть поля-ссылки (а также поля const), не может иметь operator=(), и некоторые компиляторы (Microsoft) дают об этом предупреждение.

Пример:

        int& k; // компилятор выдаст сообщение: ошибка: 'k' declared as reference but not initialized 
                // ('k' объявлена как ссылка, но не инициализирована)

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

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

Кроме того, из-за ограничения операций над ссылками, они намного легче в понимании, чем указатели, а также более защищены от ошибок. Тогда как указатели могут стать некорректными благодаря множеству причин, начиная с указания на null-значения и выходов за границы и до использования недопустимых приведений типов, ссылка может стать некорректной лишь в двух случаях:

  • Если она ссылается на объект с автоматическим размещением в памяти, с завершившимся временем жизни.
  • Если она ссылается на объект, находящийся в блоке динамической памяти, который был освобождён.

Первый вариант легко обнаруживается автоматически, если ссылка имеет статическое размещение, но возникают проблемы, если ссылка - член динамически размещённого объекта; от второго защищаться сложнее. Это единственный недостаток ссылок, который может быть нивелирован при разумной политике выделения памяти (далеко не единственный. Например, даже в случае out параметра функции зачастую лучше иметь T*, а не T&, ибо это приводит к необходимости иметь & во всех вызовах функции, который подсказывает человеку, читающему код, что данный параметр есть out. Вообще, ссылки не есть замена указателям).

Применение ссылок[править | править вики-текст]

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

void square(int x, int& result) {
      result = x * x;
  }

Тогда следующий вызов поместит 9 в y:

square(3, y);

Тем не менее, следующий вызов приведёт к ошибке компиляции, так как только ссылочные параметры с квалификатором const могут быть связаны с правосторонними значениями, например, с числовой константой:

square(3, 6);

Возврат ссылки из функции означает, что вызов данной функции есть левостороннее значение. Появляется возможность присваивания вызову функции:

int& preinc(int& x) { ++x; return x; }
preinc(y) = 5; // то же, что и ++y, y = 5

Во многих реализациях обычные механизмы передачи параметров часто подразумевают весьма затратную операцию копирования больших параметров. Ссылки, помеченные const, полезны в качестве способа передачи больших объектов между функциями без накладных расходов:

void f_slow(BigObject x) { /* ... */ }  
void f_fast(const BigObject& x) { /* ... */ }
BigObject y;
f_slow(y); // медленно, копирует y в параметр x
f_fast(y); // быстро, даёт прямой доступ к y (только для чтения)
_

Если f_fast() действительно требует собственной копии x для модификации, то она должна создать копию явным образом. Хотя того же эффекта можно достичь с использованием указателей, это потребует модификации каждого вызова функции, добавив к аргументу неуклюжий оператор разыменования (*), причём отменить изменения будет не менее сложно, если в дальнейшем объект станет маленьким.

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

class BigClass
{
//...
  friend BigClass operator-(BigClass* left, BigClass* right);
//...
};

BigClass x, y, z;
//...
x = &y - &z;

Однако, выражение &y - &z уже имело определённый смысл в языке С.

При перегрузке оператора присваивания разумно будет возвращать именно ссылку на объект. Это увеличивает производительность, особенно в случаях использования нескольких операторов присваивания подряд (например, A=B=C), так как не происходит вызов конструктора копирования.

Полиморфизм ссылок[править | править вики-текст]

Ссылки могут иметь тип, отличный от типа объекта. Главное, чтобы тип ссылки входил в иерархию типов объекта:

#include <iostream>

class A
{
    public:
	virtual void print() { std::cout << "This is class A\n"; }
};

class B: public A
{
    public:
	virtual void print() { std::cout << "This is class B\n"; }
};

int main()
{
	A a;
	A& refToa = a;

	B b;
	A& refTob = b;

	refToa.print();
	refTob.print();

	return 0;
}

Данная программа выведет в консоль следующее:

This is class A
This is class B

Ссылки на временные объекты и универсальные ссылки[править | править вики-текст]

В одиннадцатом стандарте C++ была добавлена поддержка ссылок на временные объекты (англ. rvalue reference)[1].

По стандарту C++ временный объект, появившийся в результате вычисления выражения, можно передавать в функции, но только по константной ссылке (const &). Функция не в состоянии определить, можно ли рассматривать переданный объект как временный и допускающий модификацию (константный объект, который тоже может быть передан по такой ссылке, нельзя модифицировать (легально)). Это не проблема для простейших структур наподобие complex, но для сложных типов, требующих выделения-освобождения памяти, уничтожение временного объекта и создание постоянного может отнимать много времени, в то время как можно было бы просто напрямую передать указатели.

rvalue-ссылка имеет следующий синтаксис: type && name. Новые правила разрешения перегрузки позволяют использовать разные перегруженные функции для неконстантных временных объектов, обозначаемых посредством rvalues, и для всех остальных объектов. Данное нововведение позволяет реализовывать семантику переноса (Move semantics). Ссылка на временные объекты используется в том числе и для конструктора перемещением и оператора присваивания перемещением, которые входят в правило пяти.

rvalue-ссылка T&& может быть инициализирована правосторонним значением типа T (для классовых типов понятия "правостороннее значение" и "временный объект" совпадают). Попытка проинициализировать ее левосторонним значением может вызвать предупреждение компилятора, особенно есть оное значение есть непосредственно имя переменной.

Причиной изобретения T&& было решение следующей проблемы: если есть одноименные функции f(T) и f(T&), то перегрузка по ним разрешится только в том случае, если фактический параметр есть rvalue T - в этом случае версия со ссылкой исключится из рассмотрения, и останется только f(T). Однако же, если фактический параметр есть lvalue T, вызов окажется неоднозначным и вызовет ошибку компиляции (цена неявного приведения lvalue T в T& равна нулю, т.е. оно не хуже точного совпадения, потому f(T) не закроет f(T&)).

Хотелось бы получить возможность перегрузки по одноименным функциям в зависимости от того, является ли один из фактических параметров вызова лево-, или же правосторонним значением того же типа T. T&& решает именно эту проблему: приведение rvalue T в T&& имеет нулевую цену, а приведение lvalue T в T&& - ненулевую. Таким образом, пара функций f(T&) и f(T&&) правильно разрешается - для lvalue T зовется первая, для rvalue T - вторая.

Поскольку правостороннее значение классового типа есть временный объект, оно будет неизбежно разрушено совсем скоро за исполнением выражения. Потому имеет смысл разрешить "вытягивание" содержимого из такого объекта (с правильным занулением, чтобы деструктор не вызвал краха) и перекладку оного содержимого в другой объект (после разрушения содержимого другого объекта, если оно есть). Именно это и должен делать конструктор перемещения C::C(C&&) и оператор перемещения operator=(C&&).

В отличие от конструктора копирования конструктор перемещения не выделяет памяти (которая может оказаться огромной при передаче по значению большого контейнера), выполняется намного быстрее и имеет nofail guarantee (может быть объявлен как throw()). Кроме того, для "правосторонних" выражений вроде string S = strA + strB будет автоматически вызываться перемещение (суммы в S), а для "левосторонних" вроде string S = strArray[i] - копирование (что и правильно, ибо такой оператор не имеет права менять элемент массива).

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

Также существует интересная функция T&& std::move(T&), которая не делает ничего, кроме ре-интерпретации ссылки из параметра как rvalue-ссылки. Это приводит к тому, что выражение string S = std::move(strArray[i]) "переложит" строку из массива в S, оставив в массиве пустую строку (зато быстро и не возбуждая исключений).

Второй серьезной причиной использовать rvalue-ссылки является "совершенная передача" (perfect forwarding) параметров из одной шаблонной функции в другую, с абсолютно точным сохранением типов. До введения rvalue-ccылок задача была неразрешима.

Также существуют универсальные ссылки.[2] Они имеют синтаксис схожий с правыми ссылками: T && name и становятся либо левыми, либо правыми в зависимости от выражения справа. Явное указание типа также должно отсутствовать, это должен быть либо выводимый тип шаблона, либо auto. Так auto && name = expr является универсальной ссылкой и в случае если expr представлен в коде как const int expr;, то ссылка name будет иметь тип const int &. Пример:

template <typename T>
struct Container {
  typedef T* Iterator;
  void add(T&& x); // x не является универсальной ссылкой, так как T&& будет выведен при создании шаблона
  T arr[4];
};

template <typename T>
void someFunc(T&& y) {
	 // y является универсальной ссылкой
}

template <typename T>
void someOtherFunc(T v, typename Container<T>::Iterator&& z) {
	// z не универсальная ссылка, так как тип Iterator будет известен при выведении
}

Цитаты[править | править вики-текст]

Ссылки определены стандартом ISO C++ следующим образом (исключая раздел примеров):

В объявлении T D, где D имеет вид
& D1

а типом идентификатора в объявлении T D1 является производный тип ("derived-declarator-type-list) T,” тогда типом идентификатора D будет производная (“derived-declarator-type-list) ссылка на T.” Cv-ссылки являются плохо согласованными, исключая ситуацию, когда cv-квалификаторы (от англ. const ("константный") и volatile ("временный")) представлены через использование typedef (7.1.3) или шаблон аргумента типа (14.3), в случае чего игнорируются cv-квалификаторы. [Пример: в коде

typedef int& A;
const A aref = 3; // плохо согласовано;
// неконстантная ссылка инициализируется rvalue

тип aref является “ссылкой на int”, а не “const ссылается на int”. ] [Примечание: ссылка может восприниматься как имя объекта. ] Объявление, указывающее тип “ссылается на cv void”, некорректно.

Это не определяет, требуется ли ссылке выделение памяти (3.7).

Не должно быть ссылок на ссылки, массивов ссылок, а также указателей на ссылки. Объявление ссылки должно содержать инициализатор (8.5.3), за исключением случая, когда объявление содержит явный указатель extern (7.1.1) - объявление члена класса (9.2) внутри объявления класса, объявление параметра или возврат типа (8.3.5); смотри 3.1. Ссылка должна при инициализации ссылаться на корректный объект или функцию. [Примечание: точнее говоря, ссылка на null не может существовать в корректно написанной программе, так как единственным способом создать подобную ссылку является связывание её с "объектом", полученным при помощи разыменования нуль-указателя, что вызывает неопределённое поведение. Как описано в 9.6, ссылка не может ограничиваться напрямую битовым полем. ] | cite = ISO/IEC 14882:1998(E), стандарт ISO C++, раздел 8.3.2 [dcl.ref]

См. также[править | править вики-текст]

Ссылки в программировании

Дополнительные источники[править | править вики-текст]

Примечания[править | править вики-текст]

  1. 1 2 Reference declaration - cppreference.com. en.cppreference.com. Проверено 26 января 2016.
  2. Scott Meyers. Universal References in C++11.