Czy są zalety przejścia przez wskaźnik nad przejściem przez referencję w C ++?


225

Jakie są zalety przejścia przez wskaźnik nad przejściem przez referencję w C ++?

Ostatnio widziałem wiele przykładów, które wybrały przekazywanie argumentów funkcji przez wskaźniki zamiast przekazywania przez odwołanie. Czy są z tego korzyści?

Przykład:

func(SPRITE *x);

z wezwaniem

func(&mySprite);

vs.

func(SPRITE &x);

z wezwaniem

func(mySprite);

Nie zapomnij o newutworzeniu wskaźnika i wynikających z tego kwestiach własności.
Martin York,

Odpowiedzi:


216

Wskaźnik może otrzymać parametr NULL, parametr referencyjny nie. Jeśli kiedykolwiek istnieje szansa, że ​​możesz przekazać „brak obiektu”, użyj wskaźnika zamiast odwołania.

Przekazywanie przez wskaźnik pozwala również wyraźnie zobaczyć w witrynie wywołującej, czy obiekt jest przekazywany przez wartość, czy przez odniesienie:

// 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
Niekompletna odpowiedź. Używanie wskaźników nie zezwala na użycie obiektów tymczasowych / promowanych, ani na użycie spiczastego obiektu jako obiektu stosu. Sugeruje to, że argument może mieć wartość NULL, gdy przez większość czasu wartość NULL powinna być zabroniona. Przeczytaj odpowiedź litba, aby uzyskać pełną odpowiedź.
paercebal,

Drugie wywołanie funkcji było opatrzone adnotacją func2 passes by reference. Chociaż doceniam, że miałeś na myśli, że przekazuje „referencję” z perspektywy wysokiego poziomu, zaimplementowanej przez przekazanie wskaźnika w perspektywie na poziomie kodu, było to bardzo mylące (patrz stackoverflow.com/questions/13382356/... ).
Wyścigi lekkości na orbicie

Po prostu tego nie kupuję. Tak, przekazujesz wskaźnik, więc musi to być parametr wyjściowy, ponieważ to, na co wskazuje, nie może być const?
deworde 12.12.12

Czy nie mamy tego przechodzącego odniesienia w C? Używam najnowszej wersji codeblock (mingw) i wybieram projekt C. Nadal przechodzi przez referencje (func (int & a)) działa. Czy jest dostępny od wersji C99 lub C11?
Jon Wheelock,

1
@JonWheelock: Nie, C w ogóle nie ma referencji. func(int& a)nie jest poprawny C w żadnej wersji standardu. Prawdopodobnie kompilujesz pliki jako C ++ przez przypadek.
Adam Rosenfield,

245

Przechodząc obok wskaźnika

  • Dzwoniący musi wziąć adres -> nieprzejrzysty
  • Podać można wartość 0 nothing. Można to wykorzystać do podania opcjonalnych argumentów.

Przekaż przez odniesienie

  • Dzwoniący mija obiekt -> przezroczysty. Musi być użyty do przeciążenia operatora, ponieważ przeciążenie dla typów wskaźników nie jest możliwe (wskaźniki są wbudowanymi typami). Nie możesz więc string s = &str1 + &str2;używać wskaźników.
  • Możliwe wartości 0 -> Wywołana funkcja nie musi ich sprawdzać
  • Odwołanie do const akceptuje również tymczasowe: void f(const T& t); ... f(T(a, b, c));wskaźniki nie mogą być używane w ten sposób, ponieważ nie można wziąć adresu tymczasowego.
  • Last but not least, referencje są łatwiejsze w użyciu -> mniejsza szansa na błędy.

7
Przekazanie wskaźnika powoduje również „Czy przeniesiono własność, czy nie?” pytanie. Nie dotyczy to referencji.
Frerich Raabe

43
Nie zgadzam się z „mniejszą szansą na błędy”. Podczas sprawdzania strony połączenia, a czytelnik widzi „foo (& s)”, od razu wiadomo, że może ona zostać zmodyfikowana. Kiedy czytasz „foo (s)”, wcale nie jest jasne, czy można je zmodyfikować. Jest to główne źródło błędów. Być może istnieje mniejsza szansa na pewną klasę błędów, ale ogólnie rzecz biorąc, przekazywanie przez referencję jest ogromnym źródłem błędów.
William Pursell

25
Co rozumiesz przez „przezroczysty”?
Gbert90

2
@ Gbert90, jeśli widzisz foo (i a) na stronie wywoławczej, wiesz, że foo () przyjmuje typ wskaźnika. Jeśli zobaczysz foo (a), nie wiesz, czy wymaga referencji.
Michael J. Davenport,

3
@ MichaelJ.Davenport - w swoim wyjaśnieniu sugerujesz, że „przezroczysty” oznacza coś w stylu „oczywistego, że dzwoniący mija wskaźnik, ale nie jest oczywiste, że dzwoniący przekazuje referencję”. W poście Johannesa mówi: „Przechodząc przez wskaźnik - dzwoniący musi przyjąć adres -> nieprzejrzysty” oraz „Przekaż przez odniesienie - dzwoniący mija obiekt -> przezroczysty” - co jest prawie przeciwne do tego, co mówisz . Myślę, że pytanie Gbert90 „Co rozumiesz przez„ przezroczysty ”” jest nadal aktualne.
Happy Green Kid Naps

66

Podoba mi się rozumowanie w artykule z „cplusplus.com:”

  1. Przekaż wartość, gdy funkcja nie chce modyfikować parametru, a wartość można łatwo skopiować (ints, double, char, bool itp.) Typy proste. Std :: string, std :: vector i wszystkie inne STL pojemniki NIE są typami prostymi.)

  2. Przekaż wskaźnik const, gdy kopiowanie wartości jest drogie ORAZ funkcja nie chce modyfikować wartości wskazywanej na ORAZ NULL jest prawidłową, oczekiwaną wartością obsługiwaną przez funkcję.

  3. Przekazywanie wskaźnika non-const, gdy kopiowanie wartości jest drogie ORAZ funkcja chce zmodyfikować wartość wskazywaną na ORAZ NULL jest prawidłową, oczekiwaną wartością obsługiwaną przez funkcję.

  4. Przekaż referencję const, gdy kopiowanie wartości jest drogie ORAZ funkcja nie chce modyfikować wartości, do której odwołuje się AND NULL nie byłby prawidłową wartością, gdyby zamiast niej użyto wskaźnika.

  5. Przekazywanie przez odniesienie bez kontekstu, gdy kopiowanie wartości jest drogie ORAZ funkcja chce zmodyfikować wartość, do której odwołuje się ORAZ NULL nie byłaby prawidłową wartością, gdyby zamiast niej użyto wskaźnika.

  6. Pisząc funkcje szablonów, nie ma jednoznacznej odpowiedzi, ponieważ istnieje kilka kompromisów, które należy wziąć pod uwagę, które są poza zakresem tej dyskusji, ale wystarczy powiedzieć, że większość funkcji szablonów przyjmuje swoje parametry według wartości lub (stałej) referencji , jednak ponieważ składnia iteratora jest podobna do składni wskaźników (gwiazdka do „dereferencji”), każda funkcja szablonu, która oczekuje iteratorów jako argumentów, również domyślnie akceptuje wskaźniki (i nie sprawdza NULL, ponieważ koncepcja iteratora NULL ma inną składnię ).

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

Biorę z tego, że główna różnica między wyborem wskaźnika lub parametru referencyjnego polega na tym, czy NULL jest dopuszczalną wartością. Otóż ​​to.

To, czy wartość jest wejściowa, wyjściowa, modyfikowalna itp., Powinno jednak znajdować się w dokumentacji / komentarzach dotyczących funkcji.


Tak, dla mnie najważniejsze są tutaj NULL. Dziękuję za zacytowanie ..
binaryguy

62

Allen Holub w „Wystarczającej ilości liny, by strzelić sobie w stopę” wymienia następujące 2 zasady:

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

Wymienia kilka powodów, dla których dodano odniesienia do C ++:

  • są one niezbędne do zdefiniowania konstruktorów kopii
  • są niezbędne w przypadku przeciążeń operatora
  • const referencje pozwalają na semantykę przekazywania wartości przy jednoczesnym unikaniu kopiowania

Jego głównym punktem jest to, że odniesienia nie powinny być używane jako parametry „wyjściowe”, ponieważ w witrynie wywoławczej nie ma wskazania, czy parametr jest parametrem odniesienia, czy wartością. Zatem jego regułą jest używanie constreferencji wyłącznie jako argumentów.

Osobiście uważam, że jest to dobra zasada, ponieważ wyjaśnia, kiedy parametr jest parametrem wyjściowym, czy nie. Jednakże, chociaż osobiście się z tym ogólnie zgadzam, pozwalam się zachwiać opiniami innych członków mojego zespołu, jeśli argumentują za parametrami wyjściowymi jako odniesieniami (niektórzy programiści bardzo je lubią).


8
Moje stanowisko w tym argumencie jest takie, że jeśli nazwa funkcji czyni to całkowicie oczywistym, bez sprawdzania dokumentacji, że param zostanie zmodyfikowany, wówczas odwołanie do non-const jest OK. Więc osobiście pozwoliłbym na „getDetails (DetailStruct & result)”. Wskaźnik tam podnosi brzydką możliwość wprowadzenia wartości NULL.
Steve Jessop,

3
To jest mylące. Nawet jeśli niektórzy nie lubią referencji, są ważną częścią języka i powinny być używane jako takie. Ten sposób rozumowania przypomina powiedzenie: nie używaj szablonów, zawsze możesz użyć kontenerów void * do przechowywania dowolnego typu. Przeczytaj odpowiedź litb.
David Rodríguez - dribeas

4
Nie rozumiem, jak to jest mylące - są chwile, w których wymagane są referencje, i są chwile, w których najlepsze praktyki mogą sugerować ich nieużywanie, nawet jeśli możesz. To samo można powiedzieć o dowolnej funkcji języka - dziedziczeniu, przyjaciołach nie będących członkami, przeciążeniu operatora, MI itp.
Michael Burr

Nawiasem mówiąc, zgadzam się, że odpowiedź litba jest bardzo dobra i na pewno jest bardziej wyczerpująca niż ta - właśnie postanowiłem skupić się na omówieniu uzasadnienia unikania używania referencji jako parametrów wyjściowych.
Michael Burr,

1
Ta reguła jest używana w przewodniku po stylu Google c ++: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko

9

Wyjaśnienia do poprzednich postów:


Referencje NIE są gwarancją otrzymania wskaźnika o wartości innej niż zero. (Chociaż często traktujemy je jako takie.)

Chociaż jest to przerażająco zły kod, ponieważ zabiera cię za zaszyfrowany zły kod, następujące kompilują się i uruchamiają: (Przynajmniej pod moim kompilatorem).

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

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

Prawdziwy problem, jaki mam z referencjami, dotyczy innych programistów, odtąd określanych jako IDIOTS , którzy przydzielają w konstruktorze, zwalniają w destruktorze i nie dostarczają konstruktora kopii lub operatora = ().

Nagle istnieje różnica między foo (BAR BAR) a foo (BAR & bar) . (Automatyczna bitowa operacja kopiowania zostaje wywołana. Dezalokacja w destruktorze zostaje wywołana dwukrotnie.)

Na szczęście nowoczesne kompilatory wykryją podwójną dezalokację tego samego wskaźnika. 15 lat temu tego nie zrobili. (Pod gcc / g ++, użyj setenv MALLOC_CHECK_ 0, aby wrócić do starych sposobów.) W rezultacie , w systemie DEC UNIX, ta sama pamięć jest przydzielana do dwóch różnych obiektów. Dużo zabawy przy debugowaniu ...


Bardziej praktycznie:

  • Referencje ukrywają, że zmieniasz dane przechowywane gdzie indziej.
  • Łatwo pomylić odniesienie z skopiowanym obiektem.
  • Wskaźniki to oczywiste!

16
to nie jest problem funkcji lub referencji. łamiesz zasady językowe. samo wyrejestrowanie wskaźnika zerowego jest już niezdefiniowanym zachowaniem. „Referencje NIE są gwarancją otrzymania niepustego wskaźnika.”: Sam standard mówi, że są. inne sposoby stanowią zachowanie nieokreślone.
Johannes Schaub - litb

1
Zgadzam się z litb. Chociaż jest to prawda, kod, który nam pokazujesz, jest bardziej sabotażowy niż cokolwiek innego. Istnieją sposoby sabotowania czegokolwiek, w tym zarówno notacji „odniesienia”, jak i „wskaźnika”.
paercebal,

1
Powiedziałem, że to „zabrać cię za zły kod lasu”! W tym samym stylu możesz mieć i = new FOO; usuń i; test (* i); Kolejne (niestety często występujące) występowanie wskaźnika / odniesienia.
Mr.Ree,

1
W rzeczywistości nie chodzi o dereferencję NULL, ale raczej KORZYSTANIE z tego obiektu dereferencyjnego (zerowego). W związku z tym tak naprawdę nie ma różnicy (poza składnią) między wskaźnikami i referencjami z perspektywy implementacji języka. To użytkownicy mają różne oczekiwania.
Mr.Ree

2
Niezależnie od tego, co zrobisz ze zwróconym odwołaniem, w momencie, gdy powiesz *i, twój program zachowuje się w sposób nieokreślony. Na przykład kompilator może zobaczyć ten kod i założyć „OK, ten kod ma niezdefiniowane zachowanie we wszystkich ścieżkach kodu, więc cała ta funkcja musi być nieosiągalna”. Wtedy założymy, że wszystkie gałęzie prowadzące do tej funkcji nie są brane. Jest to regularnie przeprowadzana optymalizacja.
David Stone

5

Większość odpowiedzi tutaj nie rozwiązuje wewnętrznej dwuznaczności posiadania surowego wskaźnika w sygnaturze funkcji, jeśli chodzi o wyrażanie zamiaru. Problemy są następujące:

  • Program wywołujący nie wie, czy wskaźnik wskazuje pojedynczy obiekt, czy początek „tablicy” obiektów.

  • Dzwoniący nie wie, czy wskaźnik „posiada” pamięć, na którą wskazuje. IE, czy funkcja powinna zwolnić pamięć. ( foo(new int)- Czy to wyciek pamięci?).

  • Dzwoniący nie wie, czy nullptrmożna bezpiecznie przejść do funkcji.

Wszystkie te problemy są rozwiązywane przez referencje:

  • Odnośniki zawsze odnoszą się do jednego obiektu.

  • Referencje nigdy nie posiadają pamięci, do której się odnoszą, są jedynie widokiem na pamięć.

  • Referencje nie mogą mieć wartości zerowej.

To sprawia, że ​​referencje są znacznie lepszym kandydatem do ogólnego użytku. Jednak referencje nie są idealne - należy rozważyć kilka głównych problemów.

  • Brak wyraźnej pośrednictwa. Nie jest to problem z surowym wskaźnikiem, ponieważ musimy użyć &operatora, aby pokazać, że rzeczywiście mijamy wskaźnik. Na przykład int a = 5; foo(a);tutaj wcale nie jest jasne, że a jest przekazywane przez odniesienie i może zostać zmodyfikowane.
  • Nullability. Ta słabość wskaźników może być również siłą, gdy faktycznie chcemy, aby nasze referencje były zerowalne. Uznanie za std::optional<T&>nieważne (z dobrych powodów), wskaźniki dają nam taką pożądalność.

Wydaje się więc, że kiedy chcemy nullownego odniesienia z wyraźną pośrednią interwencją, powinniśmy sięgnąć po T*prawo? Źle!

Abstrakcje

W naszej desperacji na punkcie zerowania możemy sięgnąć T*i po prostu zignorować wszystkie niedociągnięcia i semantyczne dwuznaczności wymienione wcześniej. Zamiast tego powinniśmy sięgnąć po to, co C ++ robi najlepiej: abstrakcję. Jeśli po prostu napiszemy klasę, która owija się wokół wskaźnika, zyskujemy wyrazistość, a także nullability i wyraźną pośredniość.

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

Jest to najprostszy interfejs, jaki mogłem wymyślić, ale działa on skutecznie. Pozwala na zainicjowanie odwołania, sprawdzenie, czy wartość istnieje i dostęp do wartości. Możemy go używać w następujący sposób:

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

Osiągnęliśmy nasze cele! Zobaczmy teraz zalety w porównaniu do surowego wskaźnika.

  • Interfejs pokazuje wyraźnie, że odwołanie powinno odnosić się tylko do jednego obiektu.
  • Najwyraźniej nie jest właścicielem pamięci, do której się odnosi, ponieważ nie ma niszczyciela zdefiniowanego przez użytkownika ani metody usuwania pamięci.
  • Program wywołujący wie, że nullptrmożna go przekazać, ponieważ autor funkcji wyraźnie prosi o polecenieoptional_ref

Możemy sprawić, że interfejs stanie się bardziej złożony, na przykład dodając operatory równości, interfejs monadyczny get_ori mapinterfejs, metodę, która pobiera wartość lub zgłasza wyjątek, constexprobsługę. Możesz to zrobić przez ciebie.

Podsumowując, zamiast używać surowych wskaźników, uzasadnij, co te wskaźniki faktycznie oznaczają w kodzie, albo wykorzystaj standardową abstrakcję biblioteki lub napisz własną. Znacząco poprawi to Twój kod.


3

Nie całkiem. Wewnętrznie przekazywanie przez odniesienie odbywa się poprzez zasadniczo przekazanie adresu odnośnego obiektu. Tak więc, naprawdę nie ma żadnego przyrostu wydajności po przejściu wskaźnika.

Przekazywanie przez odniesienie ma jednak jedną zaletę. Na pewno masz instancję dowolnego obiektu / typu, który jest przekazywany. Jeśli podasz wskaźnik, ryzykujesz otrzymaniem wskaźnika NULL. Korzystając z funkcji przekazywania według referencji, wypychasz niejawne sprawdzenie NULL o jeden poziom w górę do programu wywołującego twoją funkcję.


1
Jest to zarówno zaletą, jak i wadą. Wiele interfejsów API używa wskaźników NULL, aby oznaczać coś pożytecznego (tj. NULL określony czas oczekiwania na zawsze, podczas gdy wartość oznacza czekanie tak długo).
Greg Rogers,

1
@Brian: Nie chcę być nit-picking, ale: I byłoby nie powiedzieć jedno jest gwarantowane , aby uzyskać instancję gdy uzyskanie referencji. Zwisające odniesienia są nadal możliwe, jeśli funkcja wywołująca odwołanie odwołuje się do zwisającego wskaźnika, którego odbiorca nie może znać.
foraidt

czasem możesz nawet zwiększyć wydajność, korzystając z referencji, ponieważ nie muszą one zajmować miejsca i nie mają przypisanych adresów. nie wymaga pośrednictwa.
Johannes Schaub - litb

Programy zawierające zwisające odwołania nie są poprawnym językiem C ++. Dlatego tak, kod może założyć, że wszystkie odwołania są prawidłowe.
Konrad Rudolph,

2
Na pewno mogę wyrejestrować wskaźnik zerowy, a kompilator nie będzie w stanie stwierdzić ... jeśli kompilator nie może powiedzieć, że jest „nieprawidłowy C ++”, to czy naprawdę jest nieprawidłowy?
rmeador,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.