Referencja
Z Wikipedii
Spis treści |
[edytuj] Referencja w C++
W języku programowania C++ referencja jest pochodnym typem danych i -- podobnie jak wskaźnik -- umożliwia pośrednie odwoływanie się do zmiennych lub obiektów.
Z punktu widzenia programisty, referencja jest alternatywną nazwą innej, istniejącej już zmiennej lub obiektu. Stosowana jest głównie do
- przekazywania argumentów do funkcji
- przekazywania wartości z funkcji (szczególnie operatorów)
- skracania zapisu długich wyrażeń
W dwóch pierwszych zastosowaniach referencja z zasady implementowana jest jako stały wskaźnik, co w praktyce oznacza, że tak naprawdę funkcja otrzymuje jako argument lub zwraca jako wartość pewien adres. Fakt ten jest jednak zupełnie transparentny dla programisty, gdyż referencji używa się tak, jak zwykłych zmiennych i obiektów, a nie jak wskaźników. W trzecim z powyższych zastosowań, a także podczas opracowywania tzw. funkcji wklejanych (inline), kompilator może tak zoptymalizować referencję, że w programie nie będzie reprzentowana przez żaden obiekt.
Referncje wymagają inicjalizacji.
[edytuj] Referencje i stałe referencje
W języku C++ rozróżnia się dwa typy referencji: "zwyczajne" i stałe. Zwyczajne referencje umożliwiają zarówno odczyt jak i modyfikację obiektu, do którego się odwołują, natomiast stałe referencje umożliwiają wyłącznie odczyt stanu obiektu. Zwyczajne referencje wymagają podania konkretnego obiektu, do którego się mają odnosić, natomiast stałe referencje mogą się odnosić zarówno do obiektów, jak i wyrażeń. Utworzenie referencji do wyrażenia, które oczywiście nie ma własnego adresu, realizowane jest w ten sposób, że najpierw konstruowany jest tymczasowy obiekt inicjalizowany wartością danego wyrażenia, a następnie referencji przypisuje się adres tego tymczasowego obiektu. Właściwość ta ma kapitalne znaczenie przy definiowaniu argumentów funkcji (a szczególnie funkcji generowanych z szablonów): jako faktycznych argumentów funkcji pobierających argument(y) przez stałą referencję można użyć zarówno nazw zmiennych (np. f(x)
), jak i wyrażeń (np. f(x+1)
).
Zaleca się, by obiekty klas zdefiniowanych przez użytkownika przekazywać do lub z funkcji poprzez referencję lub stałą referencję zależnie od tego, czy funkcja ma mieć prawo do modyfikowania argumentu/wartości, czy nie. Przekazywanie argumentów przez wartość należy ograniczyć do niewielkich obiektów, np. prostych typów wbudowanych, jak int
, double
czy int*
.
[edytuj] Składnia
Deklaracja referencji składa się z nazwy typu, operatora & i nazwy zmiennej referencyjnej, np.
int i; ... int& q = i; // q jest referencją do zmiennej i
Deklaracja stałej referencji zawiera dodatkowo słowo kluczowe const
pisane bezpośrednio przed lub tuż za nazwą typu, np.
const int& i_ref1 = i; // i_ref1 jest stałą referencją do zmiennej i typu int int const& i_ref2 = i; // i_ref2 jest stałą referencją do zmiennej i typu int
Obie konwencje są równoważne.
[edytuj] Referencje a wskaźniki
Jak już wspomniano, referencje implementowane są przy pomocy stałych wskaźników. Analogicznie stałe referencje implementowane są przy pomocy stałych wskaźników na stałe. Ponieważ w przeciwieństwie do wskaźników, referencje muszą być inicjalizowane adresem obiektu, są one dużo bardziej bezpieczne od wskaźników. Nie oznacza to jednak, że referencje gwarantują 100% bezpieczeństwo. Wystarczy, że w programie utworzymy niezainicjowany wskaźnik:
int* p;
Ze zględu na brak inicjalizacji zmiennej p
, wyrażenie *p
nie reprezentuje żadnego obiektu. Tym niemniej może ono przypadkowo zostać przekazane do funkcji przyjmującej argument przez referencję.
f(*p); // f jest funkcją o sygnaturze void f(int & x);
W tym wypadku funkcja otrzyma referencję do zmiennej znajdującej się pod nieważnym adresem p
. Jakiekolwiek użycie takiego argumentu może zakończyć się katastrofą. Wina nie leży jednak po stronie referencji, lecz po stronie wskaźników.
[edytuj] Przykłady
[edytuj] Użycie referencji w argumentach funkcji
Poniższy kod zawiera typową (nieco uproszczoną) implementację standardowego szablonu funkcji swap
, która zamienia wartości swoich argumentów.
template<typename T> inline void swap(T& a, T& b) { const T tmp(a); a = b; b = tmp; }
Ponieważ funkcja modyfikuje argumenty, są jej one przekazywane przez referencję.
[edytuj] Użycie referencji w wartości funkcji
Podstawowym miejscem zastosowania typów referencyjnych w wartościach funkcji jest implementacja operatorów. Oto przykład (szablonu) operatora strumieniowego, który wyświetla zawartość dowolnego standardowego wektora:
template <typename T> std::ostream & operator<<(std::ostream & out, std::vector<T> const& v) { out << '('; for (size_t i = 0; i + 1 < v.size(); ++i) out << v[i] << ", "; if (!v.empty()) out << v.back(); out << ')'; return out; }
Dzięki przekazanu jako wartości operatora <<
referencji do strumienia, operatora tego można użyć w ciągu, np.
std::cout << v << ", "; // v jest typu std::vector<T>
Ponieważ operator<<
jest lewostronnie łączny, w instrukcji tej najpierw zostanie opracowane wyrażenie std::cout << v
. Jego wartość, czyli (referencja do) std::cout
, zostanie następnie użyta jako lewy argument drugiego operatora<<
. W sumie więc powyższa instrukcja równoważna jest dwóm instrukcjom
std::cout << v; std::cout << ", ";
Referencja w powyższym przykładzie jest konieczna, gdyż strumieni nie można kopiować, w związku z czym nie można ich przekazywać z funkcji przez wartość.
[edytuj] Zastosowanie referencji do upraszczania notacji i unikania zbędnych obliczeń
Rozpatrzmy następujący fragment kodu:
std::map<std::string, std::vector<int> > mapa; ... std::vector<int> & w = mapa["ala"];
Referencja udostępnia element mapa["ala"]
poprzez alternatywną "nazwę" w
. W związku z tym wyrażenie mapa["ala"].resize(100)
jest równoważne wyrażeniu w.resize(100)
, z tym że to drugie jest prostsze (w więc czytelniejsze) oraz bardziej efektywne, gdyż nie wymaga dość kosztownego obliczania adresu wyrażenia mapa["ala"]
.
[edytuj] Zastosowanie stałych referencji w argumentach i wartości funkcji
Oto typowa implementacja szablonu max
obliczającego większy z dwóch obiektów a
, b
dowolnego typu T
template<typename T> inline const T& max(const T& a, const T& b) { if (a < b) return b; return a; }
W powyższym przykładzie:
- Stałą referencję zastosowano zarówno do przekazania argumentów do funkcji, jak i jej wartości do punktu jej wywołania;
- Dzięki zastosowaniu stałej referencji, funkcji
max
można używać nawet tak, jakby argumenty były do niej przekazywane przez wartość:
int n = 9, k = 10; ... int m = max(n, 100 - k);
W przykładzie tym pierwszy argument (obiekt n
) zostanie przekazany do funkcji max
przez (stałą) referencję, natomiast drugi argument (wyrażenie 100 - k
) zostanie wpierw skopiowany (jakby był przekazywany przez wartość) i dopiero adres tej kopii zostanie przekazany do funkcji.
- Gdyby argumenty były do funkcji przekazywane przez wartość, funkcja niepotrzebie tworzyłaby ich lokalne kopie, co mogłoby być szczególnie niekorzystne dla tych typów
T
, które posiadają nietrywialne, złożone konstruktory kopiujące - Gdyby argumenty formalne przekazywano przez zwykłą referencję, faktycznymi argumentami funkcji nie mogłyby być ani stałe obiekty, ani jakiekolwiek wyrażenia.
- W wypadku zastosowania agresywnej optymalizacji, dzięki zastosowaniu atrybutu
inline
wyrażenia typumax(n, 100 - k)
przestaną byc interpretowane jak wywołania funkcji, a sposób przekazywania "argumentów" straci jakiekolwiek znaczenie. Dzięki temu wartości wyrażeń typumax(n, 100)
będą obliczane z największą możliwą prędkością, bez żadnego narzutu związanego z przekazaniem literału przez referencję.
[edytuj] Zobacz też
[edytuj] Referencja w innych językach programowania
W pewnych językach programowania referencje w ogóle nie występują. Na przykład w języku C argumenty i wartości funkcji przekazywane są wyłącznie przez wartość.
Istnieją też języki, np. Fortran 77, w których co prawda nie ma typów referencyjnych, ale referencje wykorzystywane są (niejawnie) do przekazywania argumentów i wartości funkcji. Własność ta w niektórych starszych implementacjach fortranu (np. Lahey 77) prowadziła do nieoczekiwanego efektu: programy mogły modyfikować wartości literałów (sic!).
W innych językach programowania, takich jak Python czy Java, referencja ma semantykę bliższą wskaźnikom z C++. Każde przypisanie do referencji powoduje, że wskazuje ona na nowo podany obiekt. Pustą referencję (nie wskazującą na żaden obiekt) oznacza się specjalnym słowem kluczowym (null
w Javie, nil
w Smalltalku) lub specjalnym obiektem pustym (ang. null object; w Pythonie jest to None
). Od wskaźnika tak rozumiana referencja różni się tym, że nie może wskazywać na błędny, przypadkowy adres w pamięci. Zawsze jest albo pusta, albo wskazuje na konkretny obiekt. Eliminuje to całą kategorię błędów wynikających z próby interpretacji przypadkowego fragmentu pamięci jako obszaru zawierającego konkretne, użyteczne dane.