Jakie są różnice między zmienną wskaźnika i zmienną odniesienia w C ++?


3261

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

Ale jakie są różnice?


100
Myślę, że punkt 2 powinien brzmieć: „Dozwolony jest wskaźnik NULL, ale odwołanie nie. Tylko źle sformułowany kod może utworzyć odwołanie NULL, a jego zachowanie jest niezdefiniowane”.
Mark Ransom,

19
Wskaźniki są po prostu innym typem obiektu i podobnie jak każdy obiekt w C ++ mogą być zmienną. Z drugiej strony odniesienia nie są nigdy obiektami, a jedynie zmiennymi.
Kerrek SB

19
Kompiluje się bez ostrzeżeń: int &x = *(int*)0;na gcc. Odwołanie może rzeczywiście wskazywać na NULL.
Calmarius,

20
referencja jest zmiennym aliasem
Khaled.K

20
Podoba mi się, że pierwsze zdanie jest całkowitym błędem. Odniesienia mają własną semantykę.
Wyścigi lekkości na orbicie

Odpowiedzi:


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

    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 zostać przypisane przy inicjalizacji:

    int x = 5;
    int y = 6;
    int &r = x;
  2. Wskaźnik ma swój własny adres i rozmiar pamięci na stosie (4 bajty na x86), podczas gdy odwołanie dzieli ten sam adres pamięci (z oryginalną zmienną), ale zajmuje również trochę miejsca na stosie. Ponieważ odwołanie ma ten sam adres, co sama zmienna oryginalna, można bezpiecznie traktować odwołanie jako inną nazwę dla tej samej zmiennej. Uwaga: Co może wskazywać wskaźnik na stosie lub stosie. To samo dotyczy Moim twierdzeniem w tym stwierdzeniu nie jest to, że wskaźnik musi wskazywać na stos. Wskaźnik to po prostu zmienna, która przechowuje adres pamięci. Ta zmienna znajduje się na stosie. Ponieważ odwołanie ma własną przestrzeń na stosie, a ponieważ adres jest taki sam jak zmienna, do której się odwołuje. Więcej o stosie vs kupie. 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 dodatkowy poziom pośrednictwa. Podczas gdy 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ć nullptrbezpośrednio, a odniesienia nie. Jeśli spróbujesz wystarczająco mocno i wiesz, jak to zrobić, możesz podać adres referencji nullptr. Podobnie, jeśli spróbujesz wystarczająco mocno, możesz mieć odniesienie do wskaźnika, a następnie to 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, i + 4przejść do piątego elementu. Nie ma znaczenia, jaki rozmiar ma obiekt, na który wskazuje wskaźnik.

  6. *Aby uzyskać dostęp do miejsca w pamięci, do którego wskazuje, należy oddzielić wskaźnik , natomiast odniesienia można użyć bezpośrednio. Wskaźnik do klasy / struct używa, ->aby uzyskać dostęp do jej członków, podczas gdy referencja używa ..

  7. Referencje nie mogą być umieszczane w tablicy, podczas gdy wskaźniki mogą być (wspomniane przez użytkownika @litb)

  8. Odnośniki Const mogą być powiązane z tymczasowymi. Wskaźniki nie mogą (nie bez pośrednictwa):

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

    Dzięki temu const&korzystanie z list argumentów jest bezpieczniejsze.


23
... ale dereferencje NULL są niezdefiniowane. Na przykład nie można sprawdzić, czy odwołanie ma wartość NULL (np. & Ref == NULL).
Pat Notz,

69
Liczba 2 nie jest prawdą. Odwołania nie są po prostu „inną nazwą dla tej samej zmiennej”. Odwołania mogą być przekazywane do funkcji, przechowywane w klasach itp. W sposób bardzo podobny do wskaźników. Istnieją niezależnie od zmiennych, na które wskazują.
Derek Park,

31
Brian, stos nie ma znaczenia. Odnośniki i wskaźniki nie muszą zajmować miejsca na stosie. Oba można przypisać do stosu.
Derek Park,

22
Brian, fakt, że zmienna (w tym przypadku wskaźnik lub odwołanie) wymaga spacji, nie oznacza, że ​​wymaga spacji na stosie. Wskaźniki i referencje mogą nie tylko wskazywać na stertę, ale mogą być faktycznie przypisane do sterty.
Derek Park,

38
inna ważna różnica: referencje nie mogą być umieszczone w tablicy
Johannes Schaub - litb

381

Co to jest odniesienie do C ++ ( dla programistów C )

Odniesienia może być traktowany jako stały wskaźnik (nie mylić ze wskaźnikiem do wartości stałej!) Z automatycznym zadnie, czyli będzie stosować kompilatora *operatora dla Ciebie.

Wszystkie odwołania muszą być inicjowane wartością inną niż null, w przeciwnym razie kompilacja się nie powiedzie. Nie jest możliwe uzyskanie adresu referencji - operator adresu zwróci adres referencyjnej wartości - nie jest też możliwe wykonanie arytmetyki na referencjach.

Programiści C mogą nie lubić odniesień do C ++, ponieważ nie będzie już oczywiste, kiedy nastąpi pośrednictwo lub jeśli argument zostanie przekazany przez wartość lub wskaźnik bez patrzenia na podpisy funkcji.

Programiści C ++ mogą nie lubić używania wskaźników, ponieważ są uważani za niebezpieczne - chociaż referencje nie są tak naprawdę bezpieczniejsze niż stałe wskaźniki, z wyjątkiem najbardziej trywialnych przypadków - nie mają wygody automatycznej pośredniczenia i mają inną konotację semantyczną.

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

Mimo że odwołanie jest często implementowane przy użyciu adresu w podstawowym języku asemblera, nie należy uważać odniesienia za zabawnie wyglądający wskaźnik do obiektu. Odwołanie to obiekt. Nie jest wskaźnikiem do obiektu ani kopią obiektu. To jest przedmiot.

Ale jeśli referencja naprawdę była obiektem, to jak mogłyby istnieć wiszące referencje? W językach niezarządzanych odniesienia nie mogą być „bezpieczniejsze” niż wskaźniki - na ogół po prostu nie ma sposobu na wiarygodne aliasy wartości ponad granicami zakresu!

Dlaczego uważam odniesienia C ++ za przydatne

Pochodzące z tła C referencje C ++ mogą wyglądać trochę głupio, ale nadal należy ich używać zamiast wskaźników, tam gdzie to możliwe: automatyczna pośrednia jest wygodna, a referencje stają się szczególnie przydatne, gdy mamy do czynienia z RAII - ale nie ze względu na postrzegane bezpieczeństwo zaletą, ale raczej dlatego, że sprawiają, że pisanie kodu idiomatycznego jest mniej niewygodne.

RAII jest jedną z głównych koncepcji C ++, ale współdziała w sposób niebanalny z kopiowaniem semantyki. Przekazywanie obiektów przez odniesienie pozwala uniknąć tych problemów, ponieważ nie jest wymagane kopiowanie. Jeśli referencje nie byłyby obecne w języku, należy zamiast tego użyć wskaźników, które są bardziej kłopotliwe w użyciu, naruszając w ten sposób zasadę projektowania języka, zgodnie z którą najlepsze praktyki powinny być łatwiejsze niż alternatywy.


17
@kriss: Nie, możesz również uzyskać wiszące odwołanie, zwracając zmienną automatyczną przez odniesienie.
Ben Voigt,

12
@kriss: Kompilator nie może wykryć w ogólnym przypadku. Rozważ funkcję składową, która zwraca odwołanie do zmiennej składowej klasy: jest to bezpieczne i nie powinno być zabronione przez kompilator. Następnie program wywołujący, który ma automatyczną instancję tej klasy, wywołuje tę funkcję członka i zwraca odwołanie. Presto: wiszące odniesienie. I tak, spowoduje kłopoty, @kriss: o to mi chodzi. Wiele osób twierdzi, że zaletą referencji nad wskaźnikami jest to, że referencje są zawsze ważne, ale tak nie jest.
Ben Voigt,

4
@kriss: Nie, odwołanie do obiektu o automatycznym czasie przechowywania różni się bardzo od obiektu tymczasowego. W każdym razie podałem tylko twój przykład, że możesz uzyskać nieprawidłowe odwołanie tylko poprzez odwołanie nieprawidłowego wskaźnika. Christoph ma rację - referencje nie są bezpieczniejsze niż wskaźniki, program, który używa wyłącznie referencji, może nadal naruszać bezpieczeństwo typu.
Ben Voigt,

7
Referencje nie są rodzajem wskaźnika. Są nową nazwą dla istniejącego obiektu.
catphive

18
@catphive: true, jeśli używasz semantyki języka, nie true, jeśli faktycznie spojrzysz na implementację; C ++ jest znacznie bardziej „magicznym” językiem niż C, a jeśli usuniesz magię z odniesień, otrzymasz wskaźnik
Christoph

190

Jeśli chcesz być naprawdę pedantyczny, możesz zrobić jedną rzecz z referencją, której nie możesz zrobić za pomocą wskaźnika: wydłużyć czas życia obiektu tymczasowego. W C ++, jeśli powiążesz stałe odniesienie z obiektem tymczasowym, 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 będący wynikiem konkatenacji. Natomiast s3_reference w istocie staje się obiektem tymczasowym. To tak naprawdę odniesienie do obiektu tymczasowego, który ma teraz taki sam okres istnienia jak odniesienie.

Jeśli spróbujesz tego bez constniego, kompilacja nie powiedzie się. Nie możesz powiązać nieistniejącego odwołania z obiektem tymczasowym, ani nie możesz wziąć jego adresu w tej sprawie.


5
ale jaki jest w tym przypadku przypadek użycia?
Ahmad Mushtaq

20
Cóż, s3_copy utworzy tymczasowy, a następnie skopiuje go do s3_copy, podczas gdy s3_reference używa bezpośrednio tymczasowego. Aby być naprawdę pedantycznym, musisz spojrzeć na Optymalizację wartości zwracanej, w której kompilator może pominąć konstrukcję kopiowania w pierwszym przypadku.
Matt Price

6
@digitalSurgeon: Magia jest dość potężna. Żywotność obiektu jest przedłużana przez fakt const &powiązania i tylko wtedy, gdy odwołanie wykracza poza zakres, wywoływany jest destruktor rzeczywistego typu referencyjnego (w porównaniu z typem referencyjnym, który może być bazą). Ponieważ jest to odniesienie, nie będzie między nimi krojenia.
David Rodríguez - dribeas

9
Aktualizacja do C ++ 11: ostatnie zdanie powinno brzmieć: „Nie można powiązać tymczasowego odwołania do wartości innej niż stała ”, ponieważ można powiązać tymczasowe odwołanie do wartości innej niż stała i ma to samo zachowanie przedłużające życie.
Oktalist

4
@AhmadMushtaq: Kluczowym zastosowaniem tego są klasy pochodne . Jeśli nie jest zaangażowane dziedziczenie, równie dobrze możesz użyć semantyki wartości, która będzie tania lub bezpłatna z powodu konstrukcji RVO / move. Ale jeśli masz Animal x = fast ? getHare() : getTortoise()następnie xzmierzy się klasyczny problem krojenia, gdy Animal& x = ...będzie działać prawidłowo.
Arthur Tacca

127

Oprócz cukru syntaktycznego odniesieniem jest constwskaźnik ( nie wskaźnik do a const). Musisz zadeklarować, do czego się odnosi, kiedy deklarujesz zmienną referencyjną, i nie możesz jej później zmienić.

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

Cel wskaźnika stałej może zostać zastąpiony przez pobranie jego adresu i użycie stałej rzutowania.

Celu odniesienia nie można zastąpić w żaden sposób bez UB.

Powinno to pozwolić kompilatorowi na lepszą optymalizację referencji.


8
Myślę, że jak dotąd jest to najlepsza odpowiedź. Inni mówią o referencjach i wskazówkach, jakby byli różnymi zwierzętami, a następnie wyjaśniają, jak różnią się zachowaniem. Nie ułatwia to imho. Zawsze rozumiałem odniesienia jako T* constz innym składniowym cukrem (co zdarza się, aby wyeliminować wiele * i & z twojego kodu).
Carlo Wood,

2
„Cel wskaźnika stałej może zostać zastąpiony przez pobranie jego adresu i użycie stałej rzutowania”. Takie postępowanie jest nieokreślonym zachowaniem. Szczegółowe informacje można znaleźć na stronie stackoverflow.com/questions/25209838/ ...
dgnuff

1
Próba zmiany odniesienia do referencji lub wartości wskaźnika stałej (lub dowolnego stałego skalara) jest równa. Co możesz zrobić: usunąć kwalifikację const, która została dodana przez niejawną konwersję: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);jest OK.
ciekawy

1
Różnica tutaj jest UB w porównaniu z dosłownie niemożliwym. W C ++ nie ma składni, która pozwoliłaby ci zmienić punkty odniesienia.

Nie jest to niemożliwe, trudniej, możesz po prostu uzyskać dostęp do obszaru pamięci wskaźnika modelującego to odniesienie i zmienić jego zawartość. Z pewnością można to zrobić.
Nicolas Bousquet,

125

W przeciwieństwie do popularnej opinii, możliwe jest, aby mieć referencję NULL.

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

To prawda, o wiele trudniej jest zrobić z referencją - ale jeśli sobie z tym poradzisz, oderwiesz włosy, próbując je znaleźć. Referencje nie są z natury bezpieczne w C ++!

Technicznie jest to nieprawidłowe odwołanie , a nie odwołanie zerowe. C ++ nie obsługuje pojęć zerowych jako koncepcji, którą można znaleźć w innych językach. Istnieją również inne rodzaje nieprawidłowych odniesień. Wszelkie nieprawidłowe odwołania wywołują spektrum niezdefiniowanych zachowań , tak jak zrobiłby to niewłaściwy wskaźnik.

Rzeczywisty błąd polega na dereferencji wskaźnika NULL przed przypisaniem do odwołania. Ale nie znam żadnych kompilatorów, które generowałyby błędy w tym stanie - błąd rozprzestrzenia się do punktu w dalszej części kodu. Właśnie dlatego ten problem jest tak podstępny. Przez większość czasu, jeśli odrzucisz wskaźnik NULL, rozbijasz się w tym miejscu i nie trzeba dużo debugowania, aby go zrozumieć.

Mój przykład powyżej jest krótki i wymyślony. Oto bardziej realistyczny przykład.

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

Chciałbym powtórzyć, że jedynym sposobem na uzyskanie odwołania zerowego jest użycie zniekształconego kodu, a gdy już go znajdziesz, zachowanie jest niezdefiniowane. Sprawdzanie, czy istnieje odwołanie zerowe, nigdy nie ma sensu; na przykład możesz spróbować, if(&bar==NULL)...ale kompilator może zoptymalizować nieistniejącą instrukcję! Prawidłowe odwołanie nigdy nie może mieć wartości NULL, więc z punktu widzenia kompilatora porównanie jest zawsze fałszywe i można dowolnie usunąć ifklauzulę jako martwy kod - jest to istota nieokreślonego zachowania.

Właściwym sposobem na uniknięcie problemów jest uniknięcie dereferencji wskaźnika NULL w celu utworzenia odwołania. Oto zautomatyzowany sposób na osiągnięcie 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 zobaczyć starsze spojrzenie na ten problem od kogoś z lepszymi umiejętnościami pisania, zobacz Null References od Jima Hyslopa i Herb Suttera.

Inny przykład niebezpieczeństw związanych z dereferencją wskaźnika zerowego znajduje się w sekcji Ujawnianie niezdefiniowanego zachowania podczas próby przeniesienia kodu na inną platformę przez Raymonda Chena.


63
Kod, o którym mowa, zawiera niezdefiniowane zachowanie. Technicznie rzecz biorąc, nie możesz nic zrobić ze wskaźnikiem zerowym, z wyjątkiem ustawienia go i porównania. Gdy twój program wywoła nieokreślone zachowanie, może zrobić wszystko, w tym działać poprawnie, dopóki nie udostępnisz wersji demonstracyjnej dużemu szefowi.
KeithB

9
znak ma poprawny argument. argument, że wskaźnik może mieć wartość NULL i dlatego należy go sprawdzić, również nie jest prawdziwy: jeśli powiesz, że funkcja wymaga wartości innej niż NULL, wywołujący musi to zrobić. więc jeśli dzwoniący nie wywołuje niezdefiniowanego zachowania. tak jak Mark zrobił ze złym odniesieniem
Johannes Schaub - litb

13
Opis jest błędny. Ten kod może, ale nie musi, tworzyć odwołanie o wartości NULL. Jego zachowanie jest niezdefiniowane. Może to stanowić całkowicie poprawne odniesienie. Utworzenie jakiegokolwiek odniesienia może się nie powieść.
David Schwartz,

7
@David Schwartz, gdybym mówił o sposobie, w jaki wszystko musiało działać zgodnie ze standardem, miałbyś rację. Ale nie o tym mówię - mówię o obserwowanym zachowaniu za pomocą bardzo popularnego kompilatora i ekstrapolacji na podstawie mojej wiedzy o typowych kompilatorach i architekturze procesora do tego, co prawdopodobnie się wydarzy. Jeśli uważasz, że referencje są lepsze od wskaźników, ponieważ są bezpieczniejsze i nie uważasz, że referencje mogą być złe, pewnego dnia napotkasz prosty problem, tak jak ja.
Mark Ransom

6
Dereferencje wskaźnika zerowego są nieprawidłowe. Każdy program, który to robi, nawet w celu zainicjowania odwołania, jest niepoprawny. Jeśli inicjujesz odwołanie za pomocą wskaźnika, zawsze powinieneś sprawdzić, czy wskaźnik jest poprawny. Nawet jeśli to się powiedzie, obiekt bazowy może zostać usunięty w dowolnym momencie, pozostawiając odniesienie do nieistniejącego obiektu, prawda? To, co mówisz, to dobre rzeczy. Myślę, że prawdziwym problemem jest to, że referencja NIE musi być sprawdzana pod kątem „zerowania”, kiedy widzisz jeden, a wskaźnik powinien być co najmniej potwierdzony.
t0rakka

114

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

dostęp członków za pomocą wskaźników używa ->
dostępu członków za pomocą odwołań.

foo.barjest wyraźnie lepszy od foo->bartego samego, co vi jest wyraźnie lepszy od Emacsa :-)


4
@Orion Edwards> dostęp do członków za pomocą wskaźników ->> dostęp do członków za pomocą referencji. To nie jest w 100% prawda. Możesz mieć odniesienie do wskaźnika. W takim przypadku uzyskałbyś dostęp do członków odsuniętego wskaźnika za pomocą -> struct Node {Node * next; }; Najpierw węzeł *; // p jest odwołaniem do wskaźnika void foo (Node * & p) {p-> next = first; } Węzeł * bar = nowy węzeł; foo (bar); - OP: Czy znasz pojęcia wartości i wartości lv?

3
Inteligentne wskaźniki mają jedno i drugie. (metody na inteligentnej klasie wskaźnika) i -> (metody na typie bazowym).
JBRWilkinson

1
@ user6105 Oświadczenie Oriona Edwardsa jest w 100% prawdziwe. „dostęp do elementów usuniętego wskaźnika [wskaźnik] Wskaźnik nie ma żadnych elementów. Obiekt, do którego odnosi się wskaźnik, ma elementy, a dostęp do nich jest dokładnie tym, co ->zapewnia odniesienia do wskaźników, tak jak w przypadku samego wskaźnika.
Max Truxa

1
dlaczego tak jest .i ->ma to coś wspólnego z vi i emacsem :)
artm

10
@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 lepszy, a inni myślą dokładnie odwrotnie. Podobnie myślę, że używanie .jest lepsze niż używanie ->, ale podobnie jak vi vs emacs, jest całkowicie subiektywne i nie możesz niczego udowodnić
Orion Edwards

73

Referencje są bardzo podobne do wskaźników, ale zostały specjalnie opracowane, aby pomóc w optymalizacji kompilatorów.

  • Odnośniki są zaprojektowane w taki sposób, że kompilatorowi znacznie łatwiej jest prześledzić, które odwołania aliasy, które zmienne. Bardzo ważne są dwie główne cechy: brak „arytmetyki referencyjnej” i brak ponownego przypisywania referencji. Umożliwiają one kompilatorowi ustalenie, który odnośnik alias, które zmienne w czasie kompilacji.
  • Odwołania mogą odnosić się do zmiennych, które nie mają adresów pamięci, takich jak te, które kompilator wybiera umieszczać w rejestrach. Jeśli weźmiesz adres zmiennej lokalnej, kompilatorowi bardzo trudno jest umieścić ją 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 optymalizujący może zdać sobie sprawę, że uzyskujemy dostęp do [0] i [1] całkiem sporego grona. Chciałbym zoptymalizować algorytm, aby:

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 mniejsze niż 2, więc tablica [i] nigdy nie może odnosić się do tablicy [1]. możeModify () otrzymuje a0 jako odniesienie (tablica aliasingu [0]). Ponieważ nie ma arytmetyki „referencyjnej”, kompilator musi tylko udowodnić, że być może Modify nigdy nie otrzymuje adresu x, i udowodnił, że nic nie zmienia tablicy [1].

Musi również udowodnić, że nie ma możliwości, aby przyszłe połączenie mogło odczytać / napisać [0], podczas gdy mamy tymczasową kopię rejestru w a0. Jest to często trywialne do udowodnienia, ponieważ w wielu przypadkach jest oczywiste, że odwołanie nigdy nie jest przechowywane w stał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 o wiele trudniej jest udowodnić, że być może Modify nigdy nie modyfikuje tablicy [1], ponieważ już jej nadaliśmy wskaźnik; kot wyszedł z torby. Teraz musi wykonać znacznie trudniejszy dowód: statyczną analizę możeModify, aby udowodnić, że nigdy nie zapisuje do & x + 1. Musi także udowodnić, że nigdy nie oszczędza wskaźnika, który może odnosić się do tablicy [0], która jest po prostu jak trudne.

Nowoczesne kompilatory są coraz lepsze w analizie statycznej, ale zawsze miło jest im pomóc i korzystać z referencji.

Oczywiście, pomijając takie sprytne optymalizacje, kompilatory rzeczywiście zamienią odniesienia w wskaźniki w razie potrzeby.

EDYCJA: Pięć lat po opublikowaniu tej odpowiedzi znalazłem faktyczną różnicę techniczną, w której odniesienia są inne niż tylko inny sposób patrzenia na tę samą koncepcję adresowania. Odnośniki mogą modyfikować żywotność obiektów tymczasowych w sposób, którego nie potrafią wskaźniki.

F createF(int argument);

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

Zwykle obiekty tymczasowe, takie jak ten utworzony przez wywołanie, createF(5)są niszczone na końcu wyrażenia. Jednak poprzez powiązanie tego obiektu z odwołaniem, refC ++ wydłuży czas życia tego obiektu tymczasowego, dopóki nie refwyjdzie poza zakres.


To prawda, że ​​ciało musi być widoczne. Jednak ustalenie, że maybeModifynie przyjmuje adresu niczego związanego z, xjest znacznie łatwiejsze niż udowodnienie, że nie występuje wiązka arytmetyki wskaźnika.
Cort Ammon,

Wierzę, że optymalizator już to robi, że „nie występuje kilka arytmetyki wskaźników” z wielu innych powodów.
Ben Voigt,

„Referencje są bardzo podobne do wskaźników” - semantycznie, w odpowiednich kontekstach - ale pod względem generowanego kodu, tylko w niektórych implementacjach, a nie poprzez jakąkolwiek definicję / wymaganie. Wiem, że zwróciłeś na to uwagę, i nie zgadzam się z żadnym z twoich postów pod względem praktycznym, ale mamy już za dużo problemów z czytaniem przez ludzi zbyt wielu skrótowych opisów, takich jak „referencje są jak / zwykle implementowane jako wskaźniki” .
underscore_d

Mam wrażenie, że ktoś błędnie oflagował jako nieaktualny komentarz zgodny z tym void maybeModify(int& x) { 1[&x]++; }, co omawiają pozostałe komentarze
Ben Voigt

67

W rzeczywistości odwołanie nie jest tak naprawdę wskaźnikiem.

Kompilator przechowuje „odniesienia” do zmiennych, kojarząc nazwę z adresem pamięci; to jego zadaniem jest tłumaczenie dowolnej nazwy zmiennej na adres pamięci podczas kompilacji.

Podczas tworzenia odwołania informujesz kompilator, że przypisujesz inną nazwę do zmiennej wskaźnika; dlatego referencje nie mogą „wskazywać na zero”, ponieważ zmienna nie może być i nie może być.

Wskaźniki są zmiennymi; zawierają adres innej zmiennej lub mogą być zerowe. Ważne jest to, że wskaźnik ma wartość, podczas gdy odwołanie 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 o wartości a. Pamięć ta ma teraz dwa nazwiska, aa b, a to może być adresowane przy użyciu nazwy.

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

int a;
increment(a);

Podczas wywoływania funkcji kompilator zwykle generuje przestrzenie pamięci dla argumentów, które mają zostać skopiowane. Podpis funkcji definiuje spacje, które należy utworzyć, i podaje nazwę, która powinna być użyta dla tych spacji. Zadeklarowanie parametru jako odwołania po prostu mówi kompilatorowi, aby używał wejściowej przestrzeni pamięci zmiennej zamiast przydzielać nową przestrzeń pamięci podczas wywołania metody. Może wydawać się dziwne stwierdzenie, że twoja funkcja będzie bezpośrednio manipulować zmienną zadeklarowaną w zakresie wywołującym, ale pamiętaj, że podczas wykonywania skompilowanego kodu nie ma już więcej zakresu; jest po prostu płaska pamięć, a kod funkcji może manipulować dowolnymi zmiennymi.

Teraz mogą występować przypadki, w których kompilator może nie być w stanie poznać odwołania podczas kompilacji, na przykład podczas korzystania ze zmiennej zewnętrznej. Odwołanie może, ale nie musi być zaimplementowane jako wskaźnik w podstawowym kodzie. Ale w przykładach, które ci podałem, najprawdopodobniej nie zostanie zaimplementowany ze wskaźnikiem.


2
Odwołanie jest odniesieniem do wartości l, niekoniecznie do zmiennej. Z tego powodu jest znacznie bliżej wskaźnika niż prawdziwego aliasu (konstrukcja czasu kompilacji). Przykłady wyrażeń, do których można się odwoływać, to * p lub nawet * p ++

5
Właśnie wskazałem fakt, że odwołanie nie zawsze może wypchnąć nową zmienną na stos, tak jak nowy wskaźnik.
Vincent Robert

1
@VincentRobert: Będzie działał tak samo jak wskaźnik ... jeśli funkcja jest wstawiona, zarówno odniesienie, jak i wskaźnik zostaną zoptymalizowane. W przypadku wywołania funkcji adres obiektu będzie musiał zostać przekazany do funkcji.
Ben Voigt,

1
int * p = NULL; int & r = * p; odniesienie wskazujące na NULL; if (r) {} -> boOm;)
sree

2
Skupienie się na etapie kompilacji wydaje się przyjemne, dopóki nie przypomnisz sobie, że referencje mogą być przekazywane w czasie wykonywania, w którym to momencie statyczne aliasing wychodzi z okna. (A następnie referencje są zwykle implementowane jako wskaźniki, ale standard nie wymaga tej metody.)
underscore_d

44

Referencja nigdy nie może być NULL.


10
Zobacz odpowiedź Marka Ransoma na kontrprzykład. Jest to najczęściej potwierdzany mit o referencjach, ale jest mitem. Jedyną gwarancją, jaką masz według standardu, jest to, że natychmiast masz UB, gdy masz odwołanie NULL. Ale to jest podobne do powiedzenia: „Ten samochód jest bezpieczny, nigdy nie zejdzie z drogi. (Nie ponosimy żadnej odpowiedzialności za to, co może się zdarzyć, jeśli i tak
zjedziesz

17
@cmaster: W prawidłowym programie odwołanie nie może mieć wartości NULL. Ale wskaźnik może. To nie jest mit, to fakt.
user541686,

8
@ Mehrdad Tak, ważne programy pozostają na drodze. Ale nie ma bariery komunikacyjnej, która wymusiłaby rzeczywisty program. Na dużych odcinkach drogi brakuje oznaczeń. Dlatego bardzo łatwo zejść z drogi w nocy. Ma to kluczowe znaczenie dla debugowania takich błędów, o których wiesz, że może się to zdarzyć: odwołanie zerowe może się rozprzestrzeniać, zanim spowoduje awarię programu, podobnie jak wskaźnik zerowy. A kiedy to robi, masz taki kod void Foo::bar() { virtual_baz(); }segfaults. Jeśli nie wiesz, że odwołania mogą mieć wartość NULL, nie możesz prześledzić wartości NULL z powrotem do jej początku.
cmaster

4
int * p = NULL; int & r = * p; odniesienie wskazujące na NULL; if (r) {} -> boOm;) -
sree

10
@sree int &r=*p;to niezdefiniowane zachowanie. W tym momencie nie masz „odniesienia do NULL”, masz program, którego nie można już w ogóle uzasadnić .
cdhowie

34

Chociaż zarówno odniesienia, jak i wskaźniki służą do pośredniego dostępu do innej wartości, istnieją dwie ważne różnice między odniesieniami i wskaźnikami. Po pierwsze, odwołanie zawsze odnosi się do obiektu: Błędem jest zdefiniowanie odwołania bez jego inicjalizacji. Zachowanie przypisania jest drugą ważną różnicą: przypisanie do odwołania zmienia obiekt, z którym jest powiązane; nie wiąże ponownie odwołania do innego obiektu. Po zainicjowaniu odwołanie zawsze odnosi się do tego samego obiektu podstawowego.

Rozważ te dwa fragmenty programu. W pierwszym przypisujemy jeden wskaźnik do drugiego:

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

Po przypisaniu ival obiekt adresowany przez pi pozostaje niezmieniony. Przypisanie zmienia wartość pi, powodując, że wskazuje inny obiekt. Rozważmy teraz podobny program, który przypisuje dwa odwołania:

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

To przypisanie zmienia wartość ival, wartość, do której odwołuje się ri, a nie sama referencja. Po przypisaniu dwa odniesienia nadal odnoszą się do ich oryginalnych obiektów, a ich wartość jest teraz taka sama.


„odniesienie zawsze odnosi się do obiektu” jest po prostu całkowicie fałszywe
Ben Voigt

31

Istnieje semantyczna różnica, która może wydawać się ezoteryczna, jeśli nie znasz języków komputerowych w sposób abstrakcyjny, a nawet akademicki.

Na najwyższym poziomie referencje polegają na tym, że są to przezroczyste „aliasy”. Twój komputer może użyć adresu, aby działały, ale nie powinieneś się tym przejmować: powinieneś myśleć o nich jako o „innej nazwie” dla istniejącego obiektu, a składnia to odzwierciedla. Są bardziej rygorystyczne niż wskaźniki, więc twój kompilator może bardziej niezawodnie ostrzec cię, gdy masz zamiar utworzyć wiszące odniesienie, niż kiedy masz zamiar stworzyć wiszący wskaźnik.

Poza tym istnieją oczywiście praktyczne różnice między wskaźnikami i odniesieniami. Składnia do ich użycia jest oczywiście inna i nie można „ponownie umieszczać” odniesień, mieć odniesień do nicości ani wskaźników do odniesień.


26

Odwołanie jest aliasem innej zmiennej, podczas gdy wskaźnik przechowuje adres pamięci zmiennej. Odwołania są zwykle używane jako parametry funkcji, dzięki czemu przekazywanym obiektem nie jest kopia, ale sam obiekt.

    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. 

19

Nie ma znaczenia, ile miejsca to zajmie, ponieważ w rzeczywistości nie widać żadnego efektu ubocznego (bez wykonania kodu) zajmowanego miejsca.

Z drugiej strony, jedną z głównych różnic między referencjami i wskaźnikami jest to, że tymczasowe przypisania do stałych odwołań działają, dopóki stałe odniesienie nie wyjdzie poza zakres.

Na przykład:

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

...

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

wydrukuje:

in scope
scope_test done!

Jest to mechanizm językowy, który umożliwia działanie ScopeGuard.


1
Nie możesz wziąć adresu referencji, ale to nie znaczy, że fizycznie nie zajmują miejsca. Z wyjątkiem optymalizacji, z pewnością mogą.
Lekkość ściga się na orbicie

2
Niezależnie od wpływu, „Odwołanie na stosie w ogóle nie zajmuje miejsca” jest ewidentnie fałszywe.
Lekkość ściga się na orbicie

1
@ Tomalak, cóż, to zależy również od kompilatora. Ale tak, powiedzenie, że jest to trochę mylące. Przypuszczam, że usunięcie tego byłoby mniej mylące.
MSN

1
W każdym konkretnym przypadku może, ale nie musi. Zatem „nie”, ponieważ kategoryczne twierdzenie jest błędne. Tak mówię. :) [Nie pamiętam, co mówi standard na ten temat; zasady członków odniesienia mogą nadawać ogólną zasadę „odniesienia mogą zajmować miejsce”, ale nie mam ze sobą mojej kopii tego standardu na plaży: D]
Lekkość na orbicie

19

Jest to oparte na samouczku . To, co jest napisane, wyjaśnia:

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

Po prostu o tym pamiętać,

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

Co więcej, ponieważ możemy odwoływać się do niemal każdego samouczka dotyczącego wskaźnika, wskaźnik jest obiektem obsługiwanym przez arytmetykę wskaźnika, która upodabnia wskaźnik do tablicy.

Spójrz na następujące oświadczenie:

int Tom(0);
int & alias_Tom = Tom;

alias_Tommożna rozumieć jako alias of a variable(inny z typedef, który jest alias of a type) Tom. Można również zapomnieć o terminologii takiego stwierdzenia, aby utworzyć odniesienie do Tom.


1
A jeśli klasa ma zmienną referencyjną, należy ją zainicjować za pomocą wartości nullptr lub prawidłowego obiektu na liście inicjalizacji.
Misgevolution,

1
Sformułowanie w tej odpowiedzi jest zbyt mylące, aby mogło być bardzo przydatne. Ponadto, @Misgevolution, czy poważnie polecasz czytelnikom zainicjowanie odwołania za pomocą nullptr? Czy przeczytałeś już jakąkolwiek inną część tego wątku lub ...?
underscore_d

1
Przykro mi, przepraszam za tę głupią rzecz, którą powiedziałem. Do tego czasu musiałem być pozbawiony snu. „Inicjalizacja za pomocą nullptr” jest całkowicie błędna.
Misgevolution,

18

Odwołanie nie jest inną nazwą nadaną pamięci. Jest niezmiennym wskaźnikiem, który jest automatycznie usuwany z odniesienia przy użyciu. Zasadniczo sprowadza się do:

int& j = i;

Staje się wewnętrznie

int* const j = &i;

13
Nie tak mówi standard C ++ i kompilator nie musi implementować referencji w sposób opisany w odpowiedzi.
jogojapan

@jogojapan: Dowolny sposób, w jaki kompilator C ++ może zaimplementować odwołanie, jest również prawidłowym sposobem zaimplementowania constwskaźnika. Ta elastyczność nie dowodzi, że istnieje różnica między odniesieniem a wskaźnikiem.
Ben Voigt,

2
@BenVoigt Może prawdą jest, że każda poprawna implementacja jednej jest również poprawną implementacją drugiej, ale nie wynika to w oczywisty sposób z definicji tych dwóch pojęć. Dobra odpowiedź zacznie się od definicji i pokaże, dlaczego twierdzenie o tym, że oba są ostatecznie takie same, jest prawdziwe. Ta odpowiedź wydaje się być komentarzem do niektórych innych odpowiedzi.
jogojapan

Odwołanie to inna nazwa nadana obiektowi. Kompilator może mieć dowolną implementację, o ile nie można odróżnić, nazywa się to zasadą „jak gdyby”. Ważną częścią jest to, że nie można odróżnić. Jeśli zauważysz, że wskaźnik nie ma pamięci, kompilator jest w błędzie. Jeśli okaże się, że odwołanie nie ma pamięci, kompilator nadal jest zgodny.
sp2danny,

17

Bezpośrednia odpowiedź

Co to jest odwołanie w C ++? Niektóre konkretne wystąpienia typu, który nie jest typem obiektu .

Co to jest wskaźnik w C ++? Niektóre konkretne wystąpienie typu, które jest typem obiektu .

Z definicji typu obiektu ISO C ++ :

Obiekt typu jest (ewentualnie cv -qualified) typ, który nie jest typem funkcji, a nie rodzaj odniesienia, a nie cv nieważne.

Warto wiedzieć, że typ obiektu jest najwyższą kategorią wszechświata typów w C ++. Odniesienie jest również kategorią najwyższego poziomu. Ale wskaźnik nie jest.

Wskaźniki i odniesienia są wymienione razem w kontekście typu związku . Wynika to zasadniczo z natury składni deklaratora odziedziczonej z (i rozszerzonej) C, która nie ma odwołań. (Poza tym istnieje więcej niż jeden rodzaj deklaratora odniesień od C ++ 11, podczas gdy wskaźniki są nadal „unityped”: &+ &&vs *..) Tak więc napisanie języka specyficznego dla „rozszerzenia” o podobnym stylu C w tym kontekście jest dość rozsądne . (Będę nadal twierdzą, że składnia declarators odpadów syntaktyczna wyrazistość dużo , sprawia, że zarówno użytkownicy człowieka i implementacje frustrujące. W ten sposób, wszystkie z nich nie są kwalifikacje, aby być wbudowane ww nowym projekcie językowym. Jest to jednak zupełnie inny temat dotyczący projektowania PL).

W przeciwnym razie nie ma znaczenia, że ​​wskaźniki można zakwalifikować jako określone rodzaje typów razem z odnośnikami. Po prostu mają zbyt mało wspólnych właściwości poza podobieństwem składni, więc w większości przypadków nie ma potrzeby ich łączenia.

Zwróć uwagę, że powyższe stwierdzenia wymieniają tylko typy „wskaźników” i „referencji”. Istnieją pewne pytania dotyczące ich instancji (np. Zmienne). Pojawia się też zbyt wiele nieporozumień.

Różnice kategorii najwyższego poziomu mogą już ujawnić wiele konkretnych różnic niezwiązanych bezpośrednio ze wskaźnikami:

  • Typy obiektów mogą mieć cvkwalifikatory najwyższego poziomu . Referencje nie mogą.
  • Zmienne typy obiektów zajmują pamięć zgodnie z semantyką abstrakcyjnej maszyny . Odnośniki nie muszą zajmować miejsca do przechowywania (szczegółowe informacje znajdują się w sekcji dotyczącej nieporozumień).
  • ...

Kilka dodatkowych zasad specjalnych dotyczących referencji:

  • Deklaratory złożone są bardziej restrykcyjne w odniesieniu do odniesień.
  • Referencje mogą się zwinąć .
    • Specjalne reguły dotyczące &&parametrów (jako „referencje przekazywania”) oparte na zwijaniu referencji podczas odejmowania parametrów szablonu pozwalają na „idealne przekazywanie” parametrów.
  • Referencje mają specjalne zasady inicjowania. Żywotność zmiennej zadeklarowanej jako typ odniesienia może różnić się od zwykłych obiektów poprzez rozszerzenie.
    • BTW, kilka innych kontekstów, takich jak inicjalizacja, podlega std::initializer_listpodobnym zasadom przedłużania czasu życia odniesienia. To kolejna puszka robaków.
  • ...

Błędne przekonania

Cukier syntaktyczny

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

Technicznie jest to po prostu błędne. Odnośniki nie są składniowym cukrem żadnych innych funkcji w C ++, ponieważ nie można ich dokładnie zastąpić innymi cechami bez różnic semantycznych.

(Podobnie wyrażenia lambda nie są cukrem syntaktycznym żadnych innych funkcji w C ++, ponieważ nie można go dokładnie symulować za pomocą „nieokreślonych” właściwości, takich jak kolejność deklaracji przechwyconych zmiennych , co może być ważne, ponieważ kolejność inicjalizacji takich zmiennych może być znaczący.)

C ++ ma tylko kilka rodzajów cukrów składniowych w tym ścisłym znaczeniu. Jednym z przykładów jest (odziedziczony z C) wbudowany (nie przeciążony) operator [], który jest dokładnie zdefiniowany, mając dokładnie takie same właściwości semantyczne określonych form kombinacji w porównaniu z wbudowanym operatorem jedno- *i dwójkowym+ .

Przechowywanie

Zarówno wskaźnik, jak i odwołanie wykorzystują tę samą ilość pamięci.

Powyższe stwierdzenie jest po prostu błędne. Aby uniknąć takich nieporozumień, spójrz zamiast tego na reguły ISO C ++:

Z [intro.object] / 1 :

... Obiekt zajmuje region przechowywania w okresie budowy, przez cały okres jego życia i w okresie zniszczenia. ...

Od [dcl.ref] / 4 :

Nie jest określone, czy odniesienie wymaga przechowywania.

Uwaga: są to właściwości semantyczne .

Pragmatyka

Nawet jeśli wskaźniki nie są wystarczająco wykwalifikowane, aby połączyć je z odwołaniami w sensie projektu języka, wciąż istnieją pewne argumenty, które sprawiają, że dyskusyjne jest dokonywanie wyboru między nimi w innych kontekstach, na przykład przy dokonywaniu wyborów typów parametrów.

Ale to nie jest cała historia. Mam na myśli, że jest więcej rzeczy niż wskaźników niż referencji, które należy wziąć pod uwagę.

Jeśli nie musisz trzymać się zbyt szczegółowych wyborów, w większości przypadków odpowiedź jest krótka: nie musisz używać wskaźników, więc nie musisz . Wskaźniki są zwykle wystarczająco złe, ponieważ implikują zbyt wiele rzeczy, których się nie spodziewasz, i będą opierać się na zbyt wielu domniemanych założeniach podważających łatwość konserwacji i (nawet) przenośność kodu. Niepotrzebne poleganie na wskaźnikach jest zdecydowanie złym stylem i należy go unikać w sensie współczesnego C ++. Ponownie rozważ swój cel, a w końcu okaże się, że wskaźnik jest cechą ostatnich rodzajów w większości przypadków.

  • Czasami reguły językowe wyraźnie wymagają użycia określonych typów. Jeśli chcesz korzystać z tych funkcji, przestrzegaj zasad.
    • Konstruktory kopiujące wymagają określonych typów cv - &typ odniesienia jako pierwszy typ parametru. (I zwykle powinien być constkwalifikowany.)
    • Konstruktory ruchów wymagają określonych typów cv - &&typ odniesienia jako pierwszy typ parametru. (I zwykle nie powinno być żadnych kwalifikacji.)
    • Szczególne przeciążenia operatorów wymagają typów referencyjnych lub niereferencyjnych. Na przykład:
      • Przeciążenie operator=jako specjalne funkcje składowe wymaga typów referencyjnych podobnych do 1. parametru konstruktorów kopiuj / przenieś.
      • Postfiks ++wymaga manekina int.
      • ...
  • Jeśli wiesz, że przekazanie przez wartość (tj. Użycie typów innych niż referencyjne) jest wystarczające, użyj go bezpośrednio, szczególnie w przypadku implementacji obsługującej wymuszanie kopii w C ++ 17. ( Ostrzeżenie : jednak wyczerpujące uzasadnienie konieczności może być bardzo skomplikowane ).
  • Jeśli chcesz obsługiwać niektóre uchwyty z własnością, używaj inteligentnych wskaźników, takich jak unique_ptri shared_ptr(lub nawet domowych, jeśli potrzebujesz, aby były nieprzezroczyste ), zamiast surowych wskaźników.
  • Jeśli wykonujesz kilka iteracji w zakresie, użyj iteratorów (lub niektórych zakresów, które nie są jeszcze dostarczane przez bibliotekę standardową), zamiast surowych wskaźników, chyba że jesteś przekonany, że surowe wskaźniki będą działać lepiej (np. W przypadku mniejszych zależności nagłówka) w ściśle określonych skrzynie
  • Jeśli wiesz, że przekazanie przez wartość jest wystarczające i potrzebujesz wyraźnej nullownej semantyki, użyj opakowujących std::optionalzamiast surowych wskaźników.
  • Jeśli wiesz, że przekazanie przez wartość nie jest idealne z powyższych powodów i nie chcesz semantyki zerowalnej, użyj opcji {lvalue, rvalue, forwarding}.
  • Nawet jeśli potrzebujesz semantyki, takiej jak tradycyjny wskaźnik, często jest coś bardziej odpowiedniego, na przykład observer_ptrw Library Fundamental TS.

Jedynych wyjątków nie można obejść w bieżącym języku:

  • Podczas implementowania inteligentnych wskaźników powyżej możesz mieć do czynienia z surowymi wskaźnikami.
  • Określone procedury współpracy językowej wymagają wskaźników, takich jak operator new. (Jednak cv - void*jest nadal zupełnie inny i bezpieczniejszy w porównaniu ze zwykłymi wskaźnikami obiektów, ponieważ wyklucza nieoczekiwaną arytmetykę wskaźników, chyba że polegasz na jakimś niezgodnym rozszerzeniu na void*podobnych GNU.)
  • Wskaźniki funkcji można konwertować z wyrażeń lambda bez przechwytywania, podczas gdy odwołania do funkcji nie. W takich przypadkach musisz używać wskaźników funkcji w kodzie nieogólnym, nawet celowo nie chcesz wartości zerowych.

Tak więc w praktyce odpowiedź jest tak oczywista: w razie wątpliwości należy unikać wskazówek . Wskaźników musisz używać tylko wtedy, gdy istnieją bardzo wyraźne powody, dla których nic innego nie jest bardziej odpowiednie. Z wyjątkiem kilku wyjątkowych przypadków wspomnianych powyżej, takie wybory prawie zawsze nie są wyłącznie specyficzne dla C ++ (ale prawdopodobnie będą specyficzne dla implementacji języka). Takimi instancjami mogą być:

  • Musisz obsługiwać interfejsy API w starym stylu (C).
  • Musisz spełnić wymagania ABI określonych implementacji C ++.
  • W czasie wykonywania należy współdziałać z różnymi implementacjami językowymi (w tym różnymi zestawami, językowym środowiskiem uruchomieniowym i FFI niektórych języków klienta wysokiego poziomu) w oparciu o założenia konkretnych implementacji.
  • W niektórych ekstremalnych przypadkach musisz poprawić efektywność tłumaczenia (kompilacja i linkowanie).
  • W niektórych ekstremalnych przypadkach musisz unikać wzdęcia.

Ostrzeżenia dotyczące neutralności językowej

Jeśli zobaczysz pytanie za pomocą wyników wyszukiwania Google (nie dotyczy C ++) , prawdopodobnie jest to niewłaściwe miejsce.

Zawarte w C ++ jest dość „dziwne”, gdyż nie jest w zasadzie pierwszej klasy: będą one traktowane jak obiekty lub funkcje skierowania do tak nie mają szans na poparcie niektórych operacji pierwszej klasy jak jest lewy argument z operator dostępu do elementu niezależnie od rodzaju obiektu, do którego się odnosi. Inne języki mogą, ale nie muszą, mieć podobne ograniczenia dotyczące swoich odniesień.

Odwołania w C ++ prawdopodobnie nie zachowają znaczenia w różnych językach. Na przykład odwołania w ogóle nie implikują właściwości o wartościach pustych dla wartości takich jak w C ++, więc takie założenia mogą nie działać w niektórych innych językach (i można łatwo znaleźć kontrprzykłady, np. Java, C #, ...).

Wciąż mogą istnieć pewne wspólne właściwości wśród odniesień w różnych językach programowania, ale zostawmy to na inne pytania w SO.

(Uwaga dodatkowa: pytanie może być znaczące wcześniej niż w przypadku języków „podobnych do C”, takich jak ALGOL 68 vs. PL / I. )


16

Odwołanie do wskaźnika jest możliwe w C ++, ale odwrotność nie jest możliwa, oznacza to, że wskaźnik do odniesienia nie jest możliwy. Odwołanie do wskaźnika zapewnia czystszą składnię do modyfikowania wskaźnika. Spójrz na ten przykład:

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

Rozważ wersję C powyższego programu. W C musisz użyć wskaźnika do wskaźnika (wielokrotna pośrednia), co prowadzi do zamieszania, a program może wyglądać na skomplikowany.

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

Odwiedź następujące informacje, aby uzyskać więcej informacji na temat odniesienia do wskaźnika:

Jak powiedziałem, wskaźnik do odwołania nie jest możliwy. Wypróbuj następujący program:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

15

Używam referencji, chyba że potrzebuję jednego z tych:

  • Wskaźniki zerowe mogą być użyte jako wartość wartownika, często tani sposób, aby uniknąć przeciążenia funkcji lub użycia bool.

  • Możesz wykonywać arytmetykę na wskaźniku. Na przykład,p += offset;


5
Możesz napisać, &r + offsetgdzie rzadeklarowano jako odniesienie
MM

14

Jest jedna zasadnicza różnica między wskaźnikami a referencjami, o których nikomu nie wspominałem: referencje włączają semantykę przekazywania przez argumenty w argumentach funkcji. Wskaźniki, choć na początku nie są widoczne, nie zapewniają: zapewniają jedynie semantykę wartości według wartości. Zostało to bardzo dobrze opisane w tym artykule .

Pozdrawiam i rzej


1
Odnośniki i wskaźniki są uchwytami. Oba dają semantyczny, w którym obiekt jest przekazywany przez odniesienie, ale uchwyt jest kopiowany. Bez różnicy. (Istnieją również inne sposoby posiadania uchwytów, takie jak klucz do wyszukiwania w słowniku)
Ben Voigt

Kiedyś tak myślałem. Ale zobacz powiązany artykuł opisujący, dlaczego tak nie jest.
Andrzej

2
@Andrzj: To tylko bardzo długa wersja jednego zdania w moim komentarzu: Uchwyt jest kopiowany.
Ben Voigt,

Potrzebuję więcej wyjaśnień na temat „Uchwyt został skopiowany”. Rozumiem kilka podstawowych pomysłów, ale myślę, że fizycznie odniesienie i wskaźnik wskazują lokalizację pamięci zmiennej. Czy to tak, jakby alias przechowywał zmienną wartości i aktualizował ją, ponieważ wartością zmiennej jest zmiana lub coś innego? Jestem nowicjuszem i proszę nie oznaczać tego jako głupie pytanie.
Asim,

1
@Andrzej False. W obu przypadkach występuje wartość przekazywana. Odwołanie jest przekazywane przez wartość, a wskaźnik jest przekazywany przez wartość. Mówienie inaczej dezorientuje początkujących.
Miles Rout

13

Ryzykując dodanie do zamieszania, chcę wprowadzić jakieś dane wejściowe, jestem pewien, że zależy to głównie od sposobu, w jaki kompilator implementuje referencje, ale w przypadku gcc pomysł, że referencja może wskazywać tylko zmienną na stosie nie jest właściwie poprawne, weź to na przykład:

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

Który daje to:

THIS IS A STRING
0xbb2070 : 0xbb2070

Jeśli zauważysz, że nawet adresy pamięci są dokładnie takie same, co oznacza, że ​​odwołanie pomyślnie wskazuje zmienną na stercie! Teraz, jeśli naprawdę chcesz się zakręcić, działa to również:

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).*/
}

Który daje to:

THIS IS A STRING

Dlatego odniesienie JEST wskaźnikiem pod maską, oba przechowują tylko adres pamięci, na który adres wskazuje, nie ma znaczenia, jak myślisz, co by się stało, gdybym nazwał std :: cout << str_ref; PO wywołaniu delete & str_ref? Cóż, oczywiście kompiluje się dobrze, ale powoduje błąd segmentacji w czasie wykonywania, ponieważ nie wskazuje już poprawnej zmiennej, w zasadzie mamy uszkodzone odwołanie, które wciąż istnieje (dopóki nie wykracza poza zakres), ale jest bezużyteczne.

Innymi słowy, odniesienie jest niczym innym jak wskaźnikiem, którego mechanika wskaźnika jest oderwana, co czyni go bezpieczniejszym i łatwiejszym w użyciu (bez przypadkowej matematyki wskaźnika, bez mieszania „.” I „->” itd.), Zakładając, że nie próbuj bzdur takich jak moje przykłady powyżej;)

Teraz, niezależnie od tego, jak kompilator obsługuje odniesienia, zawsze będzie miał pod maską jakiś wskaźnik, ponieważ odniesienie musi odnosić się do określonej zmiennej pod określonym adresem pamięci, aby działało zgodnie z oczekiwaniami, nie można tego obejść (stąd termin „odniesienie”).

Jedyną ważną zasadą, o której należy pamiętać w przypadku odniesień, jest to, że muszą być zdefiniowane w momencie deklaracji (z wyjątkiem odwołania w nagłówku, w takim przypadku należy je zdefiniować w konstruktorze, po tym, jak obiekt w nim zawarty jest skonstruowane, jest za późno, aby to zdefiniować).

Pamiętaj, moje powyższe przykłady są po prostu przykładami pokazującymi, czym jest referencja, nigdy nie chciałbyś używać referencji w ten sposób! W celu prawidłowego użycia referencji jest już mnóstwo odpowiedzi, które trafiły w sedno


13

Inną różnicą jest to, że możesz mieć wskaźniki do typu pustki (i oznacza to wskaźnik do czegokolwiek), ale odwołania do pustki są zabronione.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

Nie mogę powiedzieć, że jestem naprawdę zadowolony z tej szczególnej różnicy. Wolałbym, żeby to było dozwolone z odniesieniem znaczeniowym do czegokolwiek z adresem, a poza tym takie samo zachowanie dla odniesień. Umożliwiłoby to zdefiniowanie niektórych odpowiedników funkcji biblioteki C, takich jak memcpy, przy użyciu referencji.


12

Odwołanie będące parametrem funkcji wstawionej może być obsługiwane inaczej niż wskaźnik.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

Wiele kompilatorów podczas wstawiania wersji wskaźnikowej faktycznie wymusza zapis do pamięci (adres przyjmujemy jawnie). Pozostawiają jednak referencję w rejestrze, który jest bardziej optymalny.

Oczywiście dla funkcji, które nie są wstawione, wskaźnik i referencja generują ten sam kod i zawsze lepiej jest przekazywać wartości wewnętrzne przez wartość niż przez referencję, jeśli nie są one modyfikowane i zwracane przez funkcję.


10

Innym interesującym zastosowaniem odwołań jest dostarczenie domyślnego argumentu typu zdefiniowanego przez użytkownika:

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

Domyślny smak używa aspektu „powiązania stałej z tymczasowym” odwołania.


10

Ten program może pomóc w zrozumieniu odpowiedzi na pytanie. Jest to prosty program odwołania „j” i wskaźnika „ptr” wskazującego na zmienną „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();
}

Uruchom program i spójrz na wynik, a zrozumiesz.

Poświęć 10 minut i obejrzyj ten film: https://www.youtube.com/watch?v=rlJrrGV0iOg


10

Mam wrażenie, że jest jeszcze jedna kwestia, która nie została tutaj omówiona.

W przeciwieństwie do wskaźników, referencje są składniowo równoważne do obiektu, do którego się odnoszą, tj. Każda operacja, która może być zastosowana do obiektu, działa dla odwołania i przy użyciu dokładnie tej samej składni (wyjątkiem jest oczywiście inicjalizacja).

Chociaż może się to wydawać powierzchowne, uważam, że ta właściwość jest kluczowa dla wielu funkcji C ++, na przykład:

  • Szablony . Ponieważ parametry szablonu są typu kaczego, ważne są właściwości składniowe typu, dlatego często ten sam szablon może być używany zarówno z, jak Ti T&.
    (lub std::reference_wrapper<T>który nadal polega na niejawnej obsadzie T&)
    Szablony, które obejmują oba T&i T&&są jeszcze bardziej powszechne.

  • Lwowskie . Rozważmy zdanie str[0] = 'X';Bez referencji, działałoby to tylko dla c-string ( char* str). Zwrócenie znaku przez odwołanie pozwala klasom zdefiniowanym przez użytkownika mieć tę samą notację.

  • Kopiuj konstruktory . Składniowo sensowne jest przekazywanie obiektów do konstruktorów, a nie wskaźników do obiektów. Ale po prostu nie ma sposobu, aby konstruktor kopiował obiekt według wartości - spowodowałoby to rekurencyjne wywołanie tego samego konstruktora kopii. Pozostawia to odniesienia jako jedyną opcję tutaj.

  • Przeciążenie operatora . Za pomocą odniesień możliwe jest wprowadzenie pośrednictwa do wywołania operatora - powiedzmy, operator+(const T& a, const T& b)przy zachowaniu tej samej notacji infiksowej. Działa to również w przypadku zwykłych funkcji przeciążonych.

Punkty te zapewniają znaczną część C ++ i standardowej biblioteki, więc jest to dość ważna właściwość referencji.


niejawna obsada ” obsada jest konstrukcją składniową, istnieje w gramatyce; obsada jest zawsze wyraźna
ciekawy

9

Istnieje bardzo ważna nietechniczna różnica między wskaźnikami i referencjami: Argument przekazany do funkcji przez wskaźnik jest znacznie bardziej widoczny niż argument przekazany do funkcji przez odwołanie inne niż const. Na przykład:

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

Po powrocie do C wywołanie, które wygląda jak, fn(x)może zostać przekazane tylko przez wartość, więc zdecydowanie nie można go zmodyfikować x; aby zmodyfikować argument, musisz przekazać wskaźnik fn(&x). Więc jeśli argument nie był poprzedzony przez &wiesz, że nie zostanie zmodyfikowany. (Odwrotność, &czyli zmodyfikowana, nie była prawdziwa, ponieważ czasami trzeba było przekazywać duże struktury tylko do odczytu za pomocą constwskaźnika).

Niektórzy twierdzą, że jest to tak przydatna funkcja podczas odczytywania kodu, że parametry wskaźnika powinny zawsze być używane do modyfikowania parametrów, a nie do constodnośników, nawet jeśli funkcja nigdy nie oczekuje nullptr. Oznacza to, że ci ludzie twierdzą, że podpisy funkcji jak fn3()wyżej nie powinny być dozwolone. Wytyczne Google w stylu C ++ są tego przykładem.


8

Może pomogą niektóre metafory; W kontekście obszaru ekranu pulpitu -

  • Odwołanie wymaga określenia rzeczywistego okna.
  • Wskaźnik wymaga położenia fragmentu przestrzeni na ekranie, który zapewnia, że ​​będzie zawierał zero lub więcej wystąpień tego typu okna.

6

Różnica między wskaźnikiem a odniesieniem

Wskaźnik można zainicjować na 0, a odwołanie nie. W rzeczywistości odwołanie musi również odnosić się do obiektu, ale wskaźnikiem może być wskaźnik zerowy:

int* p = 0;

Ale nie możemy mieć int& p = 0;, a także int& p=5 ;.

W rzeczywistości, aby zrobić to poprawnie, musimy najpierw zadeklarować i zdefiniować obiekt, a następnie możemy zrobić odwołanie do tego obiektu, aby poprawna implementacja poprzedniego kodu była następująca:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

Inną ważną kwestią jest to, że możemy dokonać deklaracji wskaźnika bez inicjalizacji, jednak nie można tego zrobić w przypadku odwołania, które musi zawsze odwoływać się do zmiennej lub obiektu. Jednak takie użycie wskaźnika jest ryzykowne, więc ogólnie sprawdzamy, czy wskaźnik rzeczywiście wskazuje na coś, czy nie. W przypadku odniesienia taka kontrola nie jest konieczna, ponieważ wiemy już, że odniesienie do obiektu podczas deklaracji jest obowiązkowe.

Kolejna różnica polega na tym, że wskaźnik może wskazywać na inny obiekt, jednak odwołanie zawsze odnosi się do tego samego obiektu, weźmy ten przykład:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

Kolejny punkt: gdy mamy szablon taki jak szablon STL, taki rodzaj szablonu klasy zawsze zwraca odwołanie, a nie wskaźnik, aby ułatwić czytanie lub przypisywanie nowej wartości za pomocą operatora []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

1
Nadal możemy mieć const int& i = 0.
Revolver_Ocelot

1
W tym przypadku referencja zostanie użyta tylko do odczytu, nie możemy zmodyfikować tej stałej referencji nawet przy użyciu „const_cast”, ponieważ „const_cast” akceptuje tylko wskaźnik, a nie referencję.
dhokar.w

1
const_cast działa całkiem dobrze z referencjami: coliru.stacked-crooked.com/a/eebb454ab2cfd570
Revolver_Ocelot

1
dokonujesz rzutowania na odwołanie, nie rzucając odwołania, spróbuj tego; const int & i =; const_cast <int> (i); Próbuję wyrzucić ciągłość odwołania, aby umożliwić zapis i przypisanie nowej wartości do odwołania, ale nie jest to możliwe. proszę skupić się !!
dhokar.w

5

Różnica polega na tym, że niestała zmienna wskaźnika (nie mylić ze wskaźnikiem na stałą) może zostać zmieniona w pewnym momencie podczas wykonywania programu, wymaga użycia semantyki wskaźnika (&, *) operatorów, podczas gdy odniesienia można ustawić podczas inicjalizacji tylko (dlatego możesz ustawić je tylko na liście inicjalizacyjnej konstruktora, ale jakoś inaczej) i użyć zwykłej wartości uzyskującej dostęp do semantyki. Zasadniczo wprowadzono odniesienia, aby umożliwić obsługę przeciążania operatorów, jak czytałem w bardzo starej książce. Jak ktoś stwierdził w tym wątku - wskaźnik można ustawić na 0 lub dowolną inną wartość. 0 (NULL, nullptr) oznacza, że ​​wskaźnik jest inicjowany z niczym. Błędem jest wyrejestrowanie wskaźnika zerowego. Ale tak naprawdę wskaźnik może zawierać wartość, która nie wskazuje na poprawną lokalizację pamięci. Z kolei referencje starają się nie pozwolić użytkownikowi na zainicjowanie odwołania do czegoś, do czego nie można się odwoływać z uwagi na fakt, że zawsze podaje się dla niego wartość poprawnego typu. Chociaż istnieje wiele sposobów na zainicjowanie zmiennej referencyjnej w niewłaściwym miejscu w pamięci - lepiej nie zagłębiać się w szczegóły. Na poziomie maszyny zarówno wskaźnik, jak i odniesienie działają równomiernie - za pomocą wskaźników. Powiedzmy, że w podstawowych odniesieniach są cukier syntaktyczny. Odwołania do wartości są różne od tego - są to naturalnie obiekty stosu / stosu. Chociaż istnieje wiele sposobów na zainicjowanie zmiennej referencyjnej w niewłaściwym miejscu w pamięci - lepiej nie zagłębiać się w szczegóły. Na poziomie maszyny zarówno wskaźnik, jak i odniesienie działają równomiernie - za pomocą wskaźników. Powiedzmy, że w podstawowych odniesieniach są cukier syntaktyczny. Odwołania do wartości są różne od tego - są to naturalnie obiekty stosu / stosu. Chociaż istnieje wiele sposobów na zainicjowanie zmiennej referencyjnej w niewłaściwym miejscu w pamięci - lepiej nie zagłębiać się w szczegóły. Na poziomie maszyny zarówno wskaźnik, jak i odniesienie działają równomiernie - za pomocą wskaźników. Powiedzmy, że w podstawowych odniesieniach są cukier syntaktyczny. Odwołania do wartości są różne od tego - są to naturalnie obiekty stosu / stosu.


4

w prostych słowach możemy powiedzieć, że odwołanie jest alternatywną nazwą zmiennej, podczas gdy wskaźnik jest zmienną, która przechowuje adres innej zmiennej. na przykład

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
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.