20.7 Wielodziedziczenie

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 KoloKwadrat. 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).


P168: multbas.cpp     Wielodziedziczenie

      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.  }

W liniach 65-68 definiujemy wolną (globalną) funkcję drukującą obiekty typu DoDruku. Zauważmy, że w tablicy wskaźników przesyłanej do tej funkcji podczas jej wywołania (linia 81) mogliśmy umieścić adresy obiektów typu OsobaKolo, ale nie Kwadrat, chociaż ta klasa też definiuje metodę druk. Nie dziedziczy jednak z  DoDruk, a więc wskaźnik do obiektu tej klasy nie może mieć typu statycznego DoDruku*. Program drukuje
    ** 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 25
Charakterystyczna 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, 25 lutego 2017; 22:31