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.