15.4 Funkcje zaprzyjaźnione

W poprzednim przykładzie wszystkie składowe klas PunktTrojkat były publiczne, gdyż w definicji tych klas użyliśmy słowa kluczowego struct, a nie class.

Najczęściej jednak tak nie jest — pola odpowiadające danym są zwykle prywatne. Często jednak chcielibyśmy, aby pewne funkcje nie będące metodami klasy miały dostęp do składowych niepublicznych tej klasy. Oczywiście efekt ten możemy osiągnąć definiując metody publiczne klasy udostępniające dane prywatne zawarte w obiekcie. Wtedy jednak każda funkcja może ich użyć i uzyskać dostęp do tych danych, podczas gdy naszym celem było ich udostępnienie tylko wybranym funkcjom „zaprzyjaźnionym” z tą klasą. Takie pojęcie funkcji zaprzyjaźnionej rzeczywiście istnieje.

Funkcja taka musi być zadeklarowana wewnątrz klasy, z którą jest zaprzyjaźniona. Robi się to dodając na początku jej deklaracji modyfikator friend, na przykład

       class Klasa {
           // ...
           friend int fun(double, const Klasa&);
           // ...
       };
Deklarację przyjaźni można umieścić w dowolnej sekcji definicji klasy, publicznej, prywatnej lub chronionej — nie ma to żadnego znaczenia.

Funkcja zaprzyjaźniona z klasą nie jest metodą tej klasy.

Ma ona dostęp do wszystkich składowych klasy, ale nie będąc jej metodą, nie ma określonego wskaźnika this i nie jest wywoływana na rzecz obiektu. W szczególności, ta sama funkcja może być zaprzyjaźniona z wieloma klasami: deklaracja przyjaźni musi być wtedy zawarta w definicji każdej z tych klas.

Przyjaźń może być tylko zaofiarowana funkcji przez klasę. Funkcja natomiast nie może „żądać” przyjaźni od klasy. Nie ma sposobu, aby zaprzyjaźnić funkcję z klasą, która tej przyjaźni jawnie nie deklaruje; w szczególności nie da się zaprzyjaźnić funkcji z klasą pochodzącą z biblioteki której nie możemy czy nie chcemy zmodyfikować i zrekompilować.

Deklaracja przyjaźni zawarta jest wewnątrz definicji klasy, a więc jej zakresem jest zakres klasy. Jeśli w zakresie otaczającym potrzebna jest deklaracja tej samej funkcji (bo, na przykład, definicja jest w innym module programu, albo jej użycie następuje leksykalnie przed definicją), to należy taką deklarację powtórzyć poza klasą, oczywiście wtedy już bez modyfikatora friend. Wyjątkiem są funkcje zaprzyjaźnione z klasą, których parametry są typu tejże klasy (jak to zwykle ma miejsce); kompilator przegląda wtedy przestrzeń nazw klasy argumentu w poszukiwaniu deklaracji funkcji, co pozwoli mu ją odnaleźć bez deklaracji na zewnątrz klasy (jest to tzw. wyszukiwanie Koeniga, ang. Koenig lookup, lub ADL – argument-dependent name lookup).

Można zadeklarować w klasie przyjaźń ze wszystkimi metodami innej klasy: tak np., jeśli w klasie A zadeklarujemy:

       class B;

       class A {
           // ...
           friend class B;
           //...
       };
to wszystkie metody klasy B będą miały dostęp do wszystkich składowych klasy A, ale nie odwrotnie. Przyjaźń bowiem nie jest relacją zwrotną: jeśli klasa  B jest zaprzyjaźniona z klasą  A, to nie oznacza to wcale, że klasa  A jest zaprzyjaźniona z klasą  B. Zauważmy, że w powyższym przykładzie potrzebna była deklaracja zapowiadająca klasy B, jeśli definicja tej klasy następuje po definicji klasy  A (nie byłaby potrzebna, gdyby definicja B występowała przed definicją  A).

Aby było również odwrotnie, to znaczy, aby również funkcje klasy A miały dostęp do składowych klasy B, można zadeklarować przyjaźń wzajemną klas:

       class A;

       class B {
           // ...
           friend class A;
           // ...
       };

       class A {
           // ...
           friend class B;
           // ...
       };
Przyjaźń nie jest też relacją przechodnią. Jeśli klasa  A jest zaprzyjaźniona z klasą  B, a klasa  B jest zaprzyjaźniona z klasą  C, to nie znaczy, że klasa  A jest zaprzyjaźniona z klasą  C.

W końcu, przyjaźń nie jest też dziedziczna — klasy potomne nie dziedziczą przyjaźni od swoich klas bazowych.


W poniższym przykładzie definiujemy dwie klasy: jedną opisującą punkty na prostej rzeczywistej (o współrzędnej liczba) i drugą opisującą odcinki, a więc zakresy współrzędnych w przedziale [ lewy, prawy]. Pola obu klas są prywatne.


P117: iswew.cpp     Funkcje zaprzyjaźnione z dwoma klasami

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Zakres;                                          
      5.  
      6.  class Punkt {
      7.  
      8.      int liczba;
      9.      friend void isInside(const Punkt*, const Zakres*); 
     10.  
     11.  public:
     12.      Punkt(int liczba = 0)
     13.          : liczba(liczba)
     14.      { }
     15.  };
     16.  
     17.  class Zakres {
     18.  
     19.      int lewy, prawy;
     20.      friend void isInside(const Punkt*, const Zakres*);
     21.  
     22.  public:
     23.      Zakres(int lewy = 0, int prawy = 0)
     24.          : lewy(lewy), prawy(prawy)
     25.      { }
     26.  };
     27.  
     28.  void isInside(const Punkt *p, const Zakres *z) {
     29.      if ((p->liczba >= z->lewy) && (p->liczba <= z->prawy))
     30.          cout << "Punkt " << p->liczba   << " lezy w "
     31.                  "zakresie [" << z->lewy << ","
     32.               << z->prawy  << "]\n";
     33.      else
     34.          cout << "Punkt " << p->liczba   << " lezy poza "
     35.                  "zakresem [" << z->lewy << ","
     36.               << z->prawy  << "]\n";
     37.  }
     38.  
     39.  int main() {
     40.      Punkt p(7);
     41.      Zakres z1(0,10), z2(8,20);
     42.  
     43.      isInside(&p,&z1);
     44.      isInside(&p,&z2);
     45.  }

Z oboma klasami jest zaprzyjaźniona funkcja isInside, której zadaniem jest wypisanie informacji, czy dany punkt należy do zadanego zakresu czy nie. Aby sprawdzić, czy punkt przesłany do funkcji przez referencję leży w zakresie zadanym drugim argumentem funkcji, potrzebuje ona dostępu do danych zawartych w obu obiektach. Dzięki zaprzyjaźnieniu, taki dostęp posiada:

    Punkt 7 lezy w zakresie [0,10]
    Punkt 7 lezy poza zakresem [8,20]
Zauważmy, że deklaracja zapowiadająca z linii  była konieczna, bo nazwa Zakres została użyta wewnątrz definicji klasy Punkt (linia ).

Funkcje zaprzyjaźnione stosuje się szczególnie często przy przeładowywaniu operatorów (o czym więcej w rozdziale o przeciążeniach ).

Inne zastosowanie funkcji zaprzyjaźnionych to funkcje zwane fabrykami obiektów — zastępują one czasem, z punktu widzenia użytkownika klasy, konstruktory. Konstruktory, ze swej natury, są zwykle publiczne. Możliwa, i czasem wskazana, jest jednak sytuacja, gdy wszystkie lub niektóre konstruktory są prywatne. Tworzenie obiektów jest wtedy możliwe, jeśli istnieje funkcja zaprzyjaźniona tej klasy — wewnątrz takiej funkcji wszystke składowe klasy, włączając konstruktory, są dostępne, a więc może być użyty również prywatny konstruktor (inna możliwość to zdefiniowanie w klasie publicznej funkcji statycznej zwracającej utworzony za pomocą prywatnego konstruktora obiekt).


Na zakończenie tego rozdziału rozpatrzmy przykład programu ilustrującego zarówno funkcje zaprzyjaźnione, jak i listy inicjalizacyjne dla pól stałych, odnośnikowych i obiektowych.


P118: confiel.cpp     Pola stałe, referencyjne i obiektowe

      1.  #include <iostream>
      2.  #include <cstring>
      3.  using namespace std;
      4.  
      5.  // UWAGA: klasy niekompletne; brak tu
      6.  // przeciążenia operatora przypisania
      7.  
      8.  class Pracownik;                                 
      9.  
     10.  enum stanowisko {zwykly, kierownik, prezes};     
     11.  
     12.  class Osoba {
     13.      char* nazwisko;
     14.      int   rok_urodzenia;
     15.  
     16.        // deklaracja przyjazni
     17.      friend void pracinfo(const Pracownik*);
     18.  public:
     19.      Osoba(char* n, int r)
     20.          : nazwisko(strcpy(new char[strlen(n)+1],n)),
     21.            rok_urodzenia(r)
     22.      { }
     23.  
     24.        // konstruktor kopiujacy
     25.      Osoba(const Osoba& os)
     26.          : nazwisko(strcpy(new
     27.                char[strlen(os.nazwisko)+1],os.nazwisko)),
     28.            rok_urodzenia(os.rok_urodzenia)
     29.      { }
     30.  
     31.        // destruktor
     32.      ~Osoba() {
     33.          cout << "Usuwamy osobe " << nazwisko << endl;
     34.          delete [] nazwisko;
     35.      }
     36.  };
     37.  
     38.  class Pracownik {
     39.      static int      ID;
     40.      Osoba         dane;
     41.      const int &zarobki;
     42.      const int       id;
     43.  
     44.        // deklaracja przyjazni
     45.      friend void pracinfo(const Pracownik*);
     46.  public:
     47.      Pracownik(char* nazw, int rok, int& zar)
     48.          : dane(nazw,rok), zarobki(zar), id(++ID)
     49.      { }
     50.  
     51.        // konstruktor kopiujacy
     52.      Pracownik(const Pracownik& prac)
     53.          : dane(prac.dane), zarobki(prac.zarobki), id(++ID)
     54.      { }
     55.  };
     56.  int Pracownik::ID;
     57.  
     58.  void pracinfo(const Pracownik* prac) {
     59.      cout << prac->dane.nazwisko       << " (r.ur. "
     60.           << prac->dane.rok_urodzenia  << ") id="
     61.           << prac->id << "; zarobki: " << prac->zarobki
     62.                                        << endl;
     63.  }
     64.  
     65.  int main() {
     66.      int placa[] = { 1600, 2100, 8900 };
     67.  
     68.      Pracownik  jasio("Jasio  ", 1978, placa[zwykly]);
     69.      Pracownik  henio("Henio  ", 1980, placa[zwykly]);
     70.      Pracownik    jan("Jan    ", 1965, placa[kierownik]);
     71.      Pracownik panJan("Pan Jan", 1955, placa[prezes]);
     72.  
     73.      pracinfo(&jasio);
     74.      pracinfo(&henio);
     75.      pracinfo(&jan);
     76.      pracinfo(&panJan);
     77.  
     78.      cout << "\nZmieniamy place\n\n";
     79.  
     80.      placa[zwykly] -=  300;                       
     81.      placa[prezes] += 1000;                       
     82.  
     83.      pracinfo(&jasio);
     84.      pracinfo(&henio);
     85.      pracinfo(&jan);
     86.      pracinfo(&panJan);
     87.  
     88.      cout << "\nKoniec programu\n\n";
     89.  }

Definiujemy tu w linii  wyliczenie stanowisko. Następnie definiujemy dwie klasy: OsobaPracownik. Klasa Osoba jest standardowa. Użyliśmy tu list inicjalizacyjnych w konstruktorach tylko ze względu na wydajność; pól stałych, obiektowych ani referencyjnych nie ma, więc równie dobrze mogłyby to być zwykłe konstruktory.

Wewnątrz klasy deklarujemy przyjaźń z funkcją pracinfo. Ponieważ parametrem tej funkcji jest wskaźnik typu const Pracownik*, a klasa Pracownik nie była jeszcze zdefiniowana, musieliśm w linii  dostarczyć deklarację zapowiadającą.

Klasa Pracownik jest bardziej wyszukana. Ma jedno pole statyczne (ID), jedno obiektowe, jedno ustalone referencyjne i jedno ustalone. Zatem z wyjątkiem składowej statycznej, która i tak fizycznie nie wchodzi w skład obiektu, żadnej innej składowej nie da się utworzyć w konstruktorze — trzeba to zrobić posługując się listą inicjalizacyjną i to w każdym konstruktorze, również w konstruktorze kopiującym.

Klasa Pracownik deklaruje również przyjaźń z funkcją pracinfo. Funkcja ta bowiem drukuje informacje o obiekcie klasy Pracownik, a więc musi mieć dostęp do jego składowych. Bez przyjaźni byłoby to niemożliwe, gdyż wszystkie składowe są prywatne. Ponieważ funkcja nie modyfikuje obiektów klasy Pracownik, jej parametr wskaźnikowy został zadeklarowany z modyfikatorem const. Jedną ze składowych obiektu klasy Pracownik jest obiekt klasy Osoba — w niej też pola są prywatne. Aby móc wydrukować również informacje o osobie wchodzącej w skład pracownika, funkcja pracinfo musiała zatem być zaprzyjaźniona z klasą Osoba.

W funkcji main tworzymy cztery obiekty klasy Pracownik. Tworząc je musimy przesłać też do konstruktorów dane dotyczące obiektu klasy Osoba, który będzie utworzony jako podobiekt obiektu klasy Pracownik.

Zauważmy, że składową referencyjną zarobki inicjujemy referencją do jednego z elementów tablicy placa. A zatem w zakresie obiektu nazwa zarobki jest inną nazwą któregoś z elementów tej tablicy. Elementy tablicy indeksujemy tu wartościami wyliczenia: oczywiście są one konwertowane do wartości całkowitych 0, 1 i 2, ale użycie wyliczenia pozwala indeksować tablicę za pomocą nazw coś użytkownikowi mówiących.

Po utworzeniu obiektów drukujemy informacje o nich:

    Jasio   (r.ur. 1978) id=1; zarobki: 1600
    Henio   (r.ur. 1980) id=2; zarobki: 1600
    Jan     (r.ur. 1965) id=3; zarobki: 2100
    Pan Jan (r.ur. 1955) id=4; zarobki: 8900

    Zmieniamy place

    Jasio   (r.ur. 1978) id=1; zarobki: 1300
    Henio   (r.ur. 1980) id=2; zarobki: 1300
    Jan     (r.ur. 1965) id=3; zarobki: 2100
    Pan Jan (r.ur. 1955) id=4; zarobki: 9900

    Koniec programu

    Usuwamy osobe Pan Jan
    Usuwamy osobe Jan
    Usuwamy osobe Henio
    Usuwamy osobe Jasio
Następnie w liniach  zmieniamy wartości tablicy place i ponownie drukujemy informacje o wszystkich obiektach. Widzimy, że wydrukowane składowe zarobki zostały zmienione! Tak oczywiście musiało być, gdyż są one tylko innymi nazwami elementów tablicy place. Ale zauważmy, że składowe te są, po pierwsze, prywatne, po drugie stałe! Zatem „podwójnie” nie powinna być możliwa zmiana ich wartości w funkcji main. A jednak jest możliwa! Pokazuje to ponownie, że tak naprawdę chronione są nie zmienne (obszary pamięci), ale nazwy: jeśli do składowej prywatnej czy stałej możemy odnieść się poprzez inną nazwę, to żadnej ochrony nie ma.

Wydruk demonstruje też działanie destruktorów klasy Osoba. Widzimy, że zadziałały one już po wyjściu programu z funkcji main, i że wywoływane są w kolejności odwrotnej do tej, w jakiej obiekty były utworzone. Obiekty klasy Osoba usuwane są tu podczas usuwania obiektów klasy Pracownik jako ich składowe.

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