Dwa sposoby odwoływania się do czegoś
Znasz już wskaźniki: zmienne, które przechowują adres i pozwalają dotrzeć do mieszkającego tam obiektu. Referencje to drugie narzędzie, które C++ daje do pośredniej pracy z istniejącym obiektem. Pokrywają się na tyle, że początkujący często nie wiedzą, które wybrać, więc ta strona zestawia je obok siebie.
W skrócie: referencja to alias. Po wykonaniu int& r = x; r jest x: ten sam obiekt, inna nazwa. Wskaźnik to osobny obiekt, który akurat przechowuje adres innego. Ta jedna różnica napędza całą resztę.
Referencja to alias
Referencja musi zostać powiązana z obiektem w chwili utworzenia, a od tej pory każde jej użycie dotyka oryginału.
Zauważ, że w miejscu użycia nie ma * do dereferencji ani & do „pobrania adresu”: czytasz i zapisujesz alias dokładnie jak zwykły int. & w int& alias jest częścią typu, a nie operatorem pobrania adresu.
Czym się różnią
Poniższe zachowania to cały powód, dla którego istnieją oba narzędzia. Tę tabelę warto zapamiętać.
// reference pointer
// must be initialized? yes no (but should be)
// can be null? no yes (nullptr)
// can be reseated? no yes
// pointer arithmetic? no yes
// syntax to use it just the name *p or p->member
// taking address &ref == &original &p is the pointer's own address
Dwie z tych cech najczęściej wprowadzają w błąd. Po pierwsze referencji nigdy nie da się przestawić: przypisanie do niej kopiuje wartość do obiektu, na który wskazuje, a nie kieruje referencji na coś nowego.
Wskaźnik natomiast może w każdej chwili wskazać gdzie indziej:
Po drugie referencja nigdy nie może legalnie być null, a wskaźnik może. Dzięki temu „brak wartości” da się wyrazić wskaźnikiem, ale nie referencją, i na tej właściwości będziesz polegać bardzo często.
Wybór w parametrach funkcji
Tu wybór pojawia się najczęściej. Gdy funkcja ma czytać albo zmieniać obiekt wywołującego, działają oba sposoby, ale sygnalizują różne intencje.
Wersji z referencją (addTax(cart)) nie da się wywołać z „niczym”, więc wewnątrz funkcji nigdy nie sprawdzasz null: obiekt na pewno istnieje. Wersja ze wskaźnikiem (applyDiscount(&cart)) ogłasza w miejscu wywołania, przez &, że argument może się zmienić, i pozwala wywołującemu przekazać nullptr, co znaczy „nie dotyczy”. Wybierz tę, której gwarancja pasuje do twojej funkcji.
Dla parametrów tylko do odczytu o dużych typach idiomatycznym wyborem jest const T&: unika kopii i obiecuje brak modyfikacji. Więcej o przekazywaniu przez wartość i przez referencję znajdziesz na stronie o parametrach funkcji.
Prosta praktyczna zasada
W razie wątpliwości domyślnie wybierz referencję, a na wskaźnik przejdź dopiero wtedy, gdy potrzebujesz możliwości, której referencja nie ma:
- Użyj referencji, gdy obiekt zawsze istnieje, a jego tożsamość nigdy się nie zmienia: to typowy przypadek parametrów funkcji i aliasów.
- Użyj wskaźnika, gdy spełniony jest któryś z warunków:
- „Nic” jest poprawnym stanem (opcjonalny argument, wyszukiwanie, które może nic nie znaleźć): wskaźnik może być
nullptr. - Musisz z czasem wskazywać na różne obiekty: wskaźnik da się przestawić.
- Zarządzasz pamięcią na stercie, którą zwolnisz przez
delete(albo, lepiej, przez inteligentny wskaźnik), albo przechodzisz po tablicy z arytmetyką wskaźników.
- „Nic” jest poprawnym stanem (opcjonalny argument, wyszukiwanie, które może nic nie znaleźć): wskaźnik może być
Jeśli żaden z tych warunków nie zachodzi, referencja jest czystszym i bezpieczniejszym wyborem, bo kompilator sam pilnuje zasady „zawsze poprawna, nigdy nieprzestawiana”.
Częste błędy, których warto unikać
- Oczekiwanie, że
ref = otherprzestawi referencję. Zamiast tego przypisuje wartość do obiektu, do którego się odnosi. Referencje są związane na całe życie; jeśli potrzebujesz przestawiania, użyj wskaźnika. - Zwracanie referencji (albo wskaźnika) do zmiennej lokalnej.
int& f() { int x = 5; return x; }zwraca wiszącą referencję:xumiera, gdyfsię kończy, a użycie wyniku to niezdefiniowane zachowanie. Ta sama pułapka dotyczy wskaźników (return &x;). - Fabrykowanie „pustej referencji”. Zapis
int& r = *p;, gdyptonullptr, jest niezdefiniowanym zachowaniem już w chwili dereferencji, a nie bezpieczną „pustą” referencją. Opcjonalność wyrażaj wskaźnikiem albostd::optional. - Sięganie po wskaźnik z przyzwyczajenia. Jeśli argument zawsze istnieje i nigdy go nie przestawisz, referencja usuwa całą klasę sprawdzeń null i awarii. Nie płać za możliwości, z których nie korzystasz.
Dalej: pamięć dynamiczna
Do tej pory każdy obiekt, do którego się odwoływano albo na który wskazywano, był tworzony automatycznie na stosie. Następna strona, o pamięci dynamicznej, omawia new i delete: proszenie systemu operacyjnego o pamięć w czasie działania, dlaczego to wskaźniki (a nie referencje) są jej właścicielami i jak zapomnienie o jej zwolnieniu powoduje wycieki.
Najczęściej zadawane pytania
Czym się różni referencja od wskaźnika w C++?
Referencja to alias istniejącego obiektu: musi być zainicjalizowana, nigdy nie może być null i nie da się jej później przestawić na inny obiekt. Wskaźnik to osobna zmienna przechowująca adres: może być null, można go przestawić, żeby wskazywał gdzie indziej, i obsługuje arytmetykę wskaźników. Przy referencjach używasz składni &, a przy wskaźnikach */->.
Kiedy w C++ użyć wskaźnika zamiast referencji?
Użyj wskaźnika, gdy „nic” jest poprawnym stanem (opcjonalny argument, brak wyniku wyszukiwania), gdy musisz go z czasem przestawiać na różne obiekty albo gdy posiadasz pamięć na stercie, którą zwolnisz przez delete. Użyj referencji, gdy obiekt zawsze istnieje i nigdy nie zmienia tożsamości, co obejmuje większość parametrów funkcji.
Czy referencja w C++ może być null?
Nie. Poprawna referencja zawsze odnosi się do prawdziwego obiektu, więc nigdy nie sprawdzasz jej pod kątem null. Jeśli utworzysz referencję z dereferencji pustego wskaźnika (int& r = *p;, gdzie p jest null), dostajesz niezdefiniowane zachowanie, a nie pustą referencję. Gdy chcesz wyrazić „może nic”, użyj wskaźnika albo std::optional.