Pytanie Jakie są różnice między zmienną wskaźnika a zmienną referencyjną w C ++?


Wiem, że referencje to cukier syntaktyczny, więc kod jest łatwiejszy do odczytania i napisania.

Ale jakie są różnice?


Podsumowanie odpowiedzi i linków poniżej:

  1. Wskaźnik może być ponownie przydzielany dowolną liczbę razy, podczas gdy odniesienie nie może zostać ponownie przypisane po powiązaniu.
  2. Wskaźniki mogą wskazywać donikąd (NULL), podczas gdy odniesienie zawsze odnosi się do przedmiotu.
  3. Nie możesz wziąć adresu odniesienia, jak możesz, za pomocą wskaźników.
  4. Nie ma "arytmetycznej referencji" (ale można wziąć adres obiektu wskazanego przez odniesienie i wykonać na nim arytmetykę wskaźnikową, jak w &obj + 5).

Aby wyjaśnić nieporozumienie:

Standard C ++ jest bardzo ostrożny, aby uniknąć dyktowania, jak może kompilator   zaimplementuj referencje, ale każdy kompilator C ++ implementuje   referencje jako wskaźniki. Oznacza to, że deklaracja taka jak:

int &ri = i;

jeśli nie jest całkowicie zoptymalizowany, przydziela taką samą ilość pamięci   jako wskaźnik i umieszcza adres   z i do tego magazynu.

Zatem zarówno wskaźnik, jak i odniesienie używają tej samej ilości pamięci.

Z reguły,

  • Użyj odwołań w parametrach funkcji i typach zwracanych, aby zapewnić użyteczne i samokodujące się interfejsy.
  • Użyj wskaźników do implementacji algorytmów i struktur danych.

Ciekawa lektura:


2625
2017-09-11 20:03


pochodzenie


Myślę, że punkt 2 powinien brzmieć: "Wskaźnik może mieć wartość NULL, ale odniesienie nie jest, tylko źle sformułowany kod może utworzyć odwołanie NULL, a jego zachowanie jest niezdefiniowane." - Mark Ransom
Wskaźniki są po prostu kolejnym typem obiektów i jak każdy obiekt w C ++ mogą być zmienną. Referencje z drugiej strony nigdy nie są przedmiotami, tylko zmienne. - Kerrek SB
Kompiluje się bez ostrzeżeń: int &x = *(int*)0; na gcc. Odniesienie może rzeczywiście wskazywać NULL. - Calmarius
referencja jest zmiennym aliasem - Khaled.K
Podoba mi się, jak bardzo pierwsze zdanie jest całkowitym błędem. Referencje mają swoją własną semantykę. - Lightness Races in Orbit


Odpowiedzi:


  1. Wskaźnik można ponownie przypisać:

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

    Odwołanie nie może i musi być przypisane podczas inicjowania:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. Wskaźnik ma swój własny adres i rozmiar na stosie (4 bajty na x86), podczas gdy odniesienie dzieli ten sam adres pamięci (z oryginalną zmienną), ale zajmuje również trochę miejsca na stosie. Ponieważ odwołanie ma taki sam adres jak sama oryginalna zmienna, bezpiecznie można pomyśleć o referencji jako innej nazwie dla tej samej zmiennej. Uwaga: Wskazany wskaźnik może znajdować się na stosie lub stercie. To referencja. Moje twierdzenie w tym stwierdzeniu nie oznacza, że ​​wskaźnik musi wskazywać na stos. Wskaźnik jest po prostu zmienną przechowującą adres pamięci. Ta zmienna jest na stosie. Ponieważ odwołanie ma swoją własną przestrzeń na stosie, a ponieważ adres jest taki sam jak zmienna, do której się odnosi. Więcej informacji stos vs sterty. Oznacza to, że istnieje prawdziwy adres odwołania, którego kompilator nie powie.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. Możesz mieć wskaźniki do wskaźników do wskaźników oferujących dodatkowe poziomy pośrednie. Natomiast referencje oferują tylko jeden poziom pośredni.

    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. Wskaźnik można przypisać nullptr bezpośrednio, podczas gdy odniesienie nie może. Jeśli spróbujesz wystarczająco mocno, i wiesz jak, możesz zrobić adres odniesienia nullptr. Podobnie, jeśli spróbujesz wystarczająco mocno, możesz mieć odniesienie do wskaźnika, a wtedy odniesienie może zawierać 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. Wskaźniki mogą iterować po tablicy, możesz użyć ++aby przejść do następnego elementu, na który wskazuje wskaźnik, oraz + 4 aby przejść do piątego elementu. Nie ma znaczenia, jaki rozmiar jest obiekt, na który wskazuje wskaźnik.

  6. Wskaźnik musi zostać usunięty * aby uzyskać dostęp do wskazanej lokalizacji pamięci, podczas gdy odniesienie może być użyte bezpośrednio. Wskaźnik do klasy / struct używa -> aby uzyskać dostęp do jego członków, podczas gdy odwołanie używa a ..

  7. Wskaźnik jest zmienną przechowującą adres pamięci. Bez względu na to, jak implementowane jest odniesienie, odniesienie ma taki sam adres pamięci, jak element, do którego się odnosi.

  8. Odniesień nie można wstawić do tablicy, podczas gdy wskaźniki mogą być (Wspomniane przez użytkownika @litb)

  9. Odwołania do const mogą być powiązane z tymczasowymi. Wskaźniki nie mogą (nie bez pewnego pośrednictwa):

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

    To sprawia const& bezpieczniejsze w użyciu w listach argumentów i tak dalej.


1376
2018-02-27 21:26



... ale dereferencja NULL jest niezdefiniowana. Na przykład nie można przetestować, czy odwołanie ma wartość NULL (np. & Ref == NULL). - Pat Notz
Numer 2 to nie prawdziwe. Odniesienia nie są po prostu "inną nazwą tej samej zmiennej". Odnośniki mogą być przekazywane do funkcji, przechowywane w klasach itp. W sposób bardzo podobny do wskaźników. Istnieją one niezależnie od zmiennych, które wskazują. - Derek Park
Brian, stos nie jest istotny. Referencje i wskaźniki nie muszą zajmować miejsca na stosie. Oba mogą być przydzielone na stercie. - Derek Park
Brian, fakt, że zmienna (w tym przypadku wskaźnik lub odniesienie) wymaga przestrzeni nie oznacza, że ​​wymaga miejsca na stosie. Wskaźniki i referencje mogą nie tylko punkt do kupy, mogą faktycznie być asygnowany na kupie. - Derek Park
inna ważna różnica: referencje nie mogą być wstawione do tablicy - Johannes Schaub - litb


Co to jest referencja C ++ (dla programistów C)

ZA odniesienie można uważać za stały wskaźnik (nie mylić ze wskaźnikiem na stałą wartość!) z automatycznym pośrednikiem, tzn. kompilator zastosuje * operator dla ciebie.

Wszystkie odwołania muszą być zainicjowane wartością inną niż null lub kompilacja zakończy się niepowodzeniem. Nie jest możliwe uzyskanie adresu odniesienia - operator adresu zamiast tego zwróci adres przywoływanej wartości - ani nie jest możliwe wykonanie arytmetycznych odniesień.

Programiści C mogą nie lubić referencji w C ++, ponieważ nie będzie już oczywiste, gdy dojdzie do indeksu lub jeśli argument zostanie przekazany według wartości lub wskaźnika, nie patrząc na sygnatury funkcji.

Programiści C ++ mogą nie lubić używania wskaźników, ponieważ są one uważane za niebezpieczne - chociaż referencje nie są tak naprawdę bezpieczniejsze od stałych wskaźników, z wyjątkiem najtrudniejszych przypadków - brak wygody automatycznej dwukierunkowości i niosą inną semantyczną konotację.

Rozważ następujące oświadczenie z FAQ C ++:

Mimo że odniesienie jest często realizowane za pomocą adresu w   podstawowy język asemblerowy, proszę nie myśl o referencji jako   zabawny wyglądający wskaźnik do obiektu. Referencja jest obiekt. To jest   nie wskaźnik do obiektu ani kopia obiektu. To jest    obiekt.

Ale jeśli odniesienie naprawdę były przedmiotem, jak mogłyby być wiszące referencje? W językach niezarządzanych niemożliwe jest, aby odniesienia były "bezpieczniejsze" niż wskaźniki - ogólnie nie ma sposobu na niezawodne alioryzowanie wartości w obrębie granic zakresu!

Dlaczego uważam odniesienia C ++ za użyteczne

Pochodzące z tła C, odniesienia C ++ mogą wyglądać na nieco głupią koncepcję, ale należy nadal używać ich zamiast wskaźników, gdy jest to możliwe: Automatyczny dwukierunkowy jest wygodne, a odniesienia stają się szczególnie przydatne w kontaktach z RAII - ale nie z powodu jakiejkolwiek dostrzeżonej przewagi w zakresie bezpieczeństwa, ale raczej dlatego, że sprawiają, że pisanie idiomatycznego kodu jest mniej niezręczne.

RAII jest jedną z głównych koncepcji języka C ++, ale interweniuje nietrywialnie z kopiowaniem semantyki. Przekazywanie obiektów przez odniesienie pozwala uniknąć tych problemów, ponieważ nie wymaga to kopiowania. Jeśli referencje nie były obecne w tym języku, należałoby zamiast tego użyć wskaźników, które są bardziej kłopotliwe w użyciu, a zatem naruszają zasadę projektowania języka, że ​​rozwiązanie najlepszej praktyki powinno być łatwiejsze niż rozwiązania alternatywne.


301
2017-09-11 21:43



@kriss: Nie, możesz również uzyskać referencję zwisającą, zwracając zmienną automatyczną przez odniesienie. - Ben Voigt
@kriss: Kompilator jest praktycznie niemożliwy do wykrycia w ogólnym przypadku. Rozważmy funkcję składową, która zwraca referencję do zmiennej składowej klasy: jest to bezpieczne i nie powinno być zabronione przez kompilator. Następnie wywołujący, który ma automatyczne wystąpienie tej klasy, wywołuje tę funkcję członka i zwraca referencję. Presto: zwisający odnośnik. I tak, spowoduje to kłopoty, @kriss: to jest moja uwaga. Wiele osób twierdzi, że zaletą odniesień do wskaźników jest to, że referencje są zawsze ważne, ale tak nie jest. - Ben Voigt
@kriss: Nie, odwołanie do obiektu automatycznego czasu przechowywania bardzo różni się od obiektu tymczasowego. W każdym razie, właśnie dostarczałem kontrprzykład dla twojego wyciągu, że możesz uzyskać tylko nieważne referencje, usuwając odwołanie do niewłaściwego wskaźnika. Christoph ma rację - referencje nie są bezpieczniejsze od wskaźników, program, który używa wyłącznie referencji, może jeszcze złamać bezpieczeństwo typu. - Ben Voigt
Referencje nie są rodzajem wskaźnika. Są nową nazwą istniejącego obiektu. - catphive
@catphive: true, jeśli przejdziesz przez semantykę językową, nieprawdą, jeśli rzeczywiście spojrzysz na implementację; C ++ jest znacznie bardziej "magicznym" językiem C, a jeśli usuniesz magię z odniesień, otrzymasz wskaźnik - Christoph


Jeśli chcesz być naprawdę pedantyczny, możesz zrobić jedną rzecz z referencją, której nie możesz zrobić za pomocą wskaźnika: przedłużyć żywotność tymczasowego obiektu. W języku C ++, jeśli wiążesz odwołanie do obiektu tymczasowego, czas życia tego obiektu staje się czasem życia odwołania.

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

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

W tym przykładzie s3_copy kopiuje obiekt tymczasowy, który jest wynikiem konkatenacji. Natomiast s3_reference w istocie staje się obiektem tymczasowym. To jest naprawdę odniesienie do tymczasowego obiektu, który ma teraz takie samo życie jak odniesienie.

Jeśli spróbujesz tego bez const nie powinno się kompilować. Nie można powiązać niezwiązanego z nim odwołania do obiektu tymczasowego, ani nie można za jego adresu przyjąć adresu.


152
2017-09-11 21:06



ale jaki jest w tym przypadek użycia? - Ahmad Mushtaq
Cóż, s3_copy utworzy tymczasowy, a następnie skopiuje go do s3_copy, podczas gdy s3_reference bezpośrednio używa tymczasowego. Aby być naprawdę pedantycznym, musisz przyjrzeć się Optymalizacji Wartości Zwrotnej, dzięki której kompilator może usunąć konstrukcję kopii w pierwszym przypadku. - Matt Price
@digitalSurgeon: Magia jest dość potężna. Żywotność obiektu jest przedłużona przez fakt const & wiązania i tylko wtedy, gdy odwołanie wykracza poza zakres destruktora rzeczywisty typ odniesienia (w porównaniu do typu odniesienia, który może być podstawą) jest wywoływany. Ponieważ jest to odniesienie, nie będzie między nimi rozcinania. - David Rodríguez - dribeas
Aktualizacja dla C ++ 11: ostatnie zdanie powinno brzmieć "Nie można związać stałego odniesienia o l-wartości do tymczasowego", ponieważ ty mogąbind a non const rvalue odniesienie do tymczasowego i ma to samo przedłużające się zachowanie. - Oktalist
@AhmadMushtaq: Kluczowe jest to klasy pochodne. Jeśli nie ma w tym przypadku dziedziczenia, równie dobrze możesz użyć semantyki wartości, która będzie tania lub wolna ze względu na konstrukcję RVO / move. Ale jeśli masz Animal x = fast ? getHare() : getTortoise() następnie x zmierzy się z klasycznym problemem krojenia, podczas gdy Animal& x = ... będzie działać poprawnie. - Arthur Tacca


W przeciwieństwie do popularnej opinii, możliwe jest odniesienie, które jest NULL.

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

Oczywiście, dużo trudniej zrobić z referencją - ale jeśli sobie poradzisz, wyrwiesz włosy, próbując je znaleźć. Referencje są nie z natury bezpieczne w C ++!

Technicznie to jest nieprawidłowe odwołanie, a nie odwołanie zerowe. C ++ nie obsługuje odwołań zerowych jako pojęcia, które można znaleźć w innych językach. Istnieją również inne rodzaje nieprawidłowych odniesień. Każdy nieprawidłowe odwołanie podnosi widmo niezdefiniowane zachowanie, podobnie jak przy użyciu nieprawidłowego wskaźnika.

Rzeczywisty błąd dotyczy dereferencji wskaźnika NULL przed przypisaniem do odwołania. Ale nie jestem świadomy żadnych kompilatorów, które wygenerują jakiekolwiek błędy w tym stanie - błąd rozprzestrzenia się do punktu dalej w kodzie. To sprawia, że ​​ten problem jest tak podstępny. W większości przypadków, jeśli usuniesz wskaźnik NULL, ulegniesz awarii w tym miejscu i nie trzeba będzie dużo debugowania, aby to zrozumieć.

Mój powyższy przykład jest krótki i wymyślony. Oto przykład z prawdziwego świata.

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

Chcę powtórzyć, że jedynym sposobem uzyskania zerowego odniesienia jest błędny kod, a gdy już go uzyskasz, otrzymujesz niezdefiniowane zachowanie. To nigdy sensowne jest sprawdzenie zerowego odniesienia; na przykład możesz spróbować if(&bar==NULL)... ale kompilator może zoptymalizować oświadczenie nie istnieć! Poprawna referencja nigdy nie może mieć wartości NULL, więc z widoku kompilatora porównanie zawsze jest fałszywe i można je całkowicie wyeliminować if klauzula jako martwy kod - jest to esencja nieokreślonego zachowania.

Właściwym sposobem uniknięcia problemów jest uniknięcie dereferencji wskaźnika NULL w celu utworzenia odwołania. Oto zautomatyzowany sposób osiągnięcia tego.

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

Aby uzyskać starsze spojrzenie na ten problem od kogoś, kto ma lepsze umiejętności pisania, zobacz Null References od Jima Hyslopa i Herb Sutter.

Dla innego przykładu niebezpieczeństwa dereferencji zerowy wskaźnik zobacz Narażanie niezdefiniowanego zachowania podczas próby przeniesienia kodu na inną platformę Raymond Chen.


104
2017-09-11 20:07



Kod, o którym mowa, zawiera niezdefiniowane zachowanie. Z technicznego punktu widzenia nie można nic zrobić z pustym wskaźnikiem, poza ustawieniem go i porównać. Gdy twój program wywoła niezdefiniowane zachowanie, może zrobić wszystko, w tym wyglądać tak, aby działał poprawnie, dopóki nie dasz dema wielkiemu bossowi. - KeithB
mark ma prawidłowy argument. Argument, że wskaźnik może mieć wartość NULL, a ty także musisz to sprawdzić, również nie jest prawdziwy: jeśli powiesz, że funkcja wymaga wartości innej niż NULL, musi to zrobić wywołujący. więc jeśli dzwoniący nie wywołuje niezdefiniowanego zachowania. tak jak znak zrobił ze złym odniesieniem - Johannes Schaub - litb
Opis jest błędny. Ten kod może, ale nie musi, utworzyć odwołanie o wartości NULL. Jego zachowanie jest niezdefiniowane. Może stworzyć doskonałe odniesienie. W ogóle może nie utworzyć żadnego odniesienia. - David Schwartz
@ David Schwartz, gdybym mówił o tym, jak rzeczy powinny działać zgodnie ze standardem, będziesz poprawny. Ale to nie o czym mówię - mówię o faktycznym zaobserwowanym zachowaniu z bardzo popularnym kompilatorem i ekstrapolowaniem na podstawie mojej wiedzy o typowych kompilatorach i architekturach procesorów do tego, co prawdopodobnie zdarzyć. Jeśli wierzysz, że odniesienia są lepsze od wskaźników, ponieważ są bezpieczniejsze i nie uważają, że odniesienia mogą być złe, pewnego dnia będziesz zaskoczony prostym problemem, tak jak ja. - Mark Ransom
Usunięcie zerowego wskaźnika jest błędne. Każdy program, który to robi, nawet w celu zainicjowania referencji jest błędny. Jeśli inicjujesz referencję ze wskaźnika, zawsze powinieneś sprawdzić, czy wskaźnik jest prawidłowy. Nawet jeśli to się powiedzie, obiekt leżący u podstaw może zostać usunięty w dowolnym momencie, pozostawiając odwołanie do odwołania do nieistniejącego obiektu, prawda? To, co mówisz, to dobra rzecz. Myślę, że prawdziwym problemem jest to, że referencja NIE musi być sprawdzana pod kątem "zerowej", gdy widzisz jeden i wskaźnik powinien być, co najmniej, potwierdzony. - t0rakka


Oprócz cukru syntaktycznego odniesienie to const wskaźnik (nie wskaźnik do a const). Musisz określić, do czego się odnosi, kiedy deklarujesz zmienną referencyjną, i nie możesz jej później zmienić.

Aktualizacja: teraz, gdy myślę o tym więcej, jest ważna różnica.

Wskaźnik celu const można zastąpić, biorąc jego adres i używając rzutowania const.

Cel odniesienia nie może być w żaden sposób zastąpiony UB.

Powinno to pozwolić kompilatorowi na większą optymalizację referencji.


97
2017-09-11 22:10



Myślę, że to najlepsza odpowiedź. Inni mówią o referencjach i wskaźnikach, takich jak różne zwierzęta, a następnie określają, w jaki sposób różnią się zachowaniem. To nie ułatwia imho. Zawsze rozumiem odniesienia jako bycie T* const z różnym cukrem syntaktycznym (co zdarza się wyeliminować wiele * i & z twojego kodu). - Carlo Wood
wyobraź sobie, że próbujesz to zrobić cout << 42 << "hello" << endl bez referencji - pm100
Krótka i najlepsza odpowiedź - Kad
"Cel obiektu stałego można zastąpić, biorąc jego adres i używając rzutowania const." Jest to niezdefiniowane zachowanie. Widzieć stackoverflow.com/questions/25209838/... dla szczegółów. - dgnuff
Próba zmiany albo odniesienia do referencji, albo wartości wskaźnika const (lub dowolnego skalaru stałego) jest niezgodna z prawem. Co możesz zrobić: usuń kwalifikację const dodaną przez niejawną konwersję: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci); jest OK. - curiousguy


Zapomniałeś najważniejszej części:

dostęp członków z wykorzystaniem wskaźników -> 
dostęp do członków z użyciem referencji .

foo.bar jest Wyraźnie lepszy foo->bar w ten sam sposób vi jest Wyraźnie lepszy Emacs :-)


96
2017-09-19 12:23



@Orion Edwards> dostęp członków ze wskaźnikami używa ->> dostęp do członków z użyciem odniesień. To nie jest w 100% prawdziwe. Możesz mieć odniesienie do wskaźnika. W takim przypadku uzyskasz dostęp do elementów odwoływanych do wskaźnika za pomocą -> węzeł struktury {węzeł * następny; }; Węzeł * pierwszy; // p jest odniesieniem do wskaźnika void foo (Węzeł * i p) {p-> next = first; } Węzeł * pasek = nowy węzeł; foo (bar); - OP: Czy znasz pojęcia wartości r i bursztynu?
Inteligentne wskaźniki mają oba. (metody w klasie inteligentnego wskaźnika) i -> (metody na typie bazowym). - JBRWilkinson
@ user6105 Orion Edwards oświadczenie jest w rzeczywistości w 100% prawdziwe. "uzyskuj dostęp do elementu [odniesionego] odnośnika" Wskaźnik nie ma żadnych elementów. Obiekt, do którego odnosi się wskaźnik, ma członków, a dostęp do nich jest właśnie tym -> zapewnia odniesienia do wskaźników, tak jak sam wskaźnik. - Max Truxa
dlaczego . i -> ma coś wspólnego z vi i emacs :) - artm
@artM - to był żart i prawdopodobnie nie ma sensu dla osób, dla których angielski nie jest językiem ojczystym. Przepraszam. Wyjaśnienie, czy vi jest lepsze niż emacs, jest całkowicie subiektywne. Niektórzy uważają, że vi jest znacznie lepsza, a inni uważają, że jest dokładnie odwrotnie. Podobnie myślę, używając . jest lepsze niż użycie ->, ale tak jak vi vs emacs, jest całkowicie subiektywny i nie można niczego udowodnić - Orion Edwards


W rzeczywistości odwołanie nie przypomina wskaźnika.

Kompilator przechowuje "odniesienia" do zmiennych, wiążąc nazwę z adresem pamięci; jego zadaniem jest przetłumaczyć dowolną nazwę zmiennej na adres pamięci podczas kompilacji.

Kiedy tworzysz referencję, mówisz kompilatorowi, że przypisujesz inną nazwę do zmiennej wskaźnika; dlatego odniesienia nie mogą "wskazywać na wartość null", ponieważ zmienna nie może być, a nie być.

Wskaźniki są zmiennymi; zawierają adres innej zmiennej lub mogą mieć wartość zerową. Ważną rzeczą jest to, że wskaźnik ma wartość, podczas gdy odniesienie ma tylko zmienną, do której się odwołuje.

Teraz wyjaśnienie prawdziwego kodu:

int a = 0;
int& b = a;

Tutaj nie tworzysz innej zmiennej, która wskazuje a; właśnie dodajesz inną nazwę do zawartości pamięci zawierającej wartość a. Ta pamięć ma teraz dwie nazwy, a i b, i można go rozwiązać za pomocą obu nazw.

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

int a;
increment(a);

Podczas wywoływania funkcji kompilator zazwyczaj generuje spacje pamięci dla argumentów, które mają być skopiowane. Sygnatura funkcji definiuje spacje, które powinny zostać utworzone i podaje nazwę, która ma być użyta dla tych przestrzeni. Deklarowanie parametru jako odniesienia powoduje, że kompilator używa zmiennej przestrzeni wejściowej zamiast przydzielania nowego miejsca pamięci podczas wywoływania metody. Może wydawać się dziwne, że twoja funkcja będzie bezpośrednio manipulować zmienną zadeklarowaną w zakresie wywoływania, ale pamiętaj, że podczas wykonywania skompilowanego kodu, nie ma już zakresu; jest tylko zwykła płaska pamięć, a twój kod funkcji może manipulować dowolnymi zmiennymi.

Teraz mogą zdarzyć się przypadki, w których kompilator może nie znać referencji podczas kompilacji, np. Przy użyciu zmiennej extern. Odwołanie może, ale nie musi zostać zaimplementowane jako wskaźnik w kodzie źródłowym. Ale w przykładach, które ci dałem, najprawdopodobniej nie zostanie zaimplementowany za pomocą wskaźnika.


57
2017-09-01 03:44



Odniesienie jest odniesieniem do wartości l, niekoniecznie do zmiennej. Z tego powodu jest znacznie bliższy wskaźnikowi niż prawdziwemu aliasowi (konstruktowi kompilacji). Przykłady wyrażeń, do których można się odwoływać, to * p lub nawet * p ++ - Arkadiy
Tak, właśnie wskazywałem, że odwołanie nie zawsze przesuwa nową zmienną na stos tak, jak nowy wskaźnik. - Vincent Robert
@VincentRobert: Będzie działać tak samo jak wskaźnik ... jeśli funkcja jest inline, zarówno odniesienie, jak i wskaźnik zostaną zoptymalizowane. Jeśli istnieje wywołanie funkcji, adres obiektu będzie musiał zostać przekazany do funkcji. - Ben Voigt
int * p = NULL; int & r = * p; odnośnik wskazujący na NULL; if (r) {} -> boOm;) - sree
To skupienie na etapie kompilacji wydaje się fajne, dopóki nie zapamiętasz, że referencje mogą być przekazywane w czasie wykonywania, w którym to momencie alias przestaje działać. (A potem są odniesienia zazwyczaj zaimplementowane jako wskaźniki, ale standard nie wymaga tej metody.) - underscore_d


Odnośniki są bardzo podobne do wskaźników, ale są specjalnie zaprojektowane, aby pomóc w optymalizacji kompilatorów.

  • Referencje są zaprojektowane w taki sposób, że dla kompilatora jest znacznie łatwiej wyśledzić, które odwołania odwołują się do zmiennych. Dwie ważne cechy są bardzo ważne: brak "arytmetycznych odniesień" i brak ponownego przypisania referencji. Pozwalają one kompilatorowi na ustalenie, które referencje określają zmienne w czasie kompilacji.
  • Odniesienia mogą odnosić się do zmiennych, które nie mają adresów pamięci, takich jak te, które kompilator wybiera do umieszczenia w rejestrach. Jeśli przyjmiesz adres zmiennej lokalnej, kompilatorowi trudno jest umieścić go w rejestrze.

Jako przykład:

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

Kompilator optymalizacyjny może zdać sobie sprawę, że mamy dostęp do [0] i [1] całkiem sporej ilości. Z przyjemnością zoptymalizuje algorytm do:

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
}

Aby dokonać takiej optymalizacji, musi udowodnić, że nic nie może zmienić tablicy [1] podczas połączenia. Jest to raczej łatwe do zrobienia. i nigdy nie jest mniejsza niż 2, więc array [i] nigdy nie może odwoływać się do array [1]. mayModify () otrzymuje a0 jako odniesienie (aliasing array [0]). Ponieważ nie istnieje arytmetyczna "referencyjna", kompilator musi tylko udowodnić, że MaybeModify nigdy nie otrzymuje adresu x, i udowodnił, że nic nie zmienia tablicy [1].

Musi także udowodnić, że nie ma możliwości, aby przyszłe wezwanie mogło odczytać / zapisać [0], podczas gdy my mamy tymczasową kopię rejestru w a0. Często jest to łatwe do udowodnienia, ponieważ w wielu przypadkach jest oczywiste, że odniesienie nigdy nie jest przechowywane w trwałej strukturze, takiej jak instancja klasy.

Teraz zrób to samo ze wskaźnikami

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

Zachowanie jest takie samo; dopiero teraz znacznie trudniej jest udowodnić, że MaybeModify nigdy nie modyfikuje tablicy [1], ponieważ już daliśmy jej wskaźnik; kot jest poza workiem. Teraz musi zrobić o wiele trudniejszy dowód: statyczną analizę maybeModify, aby udowodnić, że nigdy nie pisze do & x + 1. Musi także udowodnić, że nigdy nie zapisuje wskaźnika, który może odnosić się do tablicy [0], która jest po prostu tak trudne.

Współczesne kompilatory są coraz lepsze w analizie statycznej, ale zawsze dobrze jest im pomóc i korzystać z referencji.

Oczywiście, poza takimi sprytnymi optymalizacjami, kompilatory w razie potrzeby zamienią odniesienia w wskaźniki.


50
2017-09-11 20:12



To prawda, że ​​ciało musi być widoczne. Jednak określenie to maybeModify nie przyjmuje adresu czegokolwiek związanego z x jest znacznie łatwiejsze niż udowodnienie, że arytmetyka wskaźnika nie występuje. - Cort Ammon
Wierzę, że optymalizator już robi, że "nie ma wiązki wskaźnika arytemetycznego", sprawdź kilka innych powodów. - Ben Voigt
"Referencje są bardzo podobne do wskaźników" - semantycznie, w odpowiednich kontekstach - ale pod względem wygenerowanego kodu, tylko w niektórych implementacjach, a nie poprzez jakąkolwiek definicję / wymóg. Wiem, że zwróciłaś na to uwagę i nie zgadzam się z żadnym z twoich postów w praktyce, ale mamy zbyt wiele problemów z ludźmi, którzy czytają zbyt wiele w skrótowych opisach, takich jak "referencje są jak / zwykle realizowane jako wskaźniki" . - underscore_d
Mam wrażenie, że ktoś niesłusznie oznaczył jako przestarzały komentarz na wzór void maybeModify(int& x) { 1[&x]++; }, które omawiają pozostałe komentarze powyżej - Ben Voigt


Odniesienie nigdy nie może być NULL.


32
2018-05-20 19:26



Zobacz odpowiedź Marka Ransoma na kontrprzykład. Jest to najczęściej podawany mit o referencjach, ale jest to mit. Jedyną gwarancją, którą masz na podstawie standardu, jest to, że od razu masz UB, gdy masz odniesienie NULL. Ale jest to podobne do powiedzenia: "Ten samochód jest bezpieczny, nigdy nie może zejść z drogi." Nie bierzemy żadnej odpowiedzialności za to, co może się zdarzyć, jeśli i tak zejdziecie z drogi, może wybuchnąć. " - cmaster
@cmaster: W ważnym programie, referencja nie może być pusta. Ale wskaźnik może. To nie jest mit, to fakt. - Mehrdad
@Mehrdad Tak, ważne programy pozostają w drodze. Ale nie ma bariery ruchu, która mogłaby wymuszać działanie programu. Duża część drogi to właściwie brak oznaczeń. Tak więc bardzo łatwo zejść z drogi w nocy. A dla debugowania takich błędów jest to bardzo ważne wiedzieć może się to zdarzyć: odwołanie zerowe może się rozprzestrzeniać, zanim wywróci program, tak samo jak wskaźnik zerowy. A kiedy już masz kod jak void Foo::bar() { virtual_baz(); } to jest segfault. Jeśli nie wiesz, że odwołania mogą mieć wartość zerową, nie możesz wyśledzić wartości null z powrotem do jej źródła. - cmaster
int * p = NULL; int & r = * p; odnośnik wskazujący na NULL; if (r) {} -> boOm;) - - sree
@ sree int &r=*p; jest niezdefiniowanym zachowaniem. W tym momencie nie masz "odniesienia wskazującego na NULL", masz program, który nie można już dłużej się zastanawiać w ogóle. - cdhowie