4.7 Referencje

Referencje (odnośniki) są często nazywane „przezwiskami” (aliasami) zmiennych — zarówno zmiennych typów prostych, jak i obiektowych. Zostały one wprowadzone w C++; w czystym C takie pojęcie nie występuje. Trzeba pamiętać, że w zasadzie nie ma czegoś takiego jak zmienna tego typu. Odnośnik (referencja), jak nazwa sugeruje, zawsze odnosi się do niezależnie istniejącej zmiennej o dobrze określonym typie (int, double, string,...). Tym niemniej referencje nazywa się często zmiennymi odnośnikowymi (referencyjnymi). Jest to dopuszczalne, pod warunkiem, że pamięta się prawdziwe znaczenie referencji. Zatem pamiętajmy, że

referencja jest inną nazwą istniejącej zmiennej.

Zanim wyjaśnimy, co to właściwie znaczy i jaki sens ma nadawanie innych nazw zmiennym, powiemy, jak takie referencje definiować i używać.

Przede wszystkim, skoro referencja to inna nazwa istniejącej zmiennej, to nie da się utworzyć referencji bez związania jej ze zmienną, której ma być inną nazwą. Takie związanie referencji ze zmienną jest trwałe: po utworzeniu nie można referencji związać z jakąś inną zmienną.

Tak więc referencja zadeklarowana musi zostać od razu zainicjowana odniesieniem do istniejącej zmiennej: od tej pory nazwa referencji i nazwa tej zmiennej są traktowane równoważnie jako dwie nazwy tej samej zmiennej, a wszelkie operacje na referencjach są równoważne operacjom na pierwotnej zmiennej (ponieważ operacjami na tej zmiennej).

Deklaracja i inicjacja referencji (tu o nazwie refk) może na przykład wyglądać tak:


P17: refer.cpp     Referencje

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main() {
      5.      int k = 5;
      6.      int &refk = k;
      7.  
      8.      cout << "refk = " << refk << endl;
      9.      cout << "   k = " << k    << endl;
     10.  
     11.      k = 7;
     12.  
     13.      cout << "refk = " << refk << endl;
     14.      cout << "   k = " << k    << endl;
     15.  
     16.      refk = 9;
     17.  
     18.      cout << "refk = " << refk << endl;
     19.      cout << "   k = " << k    << endl;
     20.  }

Na początku deklarujemy/definiujemy tu k, zwykłą zmienną typu int. W następnej linii deklarujemy referencję refk do zmiennej typu int i związujemy ją z konkretną, istniejącą zmienną tego właśnie typu, zmienną k. Ogólnie zatem

Deklaracja referencji ma postać: ' Typ &ref = zmienna'

gdzie Typ jest nazwą pewnego typu, ref dowolną nazwą, jaką nadajemy odnośnikowi, a  zmienna nazwą pewnej już istniejącej zmiennej typu Typ. Po takiej deklaracji

nazwą typu referencji ref jest Typ&.

W naszym przykładzie refk odnosi się do k i jak widzimy po wynikach

    refk = 5
       k = 5
    refk = 7
       k = 7
    refk = 9
       k = 9
nazwy krefk odnoszą się do tej samej zmiennej: możemy modyfikować tę zmienną poprzez którąkolwiek z tych nazw z tym samym efektem. Również drukowanie wartości zmiennej nie zależy od tego, której nazwy użyliśmy.

Pamiętamy (rozdz. o wskaźnikach ), że symbol ' &' oznacza operator wyłuskania adresu. Tu poznaliśmy jego drugie znaczenie. Podobnie jak to ma miejsce w wypadku symbolu ' *' (używanego przy deklarowaniu wskaźników i jako operatora wyłuskania wartości — dereferencji), właściwe znaczenie wynika zawsze z kontekstu: jeżeli chodzi nam o deklarację referencji, to znak ' &' jest zawsze poprzedzony nazwą typu, a to z kolei nie jest składniowo możliwe, jeśli ' &' ma pełnić rolę jednoargumentowego operatora wyłuskania adresu.

Spójrzmy na prosty, ale pouczający przykład:


P18: wskref.cpp     Wskaźniki i referencje

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main () {
      5.      int k = 7, *p = &k, &refk = *p, m = 9;  
      6.  
      7.      p = &m;                                 
      8.      k = 11;
      9.  
     10.      cout << "  *p = " <<   *p << endl;      
     11.      cout << "refk = " << refk << endl;      
     12.  }

którego uruchomienie daje:

      *p = 9
    refk = 11
Przyjrzyjmy się deklaracjom/definicjom w linii : W linii  zmieniamy wartość p — teraz wskazuje na zmienną m, która ma wartość 9 — wydrukowanie *p (linia ) daje więc 9. Z kolei wartość k zmieniamy na 11. Zmienna refk została związana na trwałe z  k, choć zrobiliśmy to pośrednio, poprzez wskaźnik p. Tego zmienić już się nie da; drukowanie wartości refk zawsze spowoduje pojawienie się na ekranie aktualnej wartości k — drukując w linii wartość refk otrzymujemy 11.

Podobnie jak gwiazdki dla wskaźników, znaki ' &' trzeba stawiać przed nazwą każdej referencji na liście deklaracyjnej. Poniższa deklaracja na przykład

       double x = 3, &y = x, *z = &x, &u = *z;
deklaruje dokładnie dwie zmienne: zmienną x typu double zainicjowaną wartością 3 oraz zmienną wskaźnikową z typu double* zainicjowaną adresem zmiennej x. Zauważmy, że y nie jest zmienną, tylko referencją do zmiennej —  mianowicie do zmiennej x. Podobnie u jest referencją do zmiennej aktualnie (w momencie gdy strumień sterowania przechodzi przez tę deklarację) wskazywanej przez wskaźnik z: jest to więc również zmienna x. Po takiej deklaracji referencje yu będą się odnosić do zmiennej x i tego się już zmienić nie da — zmienna x będzie miała w tym fragmencie programu trzy nazwy!

Wskaźnik z natomiast aktualnie wskazuje na x, ale w dalszej części programu to wskazanie można będzie zmienić.

Wszystko to wygląda nawet zabawnie, ale nie odpowiada na pytanie, po co nam zmienne o kilku różnych nazwach. Rzeczywiście, zwykle referencje nie służą do tego, aby mnożyć nazwy dla tych samych zmiennych. Ich prawdziwa siła tkwi w roli, jaką pełnią przy przesyłaniu danych do funkcji i, odwrotnie, przesyłaniu wyników działania z wywoływanej funkcji do funkcji wywołującej. Dyskusję tego aspektu referencji odłożymy do rozdziałów o argumentach referencyjnych i o funkcjach zwracających referencje .

Zauważmy jeszcze, że nie ma czegoś takiego jak tablica referencji. Natomiast mogą istnieć, i są czasem przydatne, referencje do tablic.

T.R. Werner, 25 lutego 2017; 22:31