Есть ли преимущества передачи по указателю перед передачей по ссылке в C ++?

225

Каковы преимущества передачи по указателю перед передачей по ссылке в C ++?

В последнее время я видел ряд примеров, в которых аргументы функции передаются по указателям, а не по ссылке. Есть ли преимущества для этого?

Пример:

func(SPRITE *x);

с вызовом

func(&mySprite);

против

func(SPRITE &x);

с вызовом

func(mySprite);
Мэтт Паско
источник
Не забудьте о newсоздании указателя и возникающих в результате проблем владения.
Мартин Йорк

Ответы:

216

Указатель может получить параметр NULL, а ссылочный параметр - нет. Если есть вероятность, что вы захотите передать «нет объекта», используйте указатель вместо ссылки.

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

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Адам Розенфилд
источник
18
Неполный ответ. Использование указателей не разрешает использование временных / повышенных объектов, а также использование заостренного объекта в качестве стековых объектов. И это предполагает, что аргумент может быть NULL, когда большую часть времени значение NULL должно быть запрещено. Прочитайте ответ Литба для полного ответа.
paercebal
Второй вызов функции раньше был аннотирован func2 passes by reference. Хотя я понимаю, что вы имели в виду, что он проходит «по ссылке» с точки зрения высокого уровня, реализуемого путем передачи указателя с точки зрения уровня кода, это было очень запутанным (см. Stackoverflow.com/questions/13382356/… ).
Гонки легкости на орбите
Я просто не покупаю это. Да, вы передаете указатель, поэтому он должен быть выходным параметром, потому что то, на что указано, не может быть const?
deworde
У нас нет этой проходной ссылки в C? Я использую последнюю версию codeblock (mingw) и выбираю проект на Си. По-прежнему передача по ссылке (func (int & a)) работает. Или это доступно в C99 или C11 и далее?
Джон Уилок
1
@JonWheelock: нет, C вообще не имеет ссылки на ссылку. func(int& a)не является допустимым C в любой версии стандарта. Вы, вероятно, собираете свои файлы как C ++ случайно.
Адам Розенфилд
245

Передача по указателю

  • Звонящий должен взять адрес -> не прозрачный
  • Значение 0 может быть предоставлено, чтобы означать nothing. Это может быть использовано для предоставления необязательных аргументов.

Передать по ссылке

  • Абонент просто передает объект -> прозрачный. Должен использоваться для перегрузки операторов, поскольку перегрузка для типов указателей невозможна (указатели являются встроенными типами). Так что вы не можете string s = &str1 + &str2;использовать указатели.
  • 0 значений невозможно -> Вызываемая функция не должна проверять их
  • Ссылка на const также принимает временные значения: void f(const T& t); ... f(T(a, b, c));указатели не могут использоваться таким образом, поскольку вы не можете получить адрес временного объекта.
  • И последнее, но не менее важное: ссылки проще в использовании -> меньше шансов для ошибок.
Йоханнес Шауб - Литб
источник
7
Передача по указателю также поднимает вопрос "Передано ли право собственности или нет?" вопрос. Это не тот случай со ссылками.
Фрерих Раабе
43
Я не согласен с «меньше шансов для ошибок». При проверке сайта вызова, и читатель видит «foo (& s)», сразу становится ясно, что s можно изменить. Когда вы читаете "foo (s)", совсем не ясно, можно ли изменить s. Это основной источник ошибок. Возможно, у определенного класса ошибок меньше шансов, но в целом передача по ссылке является огромным источником ошибок.
Уильям Перселл
25
Что вы подразумеваете под "прозрачным"?
Gbert90
2
@ Gbert90, если вы видите foo (& a) на сайте вызовов, вы знаете, что foo () принимает тип указателя. Если вы видите foo (a), вы не знаете, нужна ли ему ссылка.
Майкл Дж. Давенпорт
3
@ MichaelJ.Davenport - в своем объяснении вы предлагаете «прозрачный» для обозначения чего-то вроде «очевидно, что вызывающий объект передает указатель, но не очевидно, что вызывающий элемент передает ссылку». В сообщении Йоханнеса он говорит «Передача по указателю - вызывающий должен взять адрес -> не прозрачный» и «Передать по ссылке - вызывающий просто передает объект -> прозрачный» - что почти противоположно тому, что вы говорите , Я думаю, что вопрос Gbert90 «Что вы подразумеваете под« прозрачным »» все еще актуален.
Happy Green Kid Naps
66

Мне нравится рассуждение статьи из "cplusplus.com:"

  1. Передача по значению, когда функция не хочет изменять параметр, а значение легко копировать (целые, двойные, char, bool и т. Д. ... простые типы. Std :: string, std :: vector и все другие STL контейнеры НЕ простые типы.)

  2. Передача по указателю const, когда значение копируется слишком дорого, и функция не хочет изменять значение, указанное в AND. NULL является допустимым ожидаемым значением, которое обрабатывает функция.

  3. Передача через неконстантный указатель, когда значение копируется слишком дорого, и функция хочет изменить значение, указанное на AND. NULL является допустимым ожидаемым значением, которое обрабатывает функция.

  4. Передача по константной ссылке, когда значение копируется дорого, и функция не хочет изменять значение, указанное в AND. NULL не будет допустимым значением, если вместо этого используется указатель.

  5. Передача по неконтрольной ссылке, когда значение дорого копировать, а функция хочет изменить значение, указанное в AND, NULL не будет допустимым значением, если вместо этого используется указатель.

  6. При написании шаблонных функций нет однозначного ответа, потому что есть несколько компромиссов, которые следует рассмотреть, которые выходят за рамки этого обсуждения, но достаточно сказать, что большинство шаблонных функций принимают свои параметры по значению или (const) ссылке Однако, поскольку синтаксис итератора аналогичен синтаксису указателей (от звездочки до «разыменования»), любая шаблонная функция, которая ожидает итераторы в качестве аргументов, также по умолчанию также принимает указатели (и не проверяет NULL, поскольку концепция итератора NULL имеет другой синтаксис ).

http://www.cplusplus.com/articles/z6vU7k9E/

Из этого я понимаю, что основное различие между выбором указателя или ссылочного параметра заключается в том, что NULL является приемлемым значением. Вот и все.

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

Р. Навега
источник
Да, для меня термины, связанные с NULL, являются основными проблемами здесь. Спасибо за цитирование ..
binaryguy
62

«Достаточно веревки, чтобы выстрелить себе в ногу» Аллена Холуба, перечисляет следующие 2 правила:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Он перечисляет несколько причин, по которым ссылки были добавлены в C ++:

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

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

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

Майкл Берр
источник
8
Моя позиция в этом аргументе заключается в том, что если имя функции делает совершенно очевидным, без проверки документов, что параметр будет изменен, тогда неконстантная ссылка будет в порядке. Так что лично я бы разрешил "getDetails (DetailStruct & result)". Указатель там повышает уродливую возможность ввода NULL.
Стив Джессоп
3
Это вводит в заблуждение. Даже если некоторые не любят ссылки, они являются важной частью языка и должны использоваться как таковые. Эта линия рассуждений напоминает высказывание, что не используйте шаблоны, вы всегда можете использовать контейнеры void * для хранения любого типа. Прочитайте ответ от Литб.
Дэвид Родригес - dribeas
4
Я не понимаю, как это вводит в заблуждение - бывают моменты, когда требуются ссылки, и бывают случаи, когда лучшие практики могут предложить не использовать их, даже если вы могли бы. То же самое можно сказать о любой функции языка - наследование, друзья, не являющиеся членами, перегрузка операторов, MI и т. Д.
Майкл Барр
Между прочим, я согласен с тем, что ответ Литба очень хороший и, безусловно, более всеобъемлющий, чем этот - я просто решил сосредоточиться на обсуждении обоснования, позволяющего избежать использования ссылок в качестве выходных параметров.
Майкл Берр
1
Это правило используется в руководстве по стилю google c ++: google-styleguide.googlecode.com/svn/trunk/…
Антон Данейко
9

Пояснения к предыдущим постам:


Ссылки НЕ являются гарантией получения ненулевого указателя. (Хотя мы часто относимся к ним как к таковым.)

В то время как ужасно плохой код, например, выводит вас из-за плохого кода, скомпилирует и запустит следующее: (По крайней мере, под моим компилятором.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

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

Внезапно возникает огромная разница между foo (BAR bar) и foo (BAR & bar) . (Автоматическая побитовая операция копирования вызывается. Удаление в деструкторе вызывается дважды.)

К счастью, современные компиляторы подберут это двойное освобождение одного и того же указателя. 15 лет назад они этого не сделали. (В gcc / g ++ используйте setenv MALLOC_CHECK_ 0, чтобы вернуться к старым способам.) В результате в DEC UNIX в одной и той же памяти выделяется два разных объекта. Там много отладочного веселья ...


Более практично:

  • Ссылки скрывают, что вы меняете данные, хранящиеся где-то еще.
  • Ссылку легко скопировать с скопированным объектом.
  • Указатели делают это очевидным!
Mr.Ree
источник
16
это не проблема функции или ссылок. Вы нарушаете языковые правила. разыменование нулевого указателя само по себе уже неопределенное поведение. «Ссылки НЕ являются гарантией получения ненулевого указателя.»: Сам стандарт говорит, что это так. другие способы представляют собой неопределенное поведение.
Йоханнес Шауб - Lit
1
Я согласен с Литб. Хотя это правда, код, который вы показываете нам, является более саботажем, чем что-либо еще. Существуют способы саботировать что угодно, включая нотации «ссылка» и «указатель».
paercebal
1
Я сказал, что это «вытащить тебя за плохой код»! В том же духе вы также можете иметь i = new FOO; удалить я; тест (* I); Еще один (к сожалению распространенный) висячий указатель / эталонный случай.
Mr.Ree
1
На самом деле проблема не в разыменовании NULL, а в ИСПОЛЬЗОВАНИИ этого разыменованного (нулевого) объекта. Таким образом, на самом деле нет никакой разницы (кроме синтаксиса) между указателями и ссылками с точки зрения реализации языка. Это пользователи, которые имеют разные ожидания.
Mr.Ree
2
Независимо от того, что вы делаете с возвращенной ссылкой, в тот момент, когда вы говорите *i, ваша программа имеет неопределенное поведение. Например, компилятор может увидеть этот код и предположить: «Хорошо, этот код имеет неопределенное поведение во всех путях кода, поэтому вся эта функция должна быть недоступна». Тогда предполагается, что все ветви, которые приводят к этой функции, не заняты. Это регулярно выполняемая оптимизация.
Дэвид Стоун
5

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

  • Вызывающая сторона не знает, указывает ли указатель на один объект или на начало «массива» объектов.

  • Вызывающая сторона не знает, «принадлежит» ли указатель памяти, на которую он указывает. IE, должна ли функция освободить память. ( foo(new int)- Это утечка памяти?).

  • Вызывающая сторона не знает, nullptrможет ли она быть безопасно передана в функцию.

Все эти проблемы решаются по ссылкам:

  • Ссылки всегда относятся к одному объекту.

  • Ссылки никогда не владеют памятью, к которой они относятся, они просто видят память.

  • Ссылки не могут быть нулевыми.

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

  • Нет явного косвенного Это не проблема с необработанным указателем, так как мы должны использовать &оператор, чтобы показать, что мы действительно передаем указатель. Например, int a = 5; foo(a);здесь совершенно не ясно, что a передается по ссылке и может быть изменено.
  • Допустимость пустой. Эта слабость указателей также может быть сильной стороной, когда мы действительно хотим, чтобы наши ссылки были обнуляемыми. Видя, что std::optional<T&>это недопустимо (по уважительным причинам), указатели дают нам такую ​​обнуляемость, которую вы хотите.

Таким образом, кажется, что когда мы хотим пустую ссылку с явным косвенным обращением, мы должны достичь T*права? Неправильно!

Абстракции

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

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

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

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Мы достигли наших целей! Давайте теперь посмотрим на преимущества по сравнению с необработанным указателем.

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

Мы могли бы сделать интерфейс более сложным, например, добавив операторы равенства, монадику get_orи mapинтерфейс, метод, который получает значение или выдает исключение, constexprподдержку. Это может быть сделано вами.

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

Том VH
источник
3

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

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

Брайан
источник
1
Это и преимущество, и недостаток. Многие API используют NULL-указатели для обозначения чего-то полезного (т. Е. NULL timespec ждет вечно, в то время как значение означает «ждать так долго»).
Грег Роджерс
1
@Brian: я не хочу быть придирчивым, но: я бы не сказал, что гарантированно получить экземпляр при получении ссылки. Висячие ссылки все еще возможны, если вызывающая функция отменяет ссылку на висячий указатель, который вызываемый не может знать.
Foraidt
иногда вы можете даже повысить производительность, используя ссылки, так как им не нужно занимать какое-либо хранилище и им не назначаются адреса. косвенное обращение не требуется.
Йоханнес Шауб - Lit
Программы, содержащие висячие ссылки, не являются допустимыми для C ++. Поэтому да, код может предполагать, что все ссылки действительны.
Конрад Рудольф
2
Я могу определенно разыменовать нулевой указатель, и компилятор не сможет сказать ... если компилятор не может сказать, что это "недействительный C ++", действительно ли он недействителен?
rmeador