c++ ссылка на - Каковы различия между переменной указателя и ссылочной переменной в C ++?



15 Answers

Что такое C ++-ссылка ( для программистов C )

Ссылка может рассматриваться как постоянный указатель (не путать с указателем на постоянное значение!) С автоматической косвенностью, т. Е. Компилятор будет применять оператор * для вас.

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

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

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

Рассмотрим следующий пример из C ++ FAQ :

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

Но если ссылка действительно была объектом, как могли быть оборванные ссылки? В неуправляемых языках невозможно, чтобы ссылки были «безопаснее», чем указатели, - как правило, это просто не способ надежно присвоить значения через границы границ!

Почему я считаю ссылки на C ++ полезными

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

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

указатель отличие ссылки

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

Но каковы различия?

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

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

Чтобы прояснить заблуждение:

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

int &ri = i;

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

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

Как общее правило,

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

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




Вопреки распространенному мнению, возможно иметь ссылку NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

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

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

Фактическая ошибка заключается в разыменовании указателя NULL до назначения ссылки. Но я не знаю каких-либо компиляторов, которые будут генерировать любые ошибки при этом условии - ошибка распространяется до точки далее в коде. Вот почему эта проблема настолько коварна. В большинстве случаев, если вы разыскиваете указатель NULL, вы ругаетесь прямо в этом месте, и это не требует большой отладки, чтобы понять это.

Мой пример выше короткий и надуманный. Вот более реальный пример.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

Я хочу повторить, что единственный способ получить нулевую ссылку - это неправильный код, и как только вы его получите, вы получите неопределенное поведение. Никогда не имеет смысла проверять нулевую ссылку; например, вы можете попробовать, if(&bar==NULL)... но компилятор может оптимизировать утверждение из существования! Действительная ссылка никогда не может быть NULL, поэтому из представления компилятора сравнение всегда ложно, и можно свободно исключить предложение if как мертвый код - в этом суть неопределенного поведения.

Правильный способ избежать неприятностей - избежать разыменования указателя NULL для создания ссылки. Вот автоматический способ сделать это.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

Для более старого взгляда на эту проблему от кого-то с лучшими навыками письма, см. Null References от Jim Hyslop и Herb Sutter.

Для другого примера опасности разыменования нулевого указателя см. Раздел «Неопределенное поведение при попытке передать код на другую платформу Раймондом Ченом».




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

Обновление: теперь, когда я думаю об этом еще, есть важное различие.

Целью указателя const можно заменить его адрес и использование команды const.

Цель ссылки не может быть заменена каким-либо образом, чем UB.

Это должно позволить компилятору сделать большую оптимизацию по ссылке.




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

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

В качестве примера:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

Оптимизирующий компилятор может понять, что мы получаем доступ к [0] и [1] довольно связке. Было бы желательно оптимизировать алгоритм, чтобы:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

Чтобы сделать такую ​​оптимизацию, необходимо доказать, что во время вызова ничто не может изменить массив [1]. Это довольно легко сделать. i не меньше 2, поэтому array [i] никогда не может ссылаться на массив [1]. maybeModify () присваивается a0 как ссылка (aliasing array [0]). Поскольку нет «ссылочной» арифметики, компилятор просто должен доказать, что возможноModify никогда не получает адрес x, и он доказал, что ничто не изменяет массив [1].

Он также должен доказать, что нет способа, которым будущий вызов мог бы читать / писать [0], в то время как у нас есть временная копия реестра в a0. Это часто тривиально доказывать, потому что во многих случаях очевидно, что эта ссылка никогда не хранится в постоянной структуре, такой как экземпляр класса.

Теперь сделайте то же самое с указателями

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

Поведение такое же; только сейчас гораздо сложнее доказать, что, возможно ,Modify никогда не модифицирует массив [1], потому что мы уже указали ему указатель; кошка вышла из сумки. Теперь он должен сделать гораздо более сложное доказательство: статический анализ mayModify, чтобы доказать, что он никогда не пишет в & x + 1. Он также должен доказать, что он никогда не избавляет от указателя, который может ссылаться на массив [0], который просто как сложно.

Современные компиляторы становятся все лучше и лучше при статическом анализе, но всегда приятно помочь им и использовать ссылки.

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

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

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

Обычно в конце выражения уничтожаются обычно временные объекты, такие как созданный вызовом createF(5) . Однако, связывая этот объект с ссылкой, ref , C ++ продлит срок действия этого временного объекта, пока ref не выйдет за пределы области видимости.




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

Рассмотрим эти два фрагмента программы. В первом мы присваиваем один указатель другому:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

После назначения, ival, объект, адресуемый pi, остается неизменным. Назначение изменяет значение pi, указывая на другой объект. Теперь рассмотрим аналогичную программу, которая присваивает две ссылки:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

Это присваивание изменяет значение ival, значение, на которое ссылается ri, а не сама ссылка. После назначения две ссылки по-прежнему относятся к их исходным объектам, и значение этих объектов теперь тоже самое.




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

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 



Это основано на tutorial . То, что написано, делает его более ясным:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

Просто помните,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

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

Посмотрите на следующее утверждение,

int Tom(0);
int & alias_Tom = Tom;

alias_Tomможет быть понято как alias of a variable(отличное от того typedef, которое есть alias of a type) Tom. Также хорошо забыть терминологию такого утверждения, чтобы создать ссылку Tom.




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

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

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

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

Для получения дополнительной информации о ссылке на указатель обратитесь к следующему:

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

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}



Я использую ссылки, если мне не нужен ни один из них:

  • Нулевые указатели могут использоваться как контрольное значение, часто это дешевый способ избежать перегрузки или использования функции bool.

  • Вы можете сделать арифметику на указателе. Например,p += offset;




Другое отличие состоит в том, что вы можете иметь указатели на тип void (и это означает указатель на что-либо), но ссылки на void запрещены.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

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




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

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

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

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




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

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;
}

По умолчанию используется «привязка константы привязки к временному» аспекту ссылок.




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

  • Ссылка требует указания фактического окна.
  • Указатель требует расположения части экрана на экране, который, как вы уверяете, будет содержать ноль или более экземпляров этого типа окна.



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




Я всегда решаю this правило из Основных принципов C ++:

Предпочитайте T * над T &, когда «no argument» является допустимым вариантом




Related