Relacje między klasami - dziedziczenie
Dziedziczenie (ang. inheritance) - jest to ustanowienie związku pomiędzy klasami, polegającego na przejmowaniu właściwości jednej klasy (tzw. klasy podstawowej, bazowej, ang. base class) przez inną klasę (tzw. klasę pochodną. ang. derived class).
Jeżeli zdefiniujemy taki związek, to automatycznie obiekty klasy pochodnej zawierają:
składowe odziedziczone z klasy bazowej (określone w definicji klasy bazowej)
składowe własne klasy pochodnej (określone w definicji klasy pochodnej)
Można powiedzieć, że obiekty klasy pochodnej są szczególnymi przypadkami obiektów innej klasy.
Dziedziczenie określa się w definicji klasy pochodnej:
Klasa pochodna może dziedziczyć z więcej niż jedna klasy bazowej.
Jeśli klasa dziedziczy tylko atrybuty jednej klasy, mówimy o dziedziczeniu pojedynczym.
Jeśli klasa dziedziczy atrybuty dwóch lub więcej innych klas, mówmy o dziedziczeniu wielokrotnym (wielobazowym,
Przykład: punkt i jego specjalizacja - punkt kolorowy.
Dziedziczenie opisuje relację "jest": punkt kolorowy jest punktem
// klasa bazowa
class Punkt {
private:
int x;
int y;
public:
void inicjalizuj(int, int);
void przesun(int,int);
void drukuj () ;
} ;
// klasa pochodna
class Punktkol : public Punkt
{ short kolor ; // nowa składowa
public :
void koloruj(short); // nowa funkcja
} ;
w main() :
Punktkol a;
// można się odwoływać zarówno do składowych klasy Punkt
// (publicznych) jak i klasy Punktkol (również publicznych)
a.inicjalizuj(1,1); // przypisanie wartości współrzędnych punktu:
// funkcja klasy bazowej
a.koloruj(2); // przypisanie koloru:
// funkcja klasy pochodnej
a.drukuj(); // drukowanie współrzędnych
// funkcja klasy bazowej
Dziedziczenie nie zmienia zasad dostępu do składowych klasy bazowej: funkcje klasy pochodnej nie mają dostępu do składowych prywatnych klasy bazowej.
W klasie pochodnej określany jest tryb dziedziczenia, który określa zasady dostępu do składowych funkcji bazowej dla obiektów i funkcji zaprzyjaźnionych klasy pochodnej. Tryb public (publiczny) oznacza, że składowe publiczne klasy bazowej będą również składowymi publicznymi klasy pochodnej.
Dostęp do składowych klasy bazowej w klasie pochodnej
Jeśli składowe w klasie bazowej są zadeklarowane jako prywatne, klasa pochodna je dziedziczy ale nie ma do nich bezpośredniego dostępu.
Przykład: współrzędne punktu zdefiniowane w klasie bazowej są prywatne, klasa pochodna nie ma zatem dostępu do współrzędnych punktu, może jedynie korzystać z tych zmiennych za pomocą publicznych funkcji dostępu przewidzianych w klasie bazowej
class Punkt {
private :
int x;
int y;
public :
void inicjalizuj(int, int);
void przesun(int, int);
void drukuj ();
} ;
void Punkt::inicjalizuj(int xx, int yy)
{ x = xx ; y = yy; }
void Punkt::przesun(int dx, int dy)
{ x = x + dx ; y = y + dy ; }
void Punkt::drukuj ()
{ cout << "Moje wspolrzedne " << x << " " << y << endl ; }
/////////////////////////////////////////////////////////////////
class Punktkol : public Punkt // Punktkol jest klasą pochodną Punkt
{ short kolor;
public :
void koloruj (short kk) { kolor = kk ; }
void drukujkol();
void inicjalizujkol(int xx, int yy, int kk);
} ;
void Punktkol::drukujkol ()
{
drukuj(); // na rzecz którego obiektu jest ta funkcja wywołana?
cout << "i moj kolor " << kolor << "\n" ;
}
void Punktkol::inicjalizujkol (int xx, int yy, short kk)
{
inicjalizuj(xx,yy);
kolor=kk ;
}
W main():
Punktkol p ;
p.inicjalizujkol (1,1,3) ; // składowa klasy Punktkol
p.drukuj () ; // składowa klasy Punkt
p.przesun (2,4) ; // składowa klasy Punkt
p.koloruj (2) ; // składowa klasy Punktkol
p.drukujkol() ; // składowa klasy Punktkol
Nadpisywanie funkcji składowych w klasie pochodnej
Przykład: Funkcje drukuj() w klasie Punkt i drukujkol() w klasie Punktkol mają podobne działanie (wyświetlenie wartości składowych klasy). Czy możliwe jest nadanie im tej samej nazwy?
#include <iostream>
class Punkt
{
private :
int x ;
int y ;
public :
void inicjalizuj(int, int) ;
void przesun(int, int) ;
void drukuj () ;
} ;
void Punkt::inicjalizuj(int xx, int yy)
{
x = xx ; y = yy ;
}
void Punkt::przesun(int dx, int dy)
{
x = x + dx ; y = y + dy ;
}
void Punkt::drukuj ()
{
cout << "Moje wspolrzedne " << x << " " << y << "\n" ;
}
/////////////////////////////////////////////////////////////////
class Punktkol : public Punkt // Punktkol jest pochodną Punkt
{ short kolor ;
public :
void koloruj (short kk) { kolor = kk ; }
void drukuj(); // ponowna definicja drukuj()
void inicjalizuj(int, int, short); // ponowna definicja inicjalizuj()
};
void Punktkol::inicjalizuj(int xx, int yy, short kk)
{
Punkt::inicjalizuj(xx,yy); // funkcja inicjalizuj() klasy Punkt
kolor=kk;
}
void Punktkol::drukuj ()
{
Punkt::drukuj();
cout << "i moj kolor " << kolor << "\n" ;
}
W main():
Punktkol p ;
p.inicjalizuj (1,1,3) ;
p.drukuj () ; // funkcja drukuj() klasy Punktkol
p.Punkt::drukuj(); // funkcja drukuj() klasy Punkt
p.koloruj (2) ;
p.drukuj() ; // funkcja drukuj() klasy Punktkol
Nadpisywanie i przeciążanie
Funkcja nadpisana w klasie pochodnej zakrywa wszystkie funkcje o tej samej nazwie w klasie bazowej.
Przykład:
class A
{
...
public:
void f(int);
void f(char);
void g(int);
void g(char);
};
class B : public A
{
...
public:
void f(int);
void f(float);
};
W main() :
int n;
float x;
char c;
B b;
b.f(n); // wywołana funkcja B::f(int)
b.f(x); // wywołana funkcja B::f(float)
b.f(c); // wywołana funkcja B::f(int) - konwersja char na int
b.g(n); // wywołana funkcja A::g(int)
b.g(x); // błąd: niejednoznaczność A::g(int) czy A::g(char)
b.g(c); // wywołana funkcja A::g(char)
Wywoływanie konstruktorów i destruktorów
Brak dziedziczenia:
Jeśli istnieje przynajmniej jeden konstruktor użytkownika, do utworzenia obiektu wywołany zostanie konstruktor wybrany na podstawie argumentów dostarczonych w deklaracji obiektu. Jeśli nie zostanie znaleziony konstruktor o odpowiednim zestawie argumentów, obiekt nie będzie mógł być utworzony i wygenerowany zostanie błąd kompilacji.
Jeśli nie istnieje żaden konstruktor użytkownika, kompilator wygeneruje własny konstruktor i utworzy obiekt. W tym przypadku nie można jednak przesłać do konstruktora informacji o wartościach początkowych składowych obiektu.
Jeśli istnieje destruktor użytkownika, zostanie on wywołany przed usunięciem obiektu.
Istnieje klasa bazowa i klasa pochodna
Klasa bazowa i pochodna mają osobne konstruktory i destruktory.
Jeśli klasa bazowa posiada konstruktor, to musi on być wykonany przed konstruktorem klasy pochodnej.
Destruktory są wykonywane w odwrotnej kolejności: najpierw wywoływany jest destruktor klasy pochodnej, a potem destruktor klasy bazowej.
Załóżmy, że mamy dwie klasy:
class A { ... public: A(); ~A(); ... }; |
class B : public A { ... public: B(); ~B(); ... }; |
W main() tworzymy obiekt:
B obiekt;
Aby taki obiekt był utworzony, najpierw musi być utworzony obiekt klasy A (wywołanie konstruktora A), który następnie jest uzupełniany o składowe właściwe dla obiektu B (wywołanie konstruktora B).
Podczas usuwania obiektu, postępowanie jest odwrotne: najpierw jest wywoływany destruktor obiektu B, a dopiero potem destruktor obiektu A.
Taki schemat postępowania jest realizowany automatycznie przez kompilator C++.
Przekazywanie argumentów konstruktorowi klasy bazowej
Jeśli klasa bazowa nie zawiera konstruktora z parametrami, klasa pochodna nie musi wykonywać żadnych specjalnych działań.
Jeśli jednak konstruktor klasy bazowej wymaga przekazania mu argumentów, to klasa pochodna musi mu ich dostarczyć.
#include <iostream>
class Punkt {
int x, y ;
public :
Punkt (int xx=0, int yy=0)
{ x = xx ; y =yy ; }
...
} ;
class Punktkol : public Punkt {
short kolor ;
public :
Punktkol (int, int, short) ;
...
} ;
// Punktkol ma przesłać otrzymane wartości do Punkt
Punktkol::Punktkol (int xx=0, int yy=0, short kk=1) : Punkt (xx, yy)
{ kolor = kk ; }
W main() :
Punktkol a(1,5,3) ; // wywołanie konstruktora Punkt z 1 i 5,
// następnie wywołanie konstruktora Punktkol z 1,5 i 3
Punktkol b (2,3) ; // wywołanie konstruktora Punkt z 2 i 3
// następnie wywołanie konstruktora Punktkol z 1,5 i 1
Punktkol c (12) ; // wywołanie konstruktora Punkt z 1 i 0,
// następnie wywołanie konstruktora Punktkol z 1,0 i 1
Pytanie: Co będzie w przypadku, gdy:
klasa bazowa nie ma konstruktora, klasa pochodna ma taki konstruktor
klasa bazowa ma konstruktor, klasa pochodna nie ma konstruktora (rozważyć dwa przypadki: klasa bazowa ma konstruktor bez parametrów, klasa bazowa ma konstruktor z parametrami).
Przykład
#include <iostream.h>
// klasa bazowa
class Prostokat {
protected:
double dlugosc;
double szerokosc;
double powierzchnia;
public:
Figura(double d=0, double s=0)
{ dlugosc=d; szerokosc=s;
powierzchnia=dlugosc*szerokosc; }
void DrukujPow();
};
// klasa pochodna
class Prostopadloscian : public Prostokat {
protected :
double glebokosc;
double pojemnosc;
public:
Prostopadloscian (double z=0, double x=0, double y=0) : Prostokat(x,y)
{ glebokosc = z;
pojemnosc=glebokosc*dlugosc*szerokosc; }
void DrukujPoj();
};
void Prostokat::DrukujPow(void)
{ cout << "Powierzchnia wynosi : "<<powierzchnia<<endl; }
void Prostopadloscian::DrukujPoj()
{ cout << "Pojemnosc wynosi : "<<pojemnosc<<endl; }
int main(){
int x,y,z;
cout << "Podaj dlugosc: ";
cin >> x;
cout << "Podaj szerokosc: ";
cin >> y;
cout << "Podaj glebokosc: ";
cin >> z;
Prostopadloscian a(z,x,y); // utwórz prostopadłościan a
a.DrukujPoj(); // drukuj pojmność prostopadłościana a
return 0;
}
Nadzorowanie dostępu do składowych klasy
W klasie może określić następujące poziomy dostępu.
class X {
private:
// część prywatna: nie podlega dziedziczeniu
// dostęp mają tylko funkcje składowy klasy bazowej
// i funkcje z nią zaprzyjaźnione
...
protected:
// część chroniona: dziedziczona przez klasy pochodne
// dostęp mają:
// funkcje składowe klasy bazowej i funkcje z nią zaprzyjaźnione
// funkcje klasy pochodnej i funkcje zaprzyjaźnione z klasą pochodną
// brak dostępu dla pozostałych funkcji
...
public:
// część publiczna: dziedziczona przez klasy pochodne
// dostęp mają wszystkie funkcje programu
...
};
Pytanie: Jakie są zalety składowych chronionych? Jakie niebezpieczeństwa niesie ze sobą stosowanie składowych chronionych?
Przykład: zmieńmy typ współrzędnych punktu w klasie Punkt na chronione. Jak będzie można to wykorzystać?
// Wersja 1: współrzędne punktu są prywatne
class Punkt {
private :
int x, y ;
public :
void Punkt(int,int);
void drukuj () ;
...
} ;
void Punkt::drukuj ()
{
cout << "Moje wspolrzedne " << x << " " << y << "\n" ;
}
/////////////////////////////////////////////////////////////////
class Punktkol : public Punkt // Punktkol jest pochodną Punkt
{ short kolor ;
public :
void drukuj();
...
};
void Punktkol::drukuj ()
{
Punkt::drukuj();
cout << "i moj kolor " << kolor << "\n" ;
}
// Wersja 2: współrzędne punktu są chronione
class Punkt {
protected :
int x, y;
public :
void Punkt(int,int);
void drukuj () ;
...
} ;
void Punkt::drukuj ()
{
cout << "Moje wspolrzedne " << x << " " << y << "\n" ;
}
/////////////////////////////////////////////////////////////////
class Punktkol : public Punkt // Punktkol jest pochodną Punkt
{ short kolor ;
public :
void drukuj();
...
};
void Punktkol::drukuj ()
{
cout << "Moje wspolrzedne " << x << " " << y << "\n" ;
cout << "i moj kolor " << kolor << "\n" ;
}
Sterowanie dostępem do składowych klasy
Zasada 1: Klasa pochodna ma dostęp tylko do składowych publicznych i chronionych klasy bazowej.
Zasada 2: W klasie pochodnej można określić sposób przekazywania odziedziczonych składowych do kolejnych klas tworzonych jako klasy pochodne klasy pochodnej.
Składnia deklaracji klasy pochodnej ma zatem postać:
class nazwa_klasy_pochodnej : tryb_dziedziczenia nazwa_klasy_bazowej
{
ciało klasy_pochodnej
};
Możliwe są trzy przypadki::
class XX : public X
{ ... };
Publiczne i chronione składowe klasy bazowej stają się odpowiednio publicznymi i chronionymi składowymi klasy pochodnej.
class XX : private X
{ ... };
Publiczne i chronione składowe klasy bazowej stają się składowymi prywatnymi klasy pochodnej. Oznacza to, że funkcje składowe klasy pochodnej mają dostęp do składowych publicznych i chronionych klasy bazowej, nie mają zaś takiego dostępu użytkownicy klasy pochodnej.
class XX : protected X
{ ... };
Publiczne i chronione składowe klasy bazowej stają się składowymi chronionymi klasy pochodnej.
W każdym z tych przypadków, składowe prywatne klasy bazowej pozostają składowymi prywatnymi klasy bazowej i nie są dostępne w klasie pochodnej.
Pominięcie trybu dziedziczenia jeśli klasa jest klasą właściwą oznacza tryb private, zaś jeśli jest strukturą tryb public .
Przykłady zastosowań trybu dziedziczenia
Najczęściej stosowany jest tryb publiczny.
Zastosowanie trybu prywatnego:
wszystkie funkcje z klasy bazowej zostały przedefiniowane w klasie pochodnej, nie ma po co zezwalać użytkownikom klasy pochodnej na korzystanie z funkcji klasy bazowej
Kompatybilność klasy bazowej i klasy pochodnej
Zasada: obiekt klasy pochodnej może „zastąpić” obiekt klasy bazowej. Czyli wszędzie tam, gdzie oczekuje się obiektu klasy bazowej, może być użyty obiekt klasy pochodnej. Stwierdzenie odwrotne nie jest prawdziwe.
Uwaga: zasada ta obwiązuje dla dziedziczenia publicznego.
Techniczna realizacja - zachodzi niejawna konwersja:
obiektu klasy pochodnej na obiekt klasy bazowej
wskaźnika (lub referencji) do klasy pochodnej na wskaźnik (lub referencję) do klasy bazowej.
Konwersja typu pochodnego na typ bazowy
class Punkt
{ ... };
class Punktkol : public Punkt
{ ... };
Punkt a;
Punktkol b;
a=b; // obcięcie do tego, co należy do klasy Punkt
// jeśli istnieje przeciążony operator =, to zostanie użyty,
// w przeciwnym wypadku zostanie użyty operator domyślny
Konwersja wskaźników
class Punkt
{ ... };
class Punktkol : public Punkt
{ ... };
Punkt a, *wsk_a=&a;
Punktkol b, *wsk_b=&b;
wsk_a=wsk_b; // dokonywana jest konwersja
Ograniczenia
class Punkt
{
void drukuj();
...
};
class Punktkol : public Punkt
{
void drukuj();
...
};
Punkt a(1,1), *wsk_a=&a;
Punktkol b(2,2,0), *wsk_b=&b;
Co będzie jeśli wykonamy podstawienie:
wsk_a=wsk_b;
Która funkcja drukuj() (funkcja Punkt::drukuj() czy Punktkol::drukuj() ) będzie użyta w instrukcji:
wsk_a->drukuj();
Decyzję podejmuje kompilator w momencie kompilacji. Jest to tzw. wiązanie statyczne (ang. early binding, static binding). Dla kompilatora ważny jest typ zmiennej.
Załóżmy, że zmienne klasy Punktkol są publiczne. Czy za pomocą wskaźnika wsk_a mielibyśmy teraz dostęp do koloru?
Przykład:
#include <iostream>
class Punkt
{ protected :
int x, y ;
public :
Punkt (int xx=0, int yy=0) { x=xx ; y=yy ; }
void drukuj ()
{ cout << "Jestem zwyklym punktem \n" ;
cout << " oto moje wspolrzedne : " << x << " " << y << "\n" ;
}
} ;
class Punktkol : public Punkt
{
short kolor ;
public :
Punktkol (int xx=0, int yy=0, short kk=1) : Punkt (xx, yy)
{ kolor = kk ;
}
void drukuj ()
{ cout << "Jestem punktem kolorowym \n" ;
cout << " oto moje wspolrzedne : " << x << " " << y ;
cout << " i moj kolor : " << kolor << "\n" ;
}
} ;
Co wydrukuje następujący program:
main()
{ Punkt p(1,2) ; Punkt * wsk = &p ;
Punktkol pk (5,6,1) ; Punktkol * wsk_k = &pk ;
wsk->drukuj () ; wsk_k->drukuj () ;
cout << "-----------------\n" ;
wsk = wsk_k ;
wsk->drukuj () ;
}
Przykład
#include <iostream>
#include <string.h>
class A {
char nazwisko[30];
public:
void wpisz_nazw(char *s) {strcpy(nazwisko,s); }
void drukuj_nazw() {cout << nazwisko << " ";}
};
class B : public A {
char telefon[20];
public:
void wpisz_tel(char *numer) {
strcpy(telefon, numer);
}
void drukuj_tel() {cout << telefon << " ";}
};
int main()
{
A *wsk_a;
A obiekt_A;
B *wsk_b;
B obiekt_B;
wsk_a=&obiekt_A;
wsk_a->wpisz_nazw("Adam Kowalski");
wsk_a=&obiekt_B;
wsk_a->wpisz_nazw("Piotr Nowak");
// sprawdzenie, czy poprawnie wpisano nazwiska
obiekt_A.drukuj_nazw();
obiekt_B.drukuj_nazw();
cout << endl;
wsk_b=&obiekt_B;
wsk_b->wpisz_tel("612-23-56");
wsk_a->drukuj_nazw();
wsk_b->drukuj_tel();
return 0;
}
Konstruktor kopiujący i dziedziczenie
Konstruktory nie są dziedziczone.
Przypadek 1: klasa pochodna nie ma konstruktora kopiującego.
Dla składowych własnych klasy B zastosowany zostanie konstruktor domyślny (kopiowanie płytkie), dla części pochodzącej z klasy A - konstruktor klasy A (zdefiniowany lub w przypadku jego braku domyślny).
Przypadek 2: klasa pochodna ma konstruktor kopiujący.
Zastosowany zostanie konstruktor kopiujący klasy B. Konstruktor ten jest w całości odpowiedzialny za obiekt, zarówno za własne składowe jak i za składowe odziedziczone. Konstruktor klasy A nie zostanie w tym wypadku automatycznie wywołany.
Jeśli chcemy użyć konstruktora kopiującego klasy A należy go wywołać jawnie. Składnia konstruktora kopiującego w tym przypadku będzie następująca:
B (B & x) : A(x)
{
// tutaj kopiowanie tylko części obiektu x z klasy B
...
}
Przykład:
#include <iostream>
class Punkt
{
int x, y ;
public :
Punkt (int xx=0, int yy=0) // zwykły konstruktor
{ x = xx ; y = yy ;
cout << "++ Konstruktor Punkt " << x << " " << y << "\n" ;
}
Punkt (Punkt & p) // konstruktor kopiujący
{ x = p.x ; y = p.y ;
cout << " Konstruktor kopiujacy Punkt " << x << " " << y << "\n" ;
}
} ;
class Punktkol : public Punkt
{
char kolor ;
public :
// zwykły konstruktor
Punktkol (int xx=0, int yy=0, int kk=1) : Punkt (xx, yy)
{ kolor = kk ;
cout << "++ Konstruktor Punktkol " << int(kolor) << "\n" ;
}
// konstruktor kopiujący
Punktkol (Punktkol & p) : Punkt (p)
{ kolor = p.kolor ;
cout << " Konstruktor kopiujacy Punktkol " << int(kolor) << "\n" ;
}
};
Jakie konstruktory i w jakiej kolejności zostaną użyte w następującym programie?
void fun (Punktkol pc)
{
cout << "*** Jestem w funkcji fun ***\n" ;
}
main()
{
void fun (Punktkol) ;
Punktkol a (2,3,4) ;
fun (a) ;
}
Operator przypisania = i dziedziczenie
Operator przypisania nie jest dziedziczony. Patrz jednak przypadek 1.
Przypadek 1: klasa pochodna nie ma przeciążonego operatora =
Dla składowych własnych klasy B zastosowany zostanie domyślny operator przypisania, dla części pochodzącej z klasy A - konstruktor klasy A (zdefiniowany lub w przypadku jego braku domyślny).
Przypadek 2: klasa pochodna ma przeciążony operator =
Zastosowany zostanie przeciążony operator przypisania klasy B. Operator ten jest w całości odpowiedzialny za obiekt, zarówno za własne składowe jak i za składowe odziedziczone. Operator klasy A nie zostanie w tym wypadku automatycznie wywołany.
Przykład (działanie niepoprawne): obydwie klasy mają przeciążone operatory =, ale w funkcji operator= klasy pochodnej nie uwzględniono składowych odziedziczonych z klasy bazowej
class Punkt
{
protected :
int x, y ;
public :
Punkt (int xx=0, int yy=0)
{ x=xx ; y=yy ;}
Punkt & operator = (Punkt & a) // przeciążony operator =
{ x = a.x ; y = a.y ;
return * this ;
}
} ;
class Punktkol : public Punkt
{
protected :
int kolor ;
public :
Punktkol (int xx=0, int yy=0, int kk=1) : Punkt (xx, yy) { kolor=kk ; }
Punktkol & operator = (Punktkol & b) // przeciążony operator =
{ kolor = b.kolor ;
return * this ;
}
void drukuj ()
{ cout << "Punktkol : " << x << " " << y << " " << kolor << endl ;
}
} ;
Co wydrukuj następujący program i dlaczego?
#include <iostream>
main()
{ Punktkol p(1,3,0) , q(2,4,1) ;
cout << "p = " ; p.drukuj () ;
cout << "q przed = " ; q.drukuj () ;
q = p ;
cout << "q po = " ; q.drukuj () ;
}
Aby operator przypisania w klasie pochodnej działał prawidłowo, trzeba powiedzieć jawnie co należy zrobić ze składowymi z klasy bazowej.
Prawidłowe przeciążenie operatora = w klasie pochodnej Punktkol:
Punktkol & operator = (Punktkol & b)
{
Punkt * wsk1, * wsk2 ;
wsk1 = this ; // konswersja wskaźnika do klasy Punktkol
// na wskaźnik do klasy Punkt
wsk2 = &b ;
* wsk1 = * wsk2; // przypisanie składowych odziedziczonych z klasy Punkt
kolor = b.kolor ; // przypisanie składowych własnych klasy Punktkol
return * this ;
}
Funkcje wirtualne
Wiązanie statyczne, wczesne (ang. static binding, early binding) oznacza powiązanie obiektu z wywoływanymi funkcjami w czasie kompilacji. Takie wiązanie jest stosowane do wszystkich zwykłych funkcji.
Zaleta: efektywność.
Wada: brak elastyczności.
Wiązanie dynamiczne, późne (ang. dynamic binding, late binding) oznacza powiązanie obiektu z wywoływanymi funkcjami dopiero podczas działania programu. Uzyskuje się je za pomocą funkcji wirtualnych i klas pochodnych.
Zaleta: elastyczność.
Wada: wolniejsze działanie programu.
Jeżeli funkcja ma być funkcją wirtualną, poprzedza się jej deklarację w klasie bazowej słowem kluczowym virtual.
class Punkt
{
...
// wybór wersji funkcji drukuj() będzie dokonany
// dopiero w momencie użycia
virtual void drukuj ();
...
} ;
Jeśli w czasie kompilacji napotkana zostanie instrukcja wsk->drukuj(), kompilator nie przepisze funkcji w oparciu o typ wskaźnika wsk. Wybór funkcji zostanie dokonany dopiero w oparciu o typ obiektu, na który będzie wskazywał wskaźnik podczas wykonywania programu.
Funkcja zdefiniowana jako wirtualna będzie dowiązywana dynamicznie zarówno w klasie w której została zdefiniowana, jak i we wszystkich klasach pochodnych, w których została ponownie zdefiniowana.
Ponowna definicja funkcji wirtualnej w klasie pochodnej nie jest niezbędna. Jeśli nie zostanie ponownie zdefiniowana, używana jest funkcja z klasy, z której klasa pochodna dziedziczy.
Przykład:
class Punkt
{
protected :
int x, y ;
public :
Punkt (int xx=0, int yy=0) { x=xx ; y=yy ; }
// funkcja wirtualna
virtual void drukuj ()
{ cout << "Jestem punktem \n" ;
cout << " oto moje współrzędne : " << x << " " << y << endl ;
}
} ;
class Punktkol : public Punkt
{
short kolor ;
public :
Punktkol (int xx=0, int yy=0, short kk=1) : Punkt (xx, yy)
{ kolor = kk ; }
void drukuj ()
{ cout << "Jestem punktem kolorowym \n" ;
cout << " oto moje współrzędne : " << x << " " << y ;
cout << " i mój kolor : " << kolor << "\n" ;
}
} ;
W main() :
Punkt p(1,3) ; Punkt * wsk = &p ;
Punktkol pk (8,6,2) ; Punktkol * wsk_k = &pk ;
wsk = wsk_k ;
wsk->drukuj (); // będzie użyta funkcja Punktkol::drukuj()
wsk_k->drukuj (); // będzie użyta funkcja Punktkol::drukuj()
Przykład:
#include <iostream>
class Figura {
protected:
double x,y;
public:
void wpiszWymiary (double i, double j=0)
{ x=i; y=j; }
virtual void obliczPow()
{ cout << "Nie potrafie obliczyc powierzchni\n"; }
};
class Trojkat : public Figura {
public:
void obliczPow()
{ cout << "Trojkat o wysokosci " << x << " i podstawie " << y;
cout << " ma pole powierzchni " << 0.5*x*y << endl;
}
};
class Prostokat : public Figura {
public:
void obliczPow()
{ cout << "Prostokat o wymiarach " << x << " na " << y ;
cout << " ma pole powierzchni " << x*y << endl;
}
};
class Kolo : public Figura {
public:
void obliczPow()
{ cout << "Kolo o promieniu " << x ;
cout << " ma pole powierzchni " << 3.14*x*x << endl;
}
};
int main()
{
Figura *fig;
Trojkat t;
Prostokat p;
Kolo k;
fig=&t;
fig->wpiszWymiary(5.,2.5); fig->obliczPow();
fig=&p;
fig->wpiszWymiary(5.,2.5); fig->obliczPow();
fig=&k;
fig->wpiszWymiary(5.); fig->obliczPow();
return 0;
}
Przykład (niepoprawny):
class Punkt
{
int x, y ;
public :
Punkt (int xx=0, int yy=0) { x=xx ; y=yy ; }
void nazwa ()
{ cout << "Jestem zwykłym punktem \n" ; }
void drukuj ()
{ nazwa () ;
cout << "Oto moje wspolrzedne : " << x << " " << y << "\n" ;
}
} ;
class Punktkol : public Punkt
{
short kolor ;
public :
Punktkol (int xx=0, int yy=0, int kk=1 ) : Punkt (xx, yy)
{ kolor = kk ; }
void nazwa ()
{ cout << "Jestem punktem kolorowym a moj kolor to : "
<< kolor << "\n" ; }
} ;
Co będzie wydrukowane przez poniższy program?
#include <iostream>
main()
{ Punkt p(2,3) ; Punktkol pk(4,6,9) ;
p.drukuj () ; pk.drukuj () ;
cout << "---------------\n" ;
Punkt * wsk = &p ; Punktkol * wsk_k = &pk ;
wsk->drukuj () ; wsk_k->drukuj () ;
cout << "---------------\n" ;
wsk = wsk_k ;
wsk->drukuj () ;
wsk_k->drukuj () ;
}
Co trzeba zmienić, aby program działał poprawnie?
Czyste funkcje wirtualne
Podczas wywoływania funkcji wirtualnej w stosunku do obiektu klasy pochodnej, w której ta funkcja nie została ponownie zdefiniowana, stosowana jest wersja z klasy bazowej.
Istnieją jednak klasy, w których funkcje wirtualne nie wykonują żadnego pożytecznego działania. Często funkcje takie MUSZĄ być przykryte, gdyż w przeciwnym wypadku istnienie takich klas traci sens.
Można wymusić na klasie pochodnej przykrycie wszystkich niezbędnych funkcji za pomocą zastosowania tzw. czystych funkcji wirtualnych (ang. pure virtual function).
Czysta funkcja wirtualna to funkcja, która jest zadeklarowana w klasie bazowej, ale nie jest w niej zdefiniowana.
Klasa, w której zdefiniowano co najmniej jedną czystą funkcję wirtualną, nazywa się klasą abstrakcyjną (ang. abstract class). Ma ona ważną cechę: nie można tworzyć obiektów tej klasy. Używa się jej jedynie jako klasy bazowej do tworzenia klas pochodnych.
Przykład:
#include <iostream>
class Figura {
protected:
double x,y;
public:
void wpiszWymiary (double i, double j=0)
{ x=i; y=j; }
virtual void obliczPow()= 0;
};
class Trojkat : public Figura {
public:
void obliczPow()
{
cout << "Trojkat o wysokosci " << x << " i podstawie " << y;
cout << " ma pole powierzchni " << 0.5*x*y << endl;
}
};
class Prostokat : public Figura {
public:
// w tej klasie nie zdefiniowano funkcji obliczPow
...
};
int main()
{
Figura *fig;
Trojkat t;
Prostokat p;
fig=&t;
fig->wpiszWymiary(5.,2.5);
fig->obliczPow();
fig=&p;
fig->wpiszWymiary(5.,2.5);
fig->obliczPow();
return 0;
}
1
12
Nazwa klasy podstawowej
Tryb dziedziczenia
Rozszerzona wersja konstruktora klasy pochodnej, przekazującego argumenty konstruktorowi klasy bazowej
Punktkol::inicjalizuj()
Wywołanie konstruktora kopiującego A,
dokonana zostanie konwersja obiektu x klasy B na obiekt x klasy A
wsk_a może wskazywać na obiekt klasy pochodnej B i pozwala na dostęp do tych jej składowych, które są zdefiniowane w klasie bazowej.
Nie można jednak wykorzystać wskaźnika do klasy B do uzyskania dostępu do składowych występujących tylko w klasie pochodnej (bez rzutowania).
Program się nie skompiluje
private:
protected:
public:
Składowe własne klasy pochodnej
Klasa pochodna ma dostęp do składowych publicznych i do składowych chronionych
Klasa bazowa
Klasa pochodna
Składowe odziedziczone
Klient nie ma dostępu
Klient nie ma dostępu
Klient ma dostęp
1
1
2
2
0
wsk_a
wsk_b
a.drukuj();
wsk_a->drukuj();
b.drukuj();
wsk_b->drukuj();
wsk_b
wsk_a
0
2
2
1
1
Obiekt klasy Punkt
Punkt::drukuj()
0
2
2
4
2
Tablica funkcji wirtualnych
Funkcje
Punkt::inicjalizuj()
Funkcje
Tablica funkcji wirtualnych
Punktkol::drukuj()
Obiekt klasy Punktkol
*wsk_a nie ma
koloru