Menu

Referencje w C++: przekazywanie przez referencję, const& i aliasy

Referencje w C++ wyjaśnione: jak & w parametrze tworzy alias, dlaczego przekazywanie przez referencję unika kopii i pozwala funkcji zmieniać zmienne wywołującego, kiedy sięgać po const& i kiedy referencje, a kiedy zwracanie wartości.

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

Inna nazwa tej samej rzeczy

Na stronie o parametrach funkcji każdy argument był kopiowany do funkcji. Przez tę kopię funkcja nie może zmienić zmiennej wywołującego: widzi tylko własny duplikat. Referencja burzy ten mur. To alias: druga nazwa powiązana z istniejącą zmienną, dzieląca z nią dokładnie tę samą pamięć.

Referencję tworzysz przez & w deklaracji. Po powiązaniu referencji i oryginału nie da się odróżnić:

Dwie zasady czynią referencje bezpiecznymi i przewidywalnymi: referencja musi być zainicjalizowana w chwili deklaracji (int& r; to błąd kompilacji) i nigdy nie da się jej potem przestawić na coś innego. Przypisanie do referencji zawsze zapisuje do tego, z czym była pierwotnie powiązana.

Przekazywanie przez referencję: funkcja sięga z powrotem

Prawdziwa korzyść jest w funkcjach. Dodaj & do parametru, a funkcja dostanie alias argumentu wywołującego zamiast kopii. Zmiany wewnątrz funkcji są teraz widoczne na zewnątrz:

Usuń &, a addBonus zwiększy jednorazową kopię, zostawiając total na 100. Ten jeden znak stanowi całą różnicę. To kanoniczny sposób pisania funkcji, która zwraca więcej niż jeden wynik albo edytuje swoje wejście w miejscu. Klasyczny przykład to zamiana dwóch zmiennych:

Bez referencji swapValues zamieniłoby tylko lokalne kopie, a x/y zostałyby 1 2. (Biblioteka standardowa ma już std::swap, ale samodzielne napisanie tej funkcji dokładnie pokazuje, co daje parametr referencyjny.)

Referencje const: szybki odczyt z obietnicą nietykalności

Przekazywanie przez referencję unika też kopiowania, a przy dużym obiekcie kopia może być kosztowna. Ale zwykły parametr T& sygnalizuje „mogę to zmienić”, co wprowadza w błąd, gdy chcesz tylko czytać. Rozwiązaniem jest const T&: dostajesz szybkość referencji bez kopii i wymuszaną przez kompilator obietnicę, że funkcja nie zmieni argumentu.

Referencja nie-const może się powiązać tylko ze zmienną, którą da się modyfikować, ale referencja const może się też powiązać z literałami i obiektami tymczasowymi, dlatego greet("literal works too") się kompiluje. Praktyczna zasada wyboru typu parametru:

void f(int x)              // tani typ, tylko odczyt -> po prostu skopiuj
void f(const string& s)   // ciężki typ, tylko odczyt -> referencja const
void f(string& s)         // zamierzasz zmienić obiekt wywołującego

Domyślnie używaj const T& dla każdego typu klasowego, który tylko czytasz (string, vector, własne struktury), a referencję nie-const zostaw na sytuacje, gdy naprawdę chcesz zapisać zmiany z powrotem.

Zwracanie referencji

Funkcja może też zwracać referencję, dając wywołującemu alias do czegoś, co już istnieje. To częste w kodzie przypominającym kontenery: dzięki temu działa v[i] = 5 i tak właśnie działa pod spodem przeciążanie operatorów:

Ponieważ at zwraca int&, samo wyrażenie wywołania at(data, 1) jest lwartością, do której można przypisać. Zwróć zamiast tego zwykły int, a at(data, 1) = 42 się nie skompiluje, bo przypisanie trafiałoby do tymczasowej kopii.

Wielka pułapka: wiszące referencje

Referencja niczego nie posiada; wskazuje tylko pamięć, która żyje gdzie indziej. Jeśli ta pamięć umrze, gdy referencja jest wciąż w użyciu, masz wiszącą referencję, a czytanie przez nią to niezdefiniowane zachowanie: może wypisać śmieci, wywołać awarię albo wyglądać na działające, dopóki nie zepsuje ci dnia na produkcji. Klasyczny błąd to zwracanie referencji do zmiennej lokalnej:

int& broken() {
    int local = 42;
    return local;   // BŁĄD: local jest niszczone, gdy broken() się kończy
}                   // zwrócona referencja wisi

int main() {
    int& r = broken();
    cout << r << "\n";   // NIEZDEFINIOWANE ZACHOWANIE: czyta martwą pamięć
}

Zmienna local znika w chwili, gdy broken się kończy, więc referencja wskazuje na odzyskane miejsce na stosie. Zwracaj referencję tylko do czegoś, co przeżyje wywołanie: parametru przekazanego przez referencję, pola obiektu albo zmiennej statycznej. Jeśli wartość jest obliczana wewnątrz funkcji, zwróć ją przez wartość i pozwól kompilatorowi zoptymalizować kopię. Ta sama pułapka dotyczy pętli po zakresie i każdej referencji powiązanej z obiektem tymczasowym: nigdy nie trzymaj referencji dłużej, niż żyje to, co nazywa.

Dalej: przeciążanie funkcji

Referencje dają ci drugie pokrętło przy każdym parametrze: kopia czy alias, modyfikowalny czy const. To pokrętło bezpośrednio łączy się z kolejnym tematem. Przeciążanie funkcji pozwala zdefiniować kilka funkcji o tej samej nazwie, ale różnych listach parametrów, a kompilator wybiera właściwą, dopasowując typy argumentów, w tym to, czy są przekazywane przez wartość, przez referencję, czy przez referencję const.

Najczęściej zadawane pytania

Czym jest referencja w C++?

Referencja to alias istniejącej zmiennej, czyli inna nazwa dla tej samej pamięci. Tworzysz ją przez & w deklaracji: int& r = x;. Od tej chwili r i x są wymienne; zmiana jednego zmienia drugie. Referencje muszą być zainicjalizowane przy deklaracji i nigdy nie da się ich przestawić na inną zmienną.

Czym się różni przekazywanie przez wartość od przekazywania przez referencję w C++?

Przekazywanie przez wartość (void f(int x)) kopiuje argument, więc funkcja pracuje na własnej kopii, a zmienna wywołującego pozostaje nietknięta. Przekazywanie przez referencję (void f(int& x)) daje funkcji bezpośredni dostęp do zmiennej wywołującego, więc zmiany są widoczne po wywołaniu. Nie powstaje przy tym żadna kopia, co ma znaczenie przy dużych obiektach.

Kiedy w C++ używać parametrów będących referencją const?

Używaj const T&, gdy funkcja musi tylko czytać parametr, ale typ jest drogi w kopiowaniu (string, vector, duże struktury). Dostajesz szybkość referencji bez kopii i gwarancję kompilatora, że funkcja nie zmieni wartości wywołującego. Dla tanich typów takich jak int czy double zwykłe przekazywanie przez wartość jest prostsze i równie szybkie.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ