Wprowadzenie do programowanie
Wprowadzenie do programowanie
obiektowego w języku C++
obiektowego w języku C++
Dziedziczenie wielobazowe
Część czwarta
Roman Simiński
roman.siminski@us.edu.pl
www.us.edu.pl/~siminski
Autor KontaktKoncepcja dziedziczenia wielobazowego
Koncepcja dziedziczenia wielobazowego
Dziedziczenie wielobazowe ― o co chodzi?
Dziedziczenie wielobazowe pozwala na tworzenie klas potomnych, dziedziczących z
więcej niż jednej klasy bazowej.
Mając np. dwie, zaimplementowane klasy, można zbudować nową klasę, dziedziczącą
jednocześnie właściwości obu tych klas, co zwykle nie wymaga wprowadzania zmian
w ich kodzie lub zmiany te są nieznaczne.
Samochód Łódka Amfibia Stół Okrąg OkrągłyStół ListBox EditLine ComboBox
Koncepcja dziedziczenia wielobazowego
Koncepcja dziedziczenia wielobazowego
Za i przeciw dziedziczeniu wielobazowemu
Tak — pozwala programiście budować elastyczne i dobrze odwzorowujące rzeczywistość
hierarchie klas.
Nie — zmusza programistę do większej uwagi, zwiększa złożoność języka oraz
kompilatora.
W wielu językach programowania zrezygnowano z dziedziczenia wielobazowego:
✔
jest np. w językach C++, Pyton,
Dziedziczenie wielobazowe
Dziedziczenie wielobazowe
―
―
syntaktyka
syntaktyka
Anatomia dziedziczenia wielobazowego
class A { public: int aMember; }; class B { public: int bMember; };
class C : public A, public B { public: int cMember; }; . . . C obj; obj.aMember = 0; obj.bMember = 1; obj.cMember = 2;
Obiekt obj klasy C składa się z pól:
• aMember,
• bMember,
• cMember,
a ich kolejność jest taka, jak kolejność
nazw klas w deklaracji klasy C.
Dziedziczenie wielobazowe
Dziedziczenie wielobazowe
―
―
syntaktyka
syntaktyka
Aktywowanie konstruktorów
Kolejność aktywowania konstruktorów dla obiektu klasy pochodnej wynika z kolejności
występowania nazw klas bazowych w deklaracji tej klasy.
Nie jest istotna kolejność ich umieszczenia na liście inicjalizacyjnej konstruktora klasy
pochodnej.
Specjalnie traktowane są wirtualne klasy bazowe — ale o tym później.
Kolejność aktywowania destruktorów jest odwrotna do kolejności stosowanej w
przy-padku konstruktorów.
Dziedziczenie wielobazowe
Dziedziczenie wielobazowe
―
―
syntaktyka
syntaktyka
Aktywowanie konstruktorów
class A { public: A() : aMember( 0 ) {cout << "Konstruktor: A()" << endl; }
~A() {
cout << "Destruktor: ~A()" << endl; } int aMember; }; class B { public: B() : bMember( 0 ) {
cout << "Konstruktor: B()" << endl; }
~B() {
cout << "Destruktor: ~B()" << endl; }
int bMember; };
Dziedziczenie wielobazowe
Dziedziczenie wielobazowe
―
―
syntaktyka
syntaktyka
Aktywowanie konstruktorów, cd. ...
class C : public A, public B {
public:
C() : A(), B(), cMember( 0 ) {
cout << "Konstruktor: C()" << endl; }
~C() {
cout << "Destruktor: ~C()" << endl; } int cMember; }; int main() { C obj; obj.aMember = 0; obj.bMember = 1; obj.cMember = 2; return 0; }
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Składowe o tej samej nazwie
class A { public: int member; void memberFunction() {}; }; class B { public: int member; void memberFunction() {}; };
class C : public A, public B { public: int cMember; void cMemberFunction(); }; . . . C obj; . . . member member cMember Klasa A Klasa B Klasa C A +member +memberFunction() B +member +memberFunction() C +cMember +cMemberFunction()
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Składowe o tej samej nazwie, cd. ...
obj.cMember = 10; obj.cMemberFunction();
Odwołania jednoznaczne:
obj.member = 100; obj.memberFunction();Odwołanie niejednoznaczne:
Request for member 'member' is ambiguous, candidates are: int B::member is int A::member
Błąd kompilacji (np. Dev-C):
Member is ambiguous: 'A::member' and 'B::member'
Member is ambiguous: 'A::memberFunction' and 'B::memberFunction'
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Składowe o tej samej nazwie, wykorzystanie operatora zasięgu ::
obj.A::member = 100; obj.B::member = 200;
cout << obj.A::member << endl; obj.A::memberFunction();
cout << obj.B::member << endl; obj.B::memberFunction();
Odwołania jednoznaczne:
C * ptr; ptr = new C; ptr->A::member = 100; ptr->B::member = 200;cout << ptr->A::member << endl; ptr->A::memberFunction();
cout << ptr->B::member << endl; ptr->B::memberFunction();
delete ptr;
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Składowe o tej samej nazwie, przykład — klasa Amphibian
class Vehicle {
public:
Vehicle( int maxSpeed = 0, int numOfWheels = 0 ) : maxSpeed( maxSpeed ), numOfWheels( numOfWheels ) { } protected: int maxSpeed; int numOfWheels; }; class Boat { public:
Boat( int maxSpeed = 0, int displacement = 0 )
: maxSpeed( maxSpeed ), displacement( displacement ) { } protected: int maxSpeed; int displacement; };
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Składowe o tej samej nazwie, przykład — klasa Amphibian, cd. ...
class Amphibian : public Vehicle, public Boat {
public:
Amphibian( int maxLandSpeed = 0, int maxWaterSpeed = 0, int numOfWheels = 0, int displacement = 0 ) : Vehicle( maxLandSpeed, numOfWheels ),
Boat( maxWaterSpeed, displacement ) {
}
int getMaxLandSpeed() const { return Vehicle::maxSpeed; } int getMaxWaterSpeed() const { return Boat::maxSpeed; } int getNumOfWheels() const { return numOfWheels; } int getDisplacement() const { return displacement; } };
. . .
Amphibian a( 120, 20, 4, 1500 );
cout << endl << "Maks. predk. na ladzie : " << a.getMaxLandSpeed(); cout << endl << "Maks. predk. na wodzie : " << a.getMaxWaterSpeed(); cout << endl << "Liczba kol : " << a.getNumOfWheels();
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Problemów ciąg dalszy
Bezpośrednia klasa bazowa klasy A — klasa, z której klasa A dziedziczy.
Pośrednia klasa bazowa klasy A — klasa, z której dziedziczy bezpośrednia klasa bazowa
Pewien potomek (klasa pochodna) może mieć „rodziców” (klasy bazowe) mających
wspólnego przodka (wspólną klasę bazową).
Powoduje to pewne komplikacje dla potomka.
Samochód Łódka Amfibia ListBox EditLine ComboBox Środek Transportu Środek Transportu Control Control
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie ze wspólną, pośrednią klasą bazową
class A { public: int member; void memberFunction() {}; }; class B : public A { public: int bMember; }; class C : public A { public: int cMember; };
class D : public B, public C {
public:
int dMember; };
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie ze wspólną, pośrednią klasą bazową
W przypadku dziedziczenia wielobazowego po klasach mających wspólna klasę bazową,
klasa pochodna posiada w sobie wielokrotnie składowe występujące we wspólnej klasie
bazowej.
D obj;
obj.member = 100;
obj.memberFunction();
Odwołania niejednoznaczne (błędy kompilacji):
obj : tylko pola obiektu member bMember member cMember dMember Klasa A Klasa A Klasa B Klasa C Klasa D A +member +memberFunction() B +bMember C +cMember D +dMember A +member +memberFunction()
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Usuwanie niejednoznaczności — ponownie operator zakresu
W tym przypadku wystąpienie kopii pól jest semantycznie niepoprawne. Powoduje
to również stratę pamięci.
D obj; obj.B::member = 100; obj.C::memberFunction(); D * ptr = new D; ptr->B::member = 100; ptr->C::memberFunction(); . . .
Problem z wykorzystaniem wskaźników do klasy bazowej
A * ptr1= new D; // Bł dą
A * ptr2= new B; // OK
A * ptr3= new C; // OK
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie w trybie wirtualnym — klasy bazowe virtual
Dziedziczenie w trybie wirtualnym ma na celu wyeliminowanie niejednoznaczności
przy wielokrotnym dziedziczeniu ze wspólnych klas bazowych.
Zadeklarowanie klasy bazowej jako wirtualnej informuje kompilator, że ma umieścić
w klasach pochodnych tylko jedno wystąpienie składowych wirtualnej klasy bazowej.
Samochód
Łódka
Amfibia
ListBox
EditLine
ComboBox
Środek
Transportu
Control
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Anatomia dziedziczenia w trybie wirtualnym
class A { public: int member; void memberFunction() {}; };
class B : virtual public A {
public:
int bMember; };
class C : virtual public A {
public:
int cMember; };
class D : public B, public C {
public:
int dMember; };
Kolejność występowania słów
kluczowych public i virtual
jest dowolna.
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Anatomia dziedziczenia w trybie wirtualnym
D obj;
obj.member = 100; // Teraz tylko jedno wyst pienie tego polaą obj.memberFunction(); // Teraz tylko jedno wyst pienie tej funkcjią . . .
obj.B::member = 100; // Tak mo na było poprzednio i teraz również ż obj.C::memberFunction(); // j.w.
. . .
Odwołania jednoznaczne
Dziedziczenie wirtualne — aktywowanie konstruktorów
Konstruktory klas wirtualnych są aktywowane przed konstruktorami klas
niewirtualnych. Gdy klas wirtualnych jest więcej, kolejność aktywacji odpowiada
kolejności deklaracji.
Jeżeli klasa wirtualna dziedziczy z klasy niewirtualnej, konstruktor tej ostatniej będzie
aktywowany wcześniej.
Konstruktory pozostałych niewirtualnych klas bazowych są aktywowane zgodnie z
kolejnością deklaracji.
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie wirtualne — aktywowanie konstruktorów, cd. ...
Jeżeli w hierarchii klas występują jednocześnie wirtualne i niewirtualne wystąpienia
tej samej klasy, konstruktor tej klasy jest wywoływany raz dla wszystkich wystąpień
wirtualnych oraz raz dla każdego niewirtualnego wystąpienia klasy
class A {}; class B {}; class C: public B, virtual public A {}; class D: public B, virtual public A {}; class E: public C, virtual public D {}; class A {};
class B: virtual public A {}; class C: virtual public A {}; class D: public A {}; class E: public B, public C, public D {}; C D E A B B B C E A A D
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie wirtualne — aktywowanie konstruktorów, cd. ...
class A {
public:
A() : a( 0 ) { cout << "Default A: a = 0" << endl; } A( int a ) : a( a )
{
cout << "General A: a = " << a << endl; }
int a; };
class B: virtual public A {
public:
B() : A() { cout << "Default B" << endl; } B( int a, int b ) : A( a ), b( b )
{
cout << "General B: b = " << b << endl; }
int b; };
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Dziedziczenie wirtualne — aktywowanie konstruktorów, cd. ...
class C: virtual public A {
public:
C() : A() { cout << "Default C"; } C( int a, int c ) : A( a ), c( c ) {
cout << "General C: c = " << c << endl; }
int c; };
class D: public B, public C {
public:
D() : B(), C() { cout << "Default D"; }
D( int a, int b, int c, int d ) : B( a, b ), C( a, c ), d( d ) {
cout << "General D: d = " << d << endl; }
int d; };
. . .
D obj( 1, 2, 3, 4 );
cout << "a: " << obj.a << endl; cout << "b: " << obj.b << endl; cout << "c: " << obj.c << endl; cout << "d: " << obj.d << endl;
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Niby wszystko w porządku ale … :-/
Dlaczego pole a wirtualnej klasy bazowej A ma
wartość 0?
Przecież:
D obj( 1, 2, 3, 4 );
Co się dzieje?
Dlaczego pole a ma wartość 0? Jak nadać mu właściwą wartość?
Przecież:
Normalnie programista nie musi się przejmować inicjalizacją składowych
odziedziczonych po pośrednich klasach bazowych (czyli A dla klasy D).
Zajmują się tym konstruktory bezpośrednich i występujących wcześniej w hierarchii
pośrednich klas bazowych (czyli B i C dla klasy D).
Istnieją jednak dwie drogi realizacji inicjalizacji — poprzez klasę B lub poprzez klasę C.
Ponownie mamy niejednoznaczność, która klasa — B czy C — jest odpowiedzialna za
inicjalizację pola a?
Problemy z dziedziczeniem wielobazowym
Problemy z dziedziczeniem wielobazowym
Wirtualne klasy bazowe wymagają specjalnej inicjalizacji
Z powodu niejednoznaczności, w przypadku dziedziczenia wielobazowego z klasą
wirtualną:
Klasa pochodna musi zainicjalizować jawnie składowe wirtualnej klasy bazowej.
class D: public B, public C {
public: . . .
D( int a, int b, int c, int d ) : A( a ), B( a, b ), C( a, c ), d( d ) {
cout << "General D: d = " << d << endl; }
};