20.3 Operator przypisania dla klas pochodnych

Dużo trudności sprawia czasem właściwe zdefiniowanie (przeciążenie) operatora przypisania dla klasy pochodnej.

Jeżeli w klasie bazowej jest zdefiniowany nieprywatny operator przypisania, a w klasie pochodnej nie jest, to do przypisania odziedziczonego podobiektu klasy bazowej zostanie użyty operator przypisania zdefiniowany w klasie bazowej. Natomiast dla części „własnej” obiektu klasy pochodnej zostanie wtedy użyte przypisanie dostarczone przez system (a więc „pole po polu”; prawdopodobnie nieprawidłowo, jeśli są w klasie pola wskaźnikowe). Rozpatrzmy na przykład poniższy program, w którym definiujemy prostą klasę A i klasę dziedziczącą B:


P161: inhas.cpp     Dziedziczenie operatora przypisania

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  struct A {
      5.      char a;
      6.      A(char aa = 'a') {
      7.          a = aa;
      8.      }
      9.  
     10.      A& operator=(const A& aa) {
     11.          a = aa.a;
     12.          cout << "A::operator=()\n";
     13.          return *this;
     14.      }
     15.  };
     16.  
     17.  struct B: public A {
     18.      char b;
     19.      B(char bb = 'b') : A(bb) {
     20.          b = bb;
     21.      }
     22.  };
     23.  
     24.  int main() {
     25.      B b1(1),b2(2);
     26.      b1 = b2;
     27.  }

Program ten drukuje
    A::operator=()
co świadczy o tym, że podczas wykonywania przypisania dla obiektów klasy pochodnej  B została automatycznie wywołana metoda operator=() z klasy nadrzędnej  A. Ta odziedziczona z klasy bazowej metoda operator=() nie może oczywiście wiedzieć o składowych klasy pochodnej, których nie było w klasie bazowej.

Przeanalizujmy zatem sytuację, gdy operator przypisania został w klasie bazowej zdefiniowany, ale dla części „własnej” również chcemy przedefiniować przypisanie. Zatem w klasie pochodnej musimy również przeciążyć operator ' ='. Metoda przeciążająca operator przypisania w klasie pochodnej powinna uwzględniać składowe klasy pochodnej nieobecne w klasie bazowej oraz musi jawnie wywołać ten operator z klasy bazowej, by „zajął się” podobiektem z niej odziedziczonym: ponieważ zostanie wtedy uruchomiona wersja przypisania zdefiniowana w klasie pochodnej, więc dla części odziedziczonej metoda operator=() z klasy bazowej nie zostanie teraz wywołana „sama z siebie”.

Zauważmy, że można to zrobić przez jawne, „po nazwie”, wywołanie metody przeciążającej operator przypisania z klasy bazowej. Można też osiągnąć ten sam cel przez proste przypisanie do *this, jeśli tylko podpowiemy kompilatorowi, żeby traktował wskazywany obiekt jako obiekt klasy bazowej (obiektem tym jest podobiekt klasy bazowej zawarty w obiekcie klasy pochodnej). Wtedy bowiem, zgodnie z regułami przeciążania operatorów, na rzecz tego podobiektu wywołana zostanie funkcja operator=() zdefiniowana w klasie bazowej.

Brzmi to zawile, ale jest całkiem proste. Załóżmy, jak zwykle, że klasa  B dziedziczy z klasy  A. Załóżmy też, że w klasie A operator przypisania został przeciążony, czyli jest w niej zdefiniowana metoda operator=(). Parametrem tej metody jest referencja (zwykle z modyfikatorem const) do obiektu klasy A, ale, jak wiemy, można taką metodę wywołać z argumentem typu pochodnego, którym to argumentem będzie referencja do przypisywanego obiektu klasy pochodnej (czyli tego, który występuje po prawej stronie przypisania). Na marginesie zauważmy, że cały mechanizm nie zadziałałby, gdybyśmy w klasie A, najzupełniej legalnie, zdefiniowali metodę operator=() z parametrem typu A lub const A zamiast A& lub const A&. Tak więc, przedefiniowując operator przypisania w klasie pochodnej  B, moglibyśmy napisać (zakładając, że definicja ta jest poza klasą):

       B& B::operator=(const B& b) {
           this->A::operator=(b);
           // ...
           return *this;
       }
W ten sposób, na rzecz *this, a więc obiektu klasy pochodnej  B, który pojawił się po lewej stronie przypisania, wywołujemy jawnie metodę operator=() z klasy bazowej kwalifikując nazwę za pomocą operatora zakresu. Posyłamy jako argument przez referencję obiekt b, w więc ten, który pojawił się po prawej stronie przypisania. Ten sam efekt można uzyskać też tak:
       B& B::operator=(const B& b) {
           (A&)(*this) = b;
           // ...
           return *this;
       }
W tej metodzie referencja do obiektu *this klasy B została zrzutowana w górę do typu A&: tak więc przypisanie z trzeciej linii tego przykładu spowoduje automatycznie wywołanie metody operator=() z klasy bazowej A, podobnie jak w przypadku poprzednim. Oczywiście, zamiast rzutowania w stylu C, czyli za pomocą (A&) możemy użyć operatora rzutowania w stylu C++, czyli trzecią linię powyższego przykładu zastąpić przez
       static_cast<A&>(*this) = b;
Wreszcie, równoważnie, można użyć rzutowania wskaźników i następnie wyłuskania (dereferencji) obiektu:
       B& B::operator=(const B& b) {
           *((A*)this) = b;
           // ...
           return *this;
       }
choć wygląda to chyba najmniej czytelnie.

W poniższym programie zademonstrowane są przeciążenia operatora przypisania, konstruktory, w tym kopiujące, i destruktory dla dwóch klas: Osoba i dziedziczącej z niej klasy Pracownik. W obu klasach istnieją pola wskaźnikowe, a więc prawidłowe zdefiniowanie konstruktorów kopiujących, destruktorów i operatorów przypisania jest niezbędne.


P162: inh.cpp     Klasy z polami wskaźnikowymi: dziedzieczenie

      1.  #include <iostream>
      2.  #include <cstring>
      3.  using namespace std;
      4.  
      5.  class Osoba {
      6.      char* nazwis;
      7.  public:
      8.      Osoba()
      9.          : nazwis(strcpy(new char[14], "Nazw.Nieznane"))
     10.      {
     11.          cout << "Konstr. domyslny Osoba: "
     12.               << nazwis << endl;
     13.      }
     14.  
     15.      Osoba(const char* n)
     16.          : nazwis(strcpy(new char[strlen(n)+1], n))
     17.      {
     18.          cout << "Konstr. char* Osoba: " << nazwis << endl;
     19.      }
     20.  
     21.      Osoba(const Osoba& os)
     22.          : nazwis(strcpy(new char[strlen(os.nazwis)+1],
     23.                                             os.nazwis))
     24.      {
     25.          cout << "Konstr. kopiujacy Osoba: "
     26.               << nazwis << endl;
     27.      }
     28.  
     29.      Osoba& operator=(const Osoba& os) {
     30.          if ( this != &os ) {
     31.              delete [] nazwis;
     32.              nazwis = strcpy(new char[strlen(os.nazwis)+1],
     33.                                                 os.nazwis);
     34.              cout << "Przypisanie Osoba: "
     35.                   << nazwis << endl;
     36.          }
     37.          return *this;
     38.      }
     39.  
     40.      ~Osoba() {
     41.          cout << "Usuwamy Osoba: " << nazwis << endl;
     42.          delete [] nazwis;
     43.      }
     44.  
     45.      const char* getNazwisko() const { return nazwis; }
     46.  };
     47.  
     48.  class Pracownik : public Osoba {
     49.      char* funkcja;
     50.  public:
     51.      Pracownik()
     52.          : funkcja(strcpy(new char[14], "Stan.Nieznane"))
     53.      {
     54.          cout << "Konstruktor domyslny Pracownik: "
     55.               << funkcja << endl;
     56.      }
     57.  
     58.      Pracownik(const char* s, const char* n)
     59.          : Osoba(n),funkcja(strcpy(new char[strlen(s)+1], s))
     60.      {
     61.          cout << "Konstruktor char* char* Pracownik: "
     62.               << funkcja << endl;
     63.      }
     64.  
     65.      Pracownik(const Pracownik& prac)
     66.          : Osoba(prac), funkcja(strcpy(new
     67.                char[strlen(prac.funkcja)+1],prac.funkcja))
     68.      {
     69.          cout << "Konstruktor kopiujacy Pracownik: "
     70.               << funkcja << endl;
     71.      }
     72.  
     73.      Pracownik& operator=(const Pracownik& prac) {
     74.          if ( this != &prac ) {
     75.              (Osoba&)(*this) = prac;
     76.              delete [] funkcja;
     77.              funkcja = strcpy(new
     78.                  char[strlen(prac.funkcja)+1],
     79.                  prac.funkcja);
     80.              cout << "Przypisanie Pracownik: "
     81.                   << funkcja << endl;
     82.          }
     83.          return *this;
     84.      }
     85.  
     86.      ~Pracownik() {
     87.          cout << "Usuwamy Pracownik: " << funkcja << endl;
     88.          delete [] funkcja;
     89.      }
     90.  
     91.      const char* getFunkcja() const { return funkcja; }
     92.  };
     93.  
     94.  int main() {
     95.      cout << "\nMain: Tworzymy obiekt nem" << endl;
     96.      Pracownik nem;
     97.      cout << "Main: obiekt nemo utworzony: "
     98.           << nem.getFunkcja() << " "
     99.           << nem.getNazwisko()   << endl;
    100.  
    101.      cout << "\nMain: Tworzymy obiekt mal" << endl;
    102.      Pracownik mal("Szef", "Malinowski");
    103.      cout << "Main: obiekt mal utworzony: "
    104.           << mal.getFunkcja() << " "
    105.           << mal.getNazwisko()   << endl;
    106.  
    107.      cout << "\nMain: Kopiujemy mal -> kop" << endl;
    108.      Pracownik kop(mal);
    109.      cout << "Main: obiekt kop utworzony: "
    110.           << kop.getFunkcja() << " "
    111.           << kop.getNazwisko()   << endl;
    112.  
    113.      cout << "\nMain: Przypisujemy nem = kop" << endl;
    114.      nem = kop;
    115.      cout << "Main: nem = kop przypisane: "
    116.           << nem.getFunkcja()  << " "
    117.           << nem.getNazwisko() << endl << endl;
    118.  }

Zauważmy w liniach 59 i 66 jawne wywołania konstruktora klasy bazowej Osoba z listy inicjalizacyjnej konstruktorów klasy pochodnej Pracownik. W linii 75 z kolei następuje, w ciele metody przeciążającej operator przypisania, jawne wywołanie analogicznej metody z klasy bazowej. Wydruk tego programu
    Main: Tworzymy obiekt nem
    Konstr. domyslny Osoba: Nazw.Nieznane
    Konstruktor domyslny Pracownik: Stan.Nieznane
    Main: obiekt nemo utworzony: Stan.Nieznane Nazw.Nieznane

    Main: Tworzymy obiekt mal
    Konstr. char* Osoba: Malinowski
    Konstruktor char* char* Pracownik: Szef
    Main: obiekt mal utworzony: Szef Malinowski

    Main: Kopiujemy mal -> kop
    Konstr. kopiujacy Osoba: Malinowski
    Konstruktor kopiujacy Pracownik: Szef
    Main: obiekt kop utworzony: Szef Malinowski

    Main: Przypisujemy nem = kop
    Przypisanie Osoba: Malinowski
    Przypisanie Pracownik: Szef
    Main: nem = kop przypisane: Szef Malinowski

    Usuwamy Pracownik: Szef
    Usuwamy Osoba: Malinowski
    Usuwamy Pracownik: Szef
    Usuwamy Osoba: Malinowski
    Usuwamy Pracownik: Szef
    Usuwamy Osoba: Malinowski
demonstruje kolejność wywoływania konstruktorów i metod przeciążających operator przypisania. Po zakończeniu programu wszystkie trzy obiekty są usuwane: co prawda wszystkie przechowują takie same dane, ale są to trzy niezależne obiekty, o czym świadczy fakt, że wszystkie wywołania destruktorów powiodły się. Widzimy, że przy konstrukcji obiektów klasy pochodnej Pracownik najpierw tworzone są podobiekty klasy Osoba. Natomiast podczas destrukcji najpierw wywoływany jest destruktor klasy Pracownik, a potem destruktor klasy bazowej Osoba.

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