Jak już wspominaliśmy, w C++ klasa może dziedziczyć z wielu klas bazowych. Jest to narzędzie pozwalające na tworzenie skomplikowanych hierarchii dziedziczenia i daje spore możliwości programistyczne. Z drugiej jednak strony, zbytnie skomplikowanie struktury klas dziedziczących prowadzi wtedy do trudnego do opanowania i modyfikowania kodu. W zasadzie wielodziedziczenia należy zatem unikać, jeśli nie jest niezbędne. Poniżej podamy tylko podstawowe informacje na temat dziedziczenia wielobazowego — szczegółów należy szukać w bardziej zaawansowanych podręcznikach.
Definiując klasę dziedziczącą z wielu klas wymieniamy je na liście dziedziczenia po kolei, oddzielając przecinkami. Dla każdej z nich z osobna podajemy specyfikator dostępu (private, protected lub public). Opuszczenie tego specyfikatora jest równoważne podaniu specyfikatora private dla klas, a public dla struktur. Wszystkie klasy występujące na liście dziedziczenia muszą być kompilatorowi znane, — nie wystarczy deklaracja zapowiadająca.
Tak więc
class C public A, B { // ... };deklaruje klasę C dziedziczącą publicznie z klasy A oraz prywatnie z klasy B. W zasadzie dopuszczalne jest, aby obie klasy bazowe zawierały składniki o tej samej nazwie: w klasie pochodnej trzeba się wtedy do takich składników odnosić poprzez nazwę kwalifikowaną nazwą klasy (z czterokropkiem). Jednak trzeba pamiętać, że użycie kwalifikowanych nazw wyłącza polimorfizm.
Obiekty klasy pochodnej będą zawierać podobiekty wszystkich klas bazowych. Podczas tworzenia obiektów klasy pochodnej można więc jawnie wywoływać konstruktory dla dziedziczonych podobiektów; w przeciwnym przypadku użyte będą konstrukory domyślne. Oczywiście, jak zwykle, konstruktory klas bazowych mogą być wywołane tylko poprzez użycie listy inicjalizacyjnej.
W poniższym przykładzie definiujemy abstrakcyjną klasę
DoDruku
opisującą funkcjonalność „bycia drukowalnym”.
Klasa ma jedno pole określające strumień wyjściowy oraz jedną
czysto wirtualną metodę
druk. Mamy również standardową
klasę
Osoba, dziedziczącą z
DoDruku, a co za tym
idzie definiującą metodę
druk. Zauważmy, że na
liście inicjalizacyjnej konstruktora tej klasy wywołujemy jawnie
konstruktor
DoDruku
(linia 21) podając jako argument
odpowiedni strumień (domyślnie jest to
cout).
Definiujemy też klasę abstrakcyjną
Figura
i dwie
dziedziczące z niej klasy
Kolo
i
Kwadrat. Obie
implementują metodę
druk, ale tylko
Kolo
dziedziczy również z
DoDruku, a zatem na liście
inicjalizacyjnej jej konstruktora pojawiają się jawne wywołania
dwóch klas bazowych (linia 43).
1. #include <iostream> 2. #include <string> 3. #include <cmath> 4. using namespace std; 5. 6. class DoDruku { 7. protected: 8. ostream& str; 9. public: 10. DoDruku(ostream& str) 11. : str(str) 12. { } 13. virtual void druk() const = 0; 14. }; 15. 16. class Osoba : public DoDruku { 17. string imie; 18. int ur; 19. public: 20. Osoba(string i, int u, ostream& str = cout) 21. : DoDruku(str), imie(i), ur(u) 22. { } 23. 24. void druk() const { 25. str << imie + " (" << ur << ")" << endl; 26. } 27. }; 28. 29. class Figura { 30. protected: 31. static const double PI; 32. string name; 33. public: 34. virtual double getPole() const = 0; 35. Figura(string name) : name(name) { } 36. }; 37. const double Figura::PI = 4*atan(1.); 38. 39. class Kolo : public Figura, public DoDruku { 40. double prom; 41. public: 42. Kolo(string n, double r, ostream& str = cout) 43. : Figura(n), DoDruku(str), prom(r) 44. { } 45. double getPole() const { return PI*prom*prom; } 46. void druk() const { 47. str << "kolo " << name << " o promieniu " 48. << prom << " i polu " << getPole() << endl; 49. } 50. }; 51. 52. class Kwadrat : public Figura { 53. double side; 54. public: 55. Kwadrat(string n, double s) 56. : Figura(n),side(s) 57. { } 58. double getPole() const { return side*side; } 59. void druk() const { 60. cout << "kwadrat " << name << " o boku " 61. << side << " i polu " << getPole() << endl; 62. } 63. }; 64. 65. void drukTable(DoDruku* tab[], int size) { 66. for (int i = 0; i < size; ++i) 67. tab[i]->druk(); 68. } 69. 70. int main() { 71. Kolo ci1("pierwsze",2,cout), 72. *pci2 = new Kolo("drugie",3); 73. Kwadrat sq1("pierwszy",4), 74. *psq2 = new Kwadrat("drugi",5); 75. Osoba ps1("Jim",1972), 76. *pps2 = new Osoba("Tom",1978,cout); 77. 78. DoDruku* tab[] = {&ci1, &ps1, pci2, pps2}; 79. 80. cout << "** Drukowanie obiektow typu DoDruku" << endl; 81. drukTable(tab,4); 82. 83. cout << "** Drukowanie kwadratow" << endl; 84. sq1.druk(); 85. psq2->druk(); 86. 87. delete pci2; 88. delete psq2; 89. delete pps2; 90. }
** Drukowanie obiektow typu DoDruku kolo pierwsze o promieniu 2 i polu 12.5664 Jim (1972) kolo drugie o promieniu 3 i polu 28.2743 Tom (1978) ** Drukowanie kwadratow kwadrat pierwszy o boku 4 i polu 16 kwadrat drugi o boku 5 i polu 25Charakterystyczna w powyższym przykładzie była klasa abstrakcyjna DoDruku. Deklaruje ona jedną czysto wirtualną metodę określającą pewną funkcjonalność klas, często zupełnie niezwiązanym, z niej dziedziczących: wszystkie na pewno zawierać będą metodę druk pozwalającą na drukowanie informacji o obiekcie. Takie proste klasy, zwykle abstrakcyjne, deklarujące pewną ogólną funkcjonalność, nazywamy klasami domieszkowymi (ang. mix-in class); odpowiadają one z grubsza interfejsom z Javy. O ile skomplikowane wielodziedziczenie, ogólnie rzecz biorąc, nie jest zalecane, jeśli tylko można go uniknąć, to dodawanie do listy dziedziczenia prostych klas domieszkowych jest stosunkowo bezpieczne i często bardzo wygodne.
T.R. Werner, 21 lutego 2016; 20:17