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:
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. }
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.
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. }
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: Malinowskidemonstruje 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, 21 lutego 2016; 20:17