Каковы различия между переменной-указателем и ссылочной переменной в C ++? - PullRequest
2956 голосов
/ 12 сентября 2008

Я знаю, что ссылки являются синтаксическим сахаром, поэтому код легче читать и писать.

Но в чем различия?


Резюме из ответов и ссылок ниже:

  1. Указатель может быть переназначен любое количество раз, в то время как ссылка не может быть переназначена после привязки.
  2. Указатели не могут указывать нигде (NULL), тогда как ссылка всегда ссылается на объект.
  3. Вы не можете взять адрес ссылки, как вы можете с указателями.
  4. Там нет "ссылочной арифметики" (но вы можете взять адрес объекта, на который указывает ссылка, и сделать арифметику указателя на нем, как в &obj + 5).

Для уточнения заблуждения:

Стандарт C ++ очень осторожен, чтобы не указывать, как компилятор может реализовать ссылки, но каждый компилятор C ++ реализует ссылки как указатели. То есть декларация, такая как:

int &ri = i;

если он не оптимизирован полностью , выделяет тот же объем памяти в качестве указателя и размещает адрес i в это хранилище.

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

Как правило,

  • Используйте ссылки в параметрах функций и возвращаемых типах для предоставления полезных и самодокументируемых интерфейсов.
  • Используйте указатели для реализации алгоритмов и структур данных.

Интересно читать:

Ответы [ 35 ]

11 голосов
/ 17 февраля 2019

Прямой ответ

Что такое ссылка в C ++? Некоторый конкретный экземпляр типа, который не является типом объекта .

Что такое указатель в C ++? Некоторый конкретный экземпляр типа, который является типом объекта .

Начиная с определения типа объекта ISO C ++ :

Тип объекта - это (возможно, cv -квалифицированный) тип, который не является типом функции, не является ссылочным типом и не cv void.

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

Указатели и ссылки упоминаются вместе в контексте составной тип . Это в основном связано с природой синтаксиса объявления, унаследованного от (и расширенного) C, который не имеет ссылок. (Кроме того, начиная с C ++ 11 существует более одного вида деклараторов ссылок, в то время как указатели все еще «единообразны»: & + && против *.) Таким образом, проектируется язык, специфичный для «расширения», с похожими стиль C в этом контексте несколько разумен. (Я все еще буду утверждать, что синтаксис деклараторов тратит впустую синтаксическую выразительность много , расстраивает как пользователей, так и реализации. Таким образом, все они не квалифицируются как встроенные в новом дизайне языка. Это совершенно другая тема о дизайне PL.)

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

Обратите внимание, что в приведенных выше утверждениях упоминаются только "указатели" и "ссылки" как типы. Есть несколько интересных вопросов об их экземплярах (например, переменные). Там также приходит слишком много заблуждений.

Различия категорий верхнего уровня уже могут выявить множество конкретных различий, не связанных напрямую с указателями:

  • Типы объектов могут иметь квалификаторы верхнего уровня cv. Ссылки не могут.
  • Переменная типов объектов занимает память согласно семантике абстрактной машины . Ссылка не обязательно занимает память (подробности см. Ниже в разделе о заблуждениях).
  • ...

Еще несколько специальных правил для ссылок:

  • Составные объявления являются более строгими в отношении ссылок.
  • Ссылки могут свернуть .
    • Специальные правила для && параметров (как «пересылка ссылок»), основанные на свертывании ссылок во время вывода параметров шаблона, позволяют «идеальная пересылка» параметров.
  • Ссылки имеют особые правила при инициализации. Время жизни переменной, объявленной как ссылочный тип, может отличаться от обычных объектов через расширение.
    • Кстати, некоторые другие контексты, такие как инициализация, включающие std::initializer_list, следуют некоторым аналогичным правилам продления срока службы эталона. Это еще одна банка червей.
  • ...

Заблуждения

Синтаксический сахар

Я знаю, что ссылки - это синтаксический сахар, поэтому код легче читать и писать.

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

(Точно так же лямбда-выражения s являются не синтаксическим сахаром любых других функций в C ++, поскольку его нельзя точно смоделировать с помощью "неопределенных" свойств, таких как порядок объявления захваченных переменных , что может быть важно, потому что порядок инициализации таких переменных может быть значительным.)

C ++ имеет только несколько видов синтаксических сахаров в этом строгом смысле. Одним из экземпляров является (унаследованный от C) встроенный (не перегруженный) оператор [], который определен точно с такими же семантическими свойствами конкретных форм комбинирования, что и встроенный оператор унарный * и двоичный +.

Хранение

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

Вышеприведенное утверждение просто неверно. Чтобы избежать таких заблуждений, взгляните на правила ISO C ++:

С [intro.object] / 1 :

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

С [dcl.ref] / 4 :

Не указано, требуется ли ссылка для хранения.

Обратите внимание, что это семантические свойства.

Прагматик

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

Но это не вся история. Я имею в виду, есть больше вещей, чем указатели против ссылок, которые вы должны учитывать.

Если вам не нужно придерживаться такого чрезмерно специфического выбора, в большинстве случаев ответ будет коротким: вам не нужно использовать указатели, поэтому вы не . Указатели, как правило, достаточно плохие, потому что они подразумевают слишком много вещей, которых вы не ожидаете, и они будут полагаться на слишком много неявных предположений, подрывающих удобство сопровождения и (даже) переносимость кода. Излишне полагаться на указатели - это определенно плохой стиль, и его следует избегать в смысле современного C ++. Пересмотреть свою цель, и вы, наконец, обнаружите, что указатель является функцией последних сортов в большинство случаев.

  • Иногда языковые правила явно требуют использования определенных типов. Если вы хотите использовать эти функции, соблюдайте правила.
    • Конструкторам копирования требуются определенные типы cv - & ссылочного типа в качестве 1-го типа параметра. (И обычно он должен быть const квалифицированным.)
    • Для конструкторов перемещения требуются определенные типы cv - && ссылочного типа в качестве первого типа параметра. (И обычно не должно быть определителей.)
    • Для определенных перегрузок операторов требуются ссылочные или не ссылочные типы. Например:
      • Перегружено operator=, поскольку для специальных функций-членов требуются ссылочные типы, аналогичные 1-му параметру конструкторов копирования / перемещения.
      • Постфикс ++ требуется пустышка int.
      • ...
  • Если вы знаете, что передача по значению (т. Е. Использование нереферентных типов) достаточна, используйте ее напрямую, особенно при использовании реализации, поддерживающей обязательное копирование в C ++ 17. ( Предупреждение : Однако до исчерпывающе причина необходимости может быть очень сложной .)
  • Если вы хотите использовать некоторые маркеры с правами собственности, используйте умные указатели, такие как unique_ptr и shared_ptr (или даже сами доморощенные, если вы хотите, чтобы они были непрозрачные ), а не необработанные указатели.
  • Если вы выполняете некоторые итерации по диапазону, используйте итераторы (или некоторые диапазоны, которые еще не предоставлены стандартной библиотекой), а не необработанные указатели, если вы не уверены, что необработанные указатели будут работать лучше (например, для меньших зависимостей заголовка) в очень специфических случаях.
  • Если вы знаете, что передачи по значению достаточно, и вам нужна какая-то явная обнуляемая семантика, используйте обертку, например std::optional, а не необработанные указатели.
  • Если вы знаете, что передача по значению не идеальна по вышеуказанным причинам, и вам не нужна семантика, допускающая обнуляемость, используйте {lvalue, rvalue, forwarding} -references.
  • Даже если вам нужна семантика, такая как традиционный указатель, часто есть что-то более подходящее, например observer_ptr в Library Fundamental TS.

Единственные исключения нельзя обойти на текущем языке:

  • Когда вы реализуете умные указатели выше, вам, возможно, придется иметь дело с необработанными указателями.
  • Для определенных процедур взаимодействия языков требуются указатели, например operator new. (Тем не менее, cv - void* все еще довольно отличается и безопаснее по сравнению с обычными объектными указателями, потому что он исключает неожиданную арифметику указателей, если вы не полагаетесь на какое-то несоответствующее расширение в void*, как в GNU.)
  • Указатели на функции могут быть преобразованы из лямбда-выражений без перехватов, а ссылки на функции - нет. Вы должны использовать указатели функций в неуниверсальном коде для таких случаев, даже если вы намеренно не хотите обнуляемых значений.

Итак, на практике ответ так очевиден: когда сомневаешься, избегай указателей . Вы должны использовать указатели только тогда, когда есть очень явные причины, по которым нет ничего более подходящего. За исключением нескольких исключительных случаев, упомянутых выше, такие варианты почти всегда не являются специфичными только для C ++ (но, скорее всего, для конкретной реализации языка). Такими примерами могут быть:

  • Вы должны использовать API старого стиля (C).
  • Вы должны соответствовать требованиям ABI определенных реализаций C ++.
  • Вы должны взаимодействовать во время выполнения с различными языковыми реализациями (включая различные сборки, языковую среду выполнения и FFI некоторых высокоуровневых клиентских языков) на основе допущений конкретных реализаций.
  • Вы должны повысить эффективность перевода (компиляция и компоновка) в некоторых крайних случаях.
  • В некоторых крайних случаях вы должны избегать раздувания символов.

Предостережения в отношении языковой нейтральности

Если вы пришли к вопросу через какой-то результат поиска Google (не относится к C ++) , это, скорее всего, будет неправильное место.

Ссылки в C ++ довольно «странные», так как по сути они не первоклассные: они будут рассматриваться как объекты или функции, на которые ссылаются , поэтому у них нет шансов поддержать некоторые первые Операции класса, такие как левый операнд оператор доступа к члену независимо от типа упомянутого объекта. Другие языки могут иметь или не иметь аналогичные ограничения на свои ссылки.

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

В целом ссылки на разные языки программирования могут быть общими, но давайте оставим это для некоторых других вопросов в SO.

(Дополнительное примечание: вопрос может быть значимым раньше, чем участвуют любые "C-подобные" языки, например ALGOL 68 против PL / I .)

10 голосов
/ 15 марта 2013

Эта программа может помочь понять ответ на вопрос. Это простая программа со ссылкой «j» и указателем «ptr», указывающим на переменную «x».

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

Запустите программу и посмотрите на вывод, и вы поймете.

Кроме того, уделите 10 минут и посмотрите это видео: https://www.youtube.com/watch?v=rlJrrGV0iOg

10 голосов
/ 12 сентября 2008

Другое интересное использование ссылок - предоставление аргумента по умолчанию определенного пользователем типа:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

Вариант по умолчанию использует аспект ссылок bind const на временную ссылку.

10 голосов
/ 15 октября 2009

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

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

Многие компиляторы при вставке первой версии указателя фактически инициируют запись в память (мы явно принимаем адрес). Однако они оставят ссылку в регистре, который является более оптимальным.

Конечно, для функций, которые не являются встроенными, указатель и ссылка генерируют один и тот же код, и всегда лучше передавать внутренние значения по значению, чем по ссылке, если они не изменены и возвращены функцией.

9 голосов
/ 06 июля 2017

Мне кажется, что есть еще один момент, который здесь не освещался.

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

Хотя это может показаться поверхностным, я считаю, что это свойство имеет решающее значение для ряда функций C ++, например:

  • Шаблоны . Поскольку параметры шаблона имеют тип утка, синтаксические свойства типа - это все, что имеет значение, поэтому часто один и тот же шаблон может использоваться как с T, так и с T&.
    (или std::reference_wrapper<T>, который все еще полагается на неявное приведение до T&)
    Шаблоны, которые охватывают T& и T&&, встречаются еще чаще.

  • Lvalues ​​. Рассмотрим оператор str[0] = 'X'; Без ссылок он будет работать только для c-строк (char* str). Возвращение символа по ссылке позволяет пользовательским классам иметь одинаковые обозначения.

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

  • Перегрузки оператора . С помощью ссылок можно ввести косвенное обращение к вызову оператора, скажем, operator+(const T& a, const T& b), сохранив те же обозначения инфикса. Это также работает для обычных перегруженных функций.

Эти пункты дают значительную часть C ++ и стандартной библиотеки, так что это довольно важное свойство ссылок.

8 голосов
/ 02 ноября 2017

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

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

В C вызов, похожий на fn(x), может быть передан только по значению, поэтому он определенно не может изменить x; чтобы изменить аргумент, вам нужно передать указатель fn(&x). Поэтому, если аргументу не предшествовал &, вы знали, что он не будет изменен. (Обратное, & означает изменение, было неверным, поскольку иногда вам приходилось передавать большие структуры только для чтения указателем const.)

Некоторые утверждают, что это такая полезная функция при чтении кода, что параметры указателя всегда следует использовать для изменяемых параметров, а не для ссылок const, даже если функция никогда не ожидает nullptr. То есть эти люди утверждают, что подписи функций, такие как fn3() выше, не должны быть разрешены. Примером стиля Google C ++ является пример этого.

8 голосов
/ 27 декабря 2014

Может быть, некоторые метафоры помогут; В контексте экранного пространства вашего рабочего стола -

  • Для ссылки требуется указать фактическое окно.
  • Для указателя необходимо указать место на экране, которое, как вы уверены, будет содержать ноль или более экземпляров этого типа окна.
6 голосов
/ 06 января 2017

Разница между указателем и ссылкой

Указатель может быть инициализирован на 0, а ссылка - нет. Фактически, ссылка должна также ссылаться на объект, но указатель может быть нулевым указателем:

int* p = 0;

Но у нас не может быть int& p = 0;, а также int& p=5 ;.

Фактически, чтобы сделать это правильно, мы должны сначала объявить и определить объект, затем мы можем сделать ссылку на этот объект, поэтому правильная реализация предыдущего кода будет такой:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

Другим важным моментом является то, что мы можем сделать объявление указателя без инициализации, однако ничего такого нельзя сделать в случае ссылки, которая всегда должна делать ссылку на переменную или объект. Однако такое использование указателя рискованно, поэтому обычно мы проверяем, действительно ли указатель указывает на что-то или нет. В случае ссылки такая проверка не требуется, поскольку мы уже знаем, что ссылка на объект во время объявления является обязательной.

Другое отличие состоит в том, что указатель может указывать на другой объект, однако ссылка всегда ссылается на один и тот же объект, давайте рассмотрим следующий пример:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

Еще один момент: когда у нас есть шаблон, подобный шаблону STL, такой шаблон класса всегда будет возвращать ссылку, а не указатель, чтобы облегчить чтение или присвоение нового значения с помощью оператора []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="
5 голосов
/ 24 апреля 2016

Разница в том, что переменная-указатель с непостоянными значениями (не путать с указателем на константу) может быть изменена во время выполнения программы, требует использования семантики указателя (&, *), а ссылки установить только при инициализации (поэтому вы можете установить их только в списке инициализатора конструктора, но никак иначе) и использовать семантику доступа к обычным значениям. В основном ссылки были введены для поддержки перегрузки операторов, как я читал в одной очень старой книге. Как кто-то заявил в этой теме - указатель может быть установлен в 0 или любое другое значение, которое вы хотите. 0 (NULL, nullptr) означает, что указатель инициализируется ничем. Ошибка разыменования нулевого указателя. Но на самом деле указатель может содержать значение, которое не указывает на какое-то правильное расположение в памяти. Ссылки, в свою очередь, стараются не позволять пользователю инициализировать ссылку на что-то, на что нельзя ссылаться из-за того, что вы всегда предоставляете ему правильное значение. Хотя существует множество способов инициализировать ссылочную переменную в неправильном месте памяти, лучше не вдаваться в подробности. На уровне машины и указатель, и ссылка работают равномерно - с помощью указателей. Допустим, в основных ссылках приведены синтаксические сахара. Ссылки на значения rvalue отличаются от этого - они, естественно, являются объектами стека / кучи.

2 голосов
/ 12 ноября 2017

У меня есть аналогия для ссылок и указателей, думаю, что ссылки - это другое имя объекта, а указатели - адрес объекта.

// receives an alias of an int, an address of an int and an int value
public void my_function(int& a,int* b,int c){
    int d = 1; // declares an integer named d
    int &e = d; // declares that e is an alias of d
    // using either d or e will yield the same result as d and e name the same object
    int *f = e; // invalid, you are trying to place an object in an address
    // imagine writting your name in an address field 
    int *g = f; // writes an address to an address
    g = &d; // &d means get me the address of the object named d you could also
    // use &e as it is an alias of d and write it on g, which is an address so it's ok
}
...