Menu

Referencje a wskaźniki w C++: kiedy używać którego

Praktyczne porównanie referencji i wskaźników w C++: co mają wspólnego, czym się różnią (przestawianie, null, arytmetyka) i jasna zasada, po które narzędzie sięgać w codziennym kodzie.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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.

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 = other przestawi 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ę: x umiera, gdy f się 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;, gdy p to nullptr, jest niezdefiniowanym zachowaniem już w chwili dereferencji, a nie bezpieczną „pustą” referencją. Opcjonalność wyrażaj wskaźnikiem albo std::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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ