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

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

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


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

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

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

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

int &ri = i;

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

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

Как правило,

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

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

Ответы [ 35 ]

1536 голосов
/ 12 сентября 2008
  1. Указатель можно переназначить:

    int x = 5;
    int y = 6;
    int *p;
    p =  &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    Ссылка не может и должна быть назначена при инициализации:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. Указатель имеет свой собственный адрес и размер памяти в стеке (4 байта в x86), тогда как ссылка разделяет тот же адрес памяти (с исходной переменной), но также занимает некоторое место в стеке. Поскольку ссылка имеет тот же адрес, что и сама исходная переменная, можно с уверенностью рассматривать ссылку как другое имя для той же переменной. Примечание. То, на что указывает указатель, может быть в стеке или куче. Так же ссылка. Мое утверждение в этом утверждении не в том, что указатель должен указывать на стек. Указатель - это просто переменная, которая содержит адрес памяти. Эта переменная находится в стеке. Поскольку ссылка имеет свое собственное пространство в стеке, а адрес совпадает с переменной, на которую она ссылается. Подробнее о стек против кучи . Это означает, что существует реальный адрес ссылки, которую компилятор вам не скажет.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. Вы можете иметь указатели на указатели на указатели, предлагающие дополнительные уровни косвенности. В то время как ссылки предлагают только один уровень косвенности.

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. Указатель может быть назначен nullptr напрямую, а ссылка - нет. Если вы достаточно стараетесь и знаете, как, вы можете сделать адрес ссылки nullptr. Аналогично, если вы попытаетесь сделать это достаточно усердно, у вас может быть ссылка на указатель, и тогда эта ссылка может содержать nullptr.

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. Указатели могут перебирать массив, вы можете использовать ++ для перехода к следующему элементу, на который указывает указатель, и + 4 для перехода к 5-му элементу. Это не имеет значения, на какой размер объекта указывает указатель.

  6. Указатель необходимо разыменовать с помощью *, чтобы получить доступ к области памяти, на которую он указывает, тогда как ссылка может использоваться напрямую Указатель на класс / структуру использует -> для доступа к его членам, тогда как ссылка использует ..

  7. Указатель - это переменная, которая содержит адрес памяти. Независимо от того, как реализована ссылка, ссылка имеет тот же адрес памяти, что и элемент, на который она ссылается.

  8. Ссылки не могут быть вставлены в массив, тогда как указатели могут быть (упомянуты пользователем @litb)

  9. Const ссылки могут быть связаны с временными. Указатели не могут (не без некоторой косвенности):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.
    

    Это делает const& более безопасным для использования в списках аргументов и т. Д.

343 голосов
/ 28 февраля 2009

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

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

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

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

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

Рассмотрим следующее утверждение из C ++ FAQ :

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

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

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

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

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

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

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

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

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

Если вы попробуете это без const, оно не сможет скомпилироваться. Вы не можете привязать неконстантную ссылку к временному объекту и не можете использовать его адрес в этом отношении.

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

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

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

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

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

Фактическая ошибка заключается в разыменовании указателя 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 как мертвый код - это сущность неопределенного поведения.

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

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

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

Еще один пример опасности разыменования нулевого указателя см. Раскрытие неопределенного поведения при попытке переноса кода на другую платформу Раймондом Ченом.

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

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

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

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

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

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

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

Вы забыли самую важную часть:

членский доступ с указателями использует ->
членский доступ со ссылками использует .

foo.bar явно превосходит foo->bar точно так же, как vi явно превосходит Emacs : )

63 голосов
/ 01 сентября 2013

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

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

Как пример:

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, поэтому массив [i] никогда не может ссылаться на массив [1]. MaybeModify () получает a0 в качестве ссылки (псевдоним массива [0]). Поскольку здесь нет «ссылочной» арифметики, компилятору нужно только доказать, что MaybeModify никогда не получает адрес 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];
    }
}

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

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

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

РЕДАКТИРОВАТЬ: Через пять лет после публикации этого ответа я обнаружил реальную техническую разницу, когда ссылки отличаются от просто другого взгляда на одну и ту же концепцию адресации. Ссылки могут изменить продолжительность жизни временных объектов так, как указатели не могут.

F createF(int argument);

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

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

63 голосов
/ 19 сентября 2008

На самом деле ссылка не очень похожа на указатель.

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

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

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

Теперь немного пояснений к реальному коду:

int a = 0;
int& b = a;

Здесь вы не создаете другую переменную, которая указывает на a; вы просто добавляете другое имя к содержимому памяти, содержащему значение a. Эта память теперь имеет два имени, a и b, и к ней можно обращаться, используя любое имя.

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

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

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

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

Ссылка никогда не может быть NULL.

33 голосов
/ 20 мая 2011

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

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

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

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

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

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

...