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

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

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


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

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

Чтобы уточнить неправильное представление:

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

int &ri = i;

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

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

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

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

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

48 ответов

  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& безопаснее для использования в списках аргументов и так далее.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вопреки распространенному мнению, возможно иметь ссылку на 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));

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

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

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

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

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

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

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

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

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) уничтожаются в конце выражения. Тем не менее, связывая этот объект со ссылкой, refC++ продлит срок службы этого временного объекта до ref выходит за рамки.

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

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

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

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

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

int a = 0;
int& b = a;

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

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

int a;
increment(a);

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

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

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

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

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

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

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

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

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. 

Прямой ответ

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

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

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

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

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

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

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

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

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

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

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

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

>>> 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,

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

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

Например:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

напечатает:

in scope
scope_test done!

Это языковой механизм, который позволяет ScopeGuard работать.

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

int& j = i;

Это внутренне становится

int* const j = &i;

Ссылка на указатель возможна в 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;

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

С уважением, &rzej

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

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

Что выводит это:

THIS IS A STRING
0xbb2070 : 0xbb2070

Если вы заметили, что даже адреса памяти точно такие же, то есть ссылка успешно указывает на переменную в куче! Теперь, если вы действительно хотите стать причудливым, это также работает:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

Что выводит это:

THIS IS A STRING

Поэтому ссылка - это указатель под капотом, они оба просто хранят адрес памяти, где адрес, на который указывает адрес, не имеет значения, как вы думаете, что произойдет, если я вызову std::cout << str_ref; ПОСЛЕ вызова delete &str_ref? Что ж, очевидно, что он хорошо компилируется, но вызывает ошибку сегментации во время выполнения, потому что он больше не указывает на допустимую переменную, у нас по существу есть неработающая ссылка, которая все еще существует (пока она не выпадает из области видимости), но бесполезна.

Другими словами, ссылка - это не что иное, как указатель, у которого абстрагирована механика указателя, что делает его более безопасным и простым в использовании (без случайной математики указателя, без перемешивания '.' И '->' и т. Д.), Предполагая, что вы не пытайтесь делать глупости, как мои примеры выше;)

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

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

Помните, что приведенные выше примеры - это просто примеры, демонстрирующие, что такое ссылка, вы никогда не захотите использовать ссылку таким образом! Для правильного использования ссылки здесь уже есть множество ответов, которые бьют по голове

Другое отличие состоит в том, что вы можете иметь указатели на тип 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);
}

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

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

Эта программа может помочь в понимании ответа на вопрос. Это простая программа со ссылкой "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

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

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 на временную ссылку.

Ссылка - это константный указатель. int * const a = &b такой же как int& a = b. Вот почему не существует такой вещи, как ссылка на константу, потому что она уже является константой, тогда как ссылка на константуconst int * const a. Когда вы компилируете с использованием -O0, компилятор помещает адрес b в стек в обеих ситуациях, и как член класса он также будет присутствовать в объекте в стеке / куче, как если бы вы объявили константный указатель. С -Ofast это можно оптимизировать бесплатно.

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

#include <iostream>

int main() {
  int a =1;
  int* b = &a;
  std::cout << b ;
}

int main() {
  int a =1;
  int& b = a;
  std::cout << &b ;
}

they both have the same assembly output
-Ofast:
main:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:_ZSt4cout
        lea     rsi, [rsp+12]
        mov     DWORD PTR [rsp+12], 1
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)
        xor     eax, eax
        add     rsp, 24
        ret

-O0:
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-12], 1
        lea     rax, [rbp-12]
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
        mov     eax, 0
        leave
        ret

Как члены объекта они идентичны от -O0 до -Ofast.

#include <iostream>
int b=1;
struct A {int* i=&b; int& j=b;};
A a;
int main() {
  std::cout << &a.j << &a.i;
}

The address of b is stored twice in the object. 

a:
        .quad   b
        .quad   b

        mov     rax, QWORD PTR a[rip+8] //&a.j
        mov     esi, OFFSET FLAT:a //&a.i

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

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

Хотя это может показаться поверхностным, я считаю, что это свойство имеет решающее значение для ряда функций 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++ и стандартной библиотеки, так что это довольно важное свойство ссылок.

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

Ссылки менее мощные, чем указатели

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

2) Ссылки не могут быть NULL. Указатели часто имеют значение NULL, чтобы указать, что они не указывают ни на одну действительную вещь.

3) Ссылка должна быть инициализирована при объявлении. Там нет такого ограничения с указателями

Из-за вышеуказанных ограничений ссылки в C++ не могут использоваться для реализации структур данных, таких как Linked List, Tree и т. Д. В Java ссылки не имеют вышеуказанных ограничений и могут использоваться для реализации всех структур данных. Ссылки, являющиеся более мощными в Java, являются основной причиной, по которой Java не нуждается в указателях.

Ссылки безопаснее и проще в использовании:

1) Безопаснее: поскольку ссылки должны быть инициализированы, дикие ссылки, такие как дикие указатели, вряд ли будут существовать. Все еще возможно иметь ссылки, которые не ссылаются на действительное местоположение

2) Легче в использовании: для доступа к значению для ссылок не требуется оператор разыменования. Их можно использовать как обычные переменные. Оператор '&' необходим только во время объявления. Кроме того, к элементам ссылки на объект можно получить доступ с помощью оператора точки ('.'), В отличие от указателей, где для доступа к членам необходим оператор стрелки (->).

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

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

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++ являются тому примером.

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

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

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

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

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

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

int &ri = i;

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

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

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

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

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

Другие вопросы по тегам