Literatura: Slajdy do wykładu drugiego, trzeciego i czwartego. Stanley B. Lippman Josee Lajoie Podstawy języka C++. Stanley B. Lippman Istota języka C++. Bruce Eckel Thinking in C++ edycja polska. Bjarne Stroustrup Język C++. 1 class Podstawowa int priv; int publ; int prot; Podstawowa (int a) : priv (a), publ (2*a), prot (-a) void f(int); void wypisz ( ) const; W wyniku dziedziczenia w klasie Pochodna znajdą się wszystkie pola i prawie wszystkie metody z klasy Podstawowa. Dziedziczenie Jeśli w programie chcielibyśmy utworzyć klasę podobną do klasy Podstawowa, różniącą się w kilku szczegółach, wystarczy skorzystać z techniki programowania zwanej dziedziczeniem. class Pochodna : public Podstawowa Klasa Podstawowa musi być już zdefiniowana. double dodatek; Pochodna (double a, int b) : dodatek (a), Podstawowa (b) void g ( ); void wypisz ( ) const; 2 Klasa pochodna dziedziczy składowe klasy podstawowej oprócz: konstruktorów, destruktora, operatorów przypisania. W klasie pochodnej możemy: zdefiniować dodatkowe pola, zdefiniować dodatkowe metody, przedefiniować pola i metody, które istnieją w klasie podstawowej. Dostęp do składowych (przy dziedziczeniu publicznym): składowe prywatne klasy (etykieta private) dostępne są wyłącznie w metodach tej klasy oraz jej funkcjach (metodach, klasach) zaprzyjaźnionych, składowe publiczne klasy (etykieta public) dostępne są w całym programie, składowe chronione klasy (etykieta protected) dostępne są w metodach tej klasy i jej funkcjach (metodach, klasach) zaprzyjaźnionych oraz w metodach klasy pochodnej i funkcjach (metodach, klasach) zaprzyjaźnionych z klasą pochodną. Etykieta protected pozwala więc, by klasa posiadała inny interfejs wobec programu zewnętrznego, aniżeli w stosunku do jej klas pochodnych. Program zewnętrzny sięga do jej sekcji publicznej podczas, gdy klasy pochodne mogą także korzystać z metod i pól zadeklarowanych w jej części chronionej. Użycie etykiety protected nie chroni już tak skutecznie implementacji klasy, jak etykieta private. Gdyby zaszła konieczność zmodyfikowania klasy bazowej (podstawowej), wówczas należałoby również przeredagować wszystkie klasy 3 pochodne, które bezpośrednio korzystają z komponentów protected klasy bazowej. Przykład class Podstawowa int priv; int publ; int prot; Podstawowa (int a) : priv (a), publ (2*a), prot (-a) void f(int); void wypisz ( ) const; void Podstawowa::f ( ) priv=1; publ=2; prot=3; void Podstawowa::f (int liczba) priv=publ=prot=liczba; void Podstawowa::wypisz ( ) const cout<<priv<<endl <<publ<<endl <<prot<<endl; 4
class Pochodna : public Podstawowa double dodatek; Pochodna (double a, int b) : dodatek (a), Podstawowa (b) void g ( ); void wypisz ( ) const; void Pochodna::g ( ) priv=10; błąd brak dostępu publ=20; prot=30; dodatek=15.7; void Pochodna::wypisz ( ) const Podstawowa::wypisz ( ); cout<<dodatek<<endl; Podstawowa A(5); A.priv=1; A.publ=2; A.prot=3; Pochodna B(1.2, 6); B.priv=1; B.publ=2; B.prot=3; 5 Podstawowa A(5); cout<<"podstawowa A"<<endl; A.wypisz ( ); Pochodna B (12.5, 10); cout<<"pochodna B"<<endl; B.wypisz ( ); B.g( ); B.f( ); cout<<"podstawowa B"<<endl; B.Podstawowa::wypisz ( ); Wynik działania programu: Podstawowa A 5 10-5 Pochodna B 10 20-10 12.5 Podstawowa B 1 2 3 6 Ukrywanie nazw w klasie pochodnej W klasie Pochodna przedefiniowaliśmy metodę wypisz (w tym przypadku zmianie uległa treści metody), ukryliśmy tym samym w klasie Pochodna metodę wypisz klasy Podstawowa, którą można jedynie wywołać w sposób jawny, tzn. nazwę metody musimy poprzedzić nazwą klasy, czyli B.Podstawowa::wypisz ( ); Jeśli w przedefiniowywanej w klasie pochodnej metodzie, zmienimy typ zwracanej wartości lub/i argumenty metody, to również mamy do czynienia z ukrywaniem metody z klasy bazowej (podstawowej) w klasie pochodnej i tylko w sposób jawny możemy ją wywołać (tzn. j.w.). Taką samą mamy sytuację dla pól klasy. Jeśli przedefiniowujemy metody, których nazwa jest przeciążona w klasie bazowej (podstawowej), to wszystkie pozostałe wersje tej metody są automatycznie ukrywane w nowej, pochodnej klasie. 7 class Podstawowa // int publ; void f(int); // class Pochodna2 : public Podstawowa Przykłady Pochodna2 ( ) : Podstawowa (0) Pochodna2 C; C.f( ); // C.Pochodna2::f( ); C.f(10); //błąd metoda Podstawowa::f(int) została ukryta w klasie Pochodna2, można ją wywołać w sposób jawny tzn. C.Podstawowa::f(10); class Pochodna3 : public Podstawowa Pochodna3 ( ) : Podstawowa (-5) int f ( ); Pochodna3 D; int x=d.f ( ); //int x=d.pochodna3::f ( ); D.f (-5); //błąd metoda Podstawowa::f (int); jest ukryta w klasie Pochodna3 class Pochodna4 : public Podstawowa double publ; Pochodna4 ( ) : publ (0), Podstawowa (8) Pochodna4 E; cout<<e.publ; cout<<e.podstawowa::publ; 8
class Podstawowa // int publ; void f(int); // class Pochodna3bis : public Podstawowa Pochodna3bis ( ) : Podstawowa (-5) using Podstawowa::f; int f ( ); class Pochodna2bis : public Podstawowa Przykłady inaczej Pochodna2bis ( ) : Podstawowa (0) using Podstawowa::f; Pochodna2bis C; C.f( ); // C.Pochodna2bis::f( ); C.f(10); //C.Podstawowa::f(10) Pochodna3bis D; int x=d.f ( ); //int x=d.pochodna3bis::f ( ); D.f (-5); //D.Podstawowa::f (-5); 9 Konstruktor i destruktor klasy pochodnej class Test1 Test1 (int a) : a1 (a) cout<<"konstruktor Test1"<<endl; ~Test1 ( ) cout<<"destruktor Test1"<<endl; int a1; class Test2 Test2 (int b=10) : a2 (b) cout<<"konstruktor Test2"<<endl; ~Test2 ( ) cout<<"destruktor Test2"<<endl; int a2; Pisząc: Pochodna P (2); na ekranie zobaczymy: Konstruktor Test1 Konstruktor Test2 ****!!!!! Destruktor Test2 Destruktor Test1 class Pochodna : public Test1 Pochodna (int a) : t2(5), b(12), Test1(a) cout<<"****"<<endl; ~Pochodna ( ) cout<<"!!!!!"<<endl; Test2 t2; int b; 10 Kolejność wywoływania konstruktorów nie zależy od kolejności ich występowania na liście, ale od związków zachodzących między klasami. Destruktory wywoływane są w kolejności odwrotnej do wywołań konstruktorów. Konstrukcja obiektu klasy pochodnej zaczyna się od wywołania konstruktora klasy pochodnej, który wywołuje konstruktor klasy bazowej, a następnie zostaną zainicjowane pola klasy pochodnej (w przypadku pól, które są obiektami, zostaną wywołane odpowiednie konstruktory) zgodnie z kolejnością deklaracji w klasie. W klasie pochodnej bez jawnej definicji konstruktora (w tym też bez konstruktora kopiującego), konstruktor domyślny jest generowany automatycznie przez kompilator pod warunkiem, że w klasie bazowej istnieje i jest dostępny konstruktor domyślny oraz wszystkie pola klasy pochodnej mają szanse być poprawnie zainicjowanymi. Konstruktory i destruktor nie są dziedziczone. W klasie pochodnej konstruktor kopiujący jest generowany automatycznie przez kompilator, w którym kopiowanie odbywa się składowa po składowej, co w przypadku dziedziczenia oznacza uruchomienie konstruktora kopiującego klasy podstawowej, pod warunkiem, że istnieje i jest dostępny. Jeśli konstruktor kopiujący klasy podstawowej nie jest dostępny, to kompilator nie wygeneruje konstruktora kopiującego dla klasy pochodnej. W tym przypadku, o ile 11to jest konieczne, możemy konstruktor kopiujący zdefiniować samodzielnie. Operator przypisania nie jest dziedziczony. W klasie pochodnej operator przypisania jest generowany automatycznie przez kompilator, który jest przypisaniem składowa po składowej, co w przypadku dziedziczenia oznacza uruchomienie operatora przypisania klasy podstawowej pod warunkiem, że istnieje i jest dostępny. Gdy klasa podstawowa zawiera pola const lub/i pola będące referencjami, to operator przypisania nie zostanie automatycznie wygenerowany, gdyż do tych pól nie można nic przypisać. Gdy klasa podstawowa posiada prywatny operator przypisania, to kompilator również nie wygeneruje operatora przypisania dla klasy pochodnej. W tych przypadkach, jak również w przypadku, gdy automatycznie wygenerowany operator przypisania nam nie odpowiada możemy sami go zdefiniować. 12
Definicja konstruktora kopiującego i operatora przypisania dla klasy pochodnej. #include <iostream> using std::cout; class Podstawowa int numer; double kwota; Podstawowa (int a=1, double b=0) : numer (a), kwota (b) Podstawowa (const Podstawowa& wzor); //konstruktor kopiujący Podstawowa& operator= (const Podstawowa& co); //operator przypisania void wypisz() const cout<<'\n'<<numer<<" "<<kwota<<" "; Podstawowa::Podstawowa (const Podstawowa& wzor) : numer (wzor.numer+1), kwota (wzor.kwota) Podstawowa& Podstawowa::operator= (const Podstawowa& co) if (this==&co) return *this; numer=co.numer+1; kwota=co.kwota; return *this; 13 class Pochodna : public Podstawowa int ilosc; Pochodna (int c=10) : ilosc (c) Pochodna (const Pochodna& wzor); Pochodna & operator= (const Pochodna& co); void wypisz( ) const Podstawowa::wypisz();cout<<ilosc<<'\n'; Pochodna ::Pochodna (const Pochodna& wzor) : Podstawowa (wzor), ilosc(wzor.ilosc) Pochodna& Pochodna::operator= (const Pochodna& co) if (this==&co) return *this; Podstawowa::operator= (co); ilosc=co.ilosc+100; return *this; //konstruktor kopiujący //operator przypisania konwersja Pochodna& Podstawowa& 14 int main() Podstawowa a(2,3); Pochodna P(4), B=P; a.wypisz(); P.wypisz(); B.wypisz(); P=B; P.wypisz(); a=b; a.wypisz(); Podstawowa b=b; b.wypisz(); 2 3 1 0 4 2 0 4 3 0 104 3 0 3 0 15 Standardowe konwersje. Rzutowanie w górę. Obiekt klasy pochodnej można uznać za obiekt klasy podstawowej. Predefiniowane są następujące konwersje standardowe: wskaźnik do obiektu klasy pochodnej może być niejawnie przekształcony na wskaźnik do obiektu klasy podstawowej. Pochodna* Podstawowa* referencja obiektu klasy pochodnej może być niejawnie przekształcona na referencję do obiektu klasy podstawowej. Pochodna& Podstawowa& Poprawne są więc linie: Pochodna P; Podstawowa T=P; // W linii zostanie wywołany konstruktor kopiujący Podstawowa::Podstawowa (const Podstawowa&); którego argumentem jest referencja do obiektu klasy Podstawowa. W naszym przykładzie po prawej stronie znaku = znajduje się obiekt klasy Pochodna. Wykonanie konstruktora kopiującego rozpocznie się wykonaniem konwersji Pochodna& Podstawowa&. Natomiast błędem zakończy się próba kompilacji następujących linii: Podstawowa T; 16 Pochodna P=T; //błąd niemożliwa jest konwersja Podstawowa& Pochodna&
W przypadku operatora przypisania sytuacja wygląda analogicznie. Pochodna P; Podstawowa T; T=P; // W linii przypadku zostanie wywołana metoda operatorowa Podstawowa& Podstawowa::operator= (const Podstawowa&); której argumentem jest referencja do obiektu klasy Podstawowa, tu również na początku zostanie wykonana konwersja Pochodna& Podstawowa&, ponieważ po prawej stronie operatora przypisania stoi obiekt klasy Pochodna. Rozważmy następującą hierarchię klas: class Samochod int zbiornik; class Mercedes : public Samochod class Fiat : public Samochod void stacja_benzynowa (Samochod &klient) klient.zbiornik+=50; Samochod idealny; stacja_benzynowa (idealny); spalanie (&idealny); Zdefiniujemy dwie funkcje stacja_benzynowa(samochod&) i spalanie(samochod*), które mogą pracować z każdym obiektem klasy pochodzącej od klasy Samochod. void spalanie (Samochod *wsk) wsk->zbiornik-=5; Niejawne konwersje użytkownik klasy może wykonać tylko przy dziedziczeniu publicznym. Sposób dziedziczenia określamy na liście pochodzenia klasy. Mercedes m_piotra; stacja_benzynowa (m_piotra); spalanie (&m_piotra); // Mercedes& Samochod& // Mercedes* Samochod* 17 Fiat f_pawla; stacja_benzynowa (f_pawla); spalanie (&f_pawla); // Fiat& Samochod& // Fiat* Samochod* 18 Lista pochodzenia klasy Na liście pochodzenia klasy, zamiast public (wykorzystywane najczęściej) mogą pojawić się etykiety private, protected. Etykiety określają w jaki sposób mają być traktowane składowe nieprywatne (składowe public i protected) klasy podstawowej w klasie pochodnej. class Pochodna: public Podstawowa składowe publiczne i chronione (public i protected) klasy Podstawowa mają ten sam specyfikator dostępu w klasie Pochodna. class Pochodna: private Podstawowa składowe nieprywatne klasy Podstawowa są składowymi prywatnymi klasy Pochodna. class Pochodna: protected Podstawowa składowe nieprywatne klasy Podstawowa są składowymi chronionymi klasy Pochodna. Domyślna etykietą na liście inicjalizującej jest etykieta private. 19 W przypadku dziedziczenia prywatnego użytkownik klasy nie może korzystać z niejawnych konwersji w górę. class Samochod int zbiornik; class Dom : private Samochod void stacja_benzynowa (Samochod &klient) klient.zbiornik+=50; Dom moj_dom; //stacja_benzynowa (moj_dom); błąd stacja_benzynowa ((Samochod&) moj_dom); W przypadku dziedziczenia prywatnego możliwe są tylko jawne konwersje. 20
class Przodek int i; float f; void fun1 (int, float); long l; int fun2 (double); class Potomek : private Przodek int dodatek; Przodek::i; Przodek::fun1; Przodek::fun2; W przypadku dziedziczenia prywatnego możemy wybiórczo udostępniać składowe publiczne i chronione klasy podstawowej. W deklaracjach dostępu podajemy same nazwy składowych z klasy bazowej. Nie można zmienić rodzaju dostępu do składowych udostępnianych w klasach pochodnych. 21 Klasa może wywodzić się od więcej niż jednej klasy, mówimy wtedy o dziedziczeniu wielobazowym. class B void metoda ( ); class C void metoda ( ); class A : public B, public C Klasy podstawowe umieszczamy na liście pochodzenia klasy pochodnej poprzedzając ich nazwy odpowiednimi etykietami. Dość często w przypadku dziedziczenia wielobazowego pojawiają się błędy niejednoznaczności. Najprostszym przypadkiem takiego błędu jest próba dziedziczenia przez klasę potomną składowej o nazwie, która występuje w więcej niż jednej klasie bazowej. Pisząc obiekt.metoda ( ); spowodujemy reakcję kompilatora np. następującą [C++ Error] Member is ambiguous: 'B::metoda' and 'C::metoda' W tym przypadku należy jawnie napisać z której klasy ma być wywołana metoda tzn. obiekt.c::metoda(); lub obiekt.b::metoda( ); 22 class X int x; X( ):x(-5) cout<<"konst X"<<endl; class B : public X int y; B( ) cout<<"konst B"<<endl; class C : public X int z; C( ) cout<<"konst C"<<endl; int main( ) cout<<sizeof(obiekt)<<endl; Wynik konst X konst B konst X konst C konst A 20 class X int x; X( ):x(-5) cout<<"konst X"<<endl; class B : public X int y; B( ) cout<<"konst B"<<endl; class C : public X int z; C( ) cout<<"konst C"<<endl; int main( ) cout<<sizeof(obiekt)<<endl; class A : public B, public C int h; A( ) cout<<"konst A"<<endl; 23 class A : public B, public C int h; [C++ Error] Member is ambiguous: 'X::x' and 'X::x' 24 A( ) cout<<x<<" konst A"<<endl;
class X int x; X( ):x(-5) cout<<"konst X"<<endl; class B : virtual public X int y; B( ) cout<<"konst B"<<endl; class C : virtual public X int z; C( ) cout<<"konst C"<<endl; int main( ) cout<<sizeof(obiekt)<<endl; Wynik konst X konst B konst C -5 konst A 24 Problemy z konstruktorem w hierarchiach z wirtualnymi klasami bazowymi class X int x; X(int a) :x(a) cout<<"konst X "<<x<<endl; class B : public X int y; B( ): X(2) cout<<"konst B"<<endl; class C : public X int z; C( ): X(120) cout<<"konst C"<<endl; int main( ) cout<<sizeof(obiekt)<<endl; Wynik konst X 2 konst B konst X 120 konst C konst A 20 class A : public B, public C int h; A( ) cout<<x<<" konst A"<<endl; 25 class A : public B, public C int h; A( ) cout<<"konst A"<<endl; 26 Problemy z konstruktorem w hierarchiach z wirtualnymi klasami bazowymi class X int x; X(int a) :x(a) cout<<"konst X "<<x<<endl; class B : virtual public X int y; B( ): X(2) cout<<"konst B"<<endl; class C : virtual public X int z; C( ): X(120) cout<<"konst C"<<endl; class A : public B, public C int h; A( ): X(456) cout<<"konst A"<<endl; int main( ) cout<<sizeof(obiekt)<<endl; Wynik konst X 456 konst B konst C konst A 24 27 Polimorfizm i metody wirtualne. #include <iostream> using std::cout; using std::endl; cout<<""<<endl; cout<<"gra flet"<<endl; cout<<"gra trabka"<<endl; void muzyka (const Instrument& i) i.graj ( ); Trabka t; Flet f; muzyka (t); muzyka (f); Na ekranie zobaczymy. 28
Ten sam kod z dopisanym słowem virtual przed deklaracją metody Instrument::graj() wygeneruje inny wynik Ten sam kod z dopisanym słowem const po deklaracji metody Instrument::graj() wygeneruje wynik virtual cout<<""<<endl; cout<<"gra flet"<<endl; void muzyka (const Instrument& i) i.graj ( ); Trabka t; Flet f; muzyka (t); muzyka (f); virtual cout<<""<<endl; cout<<"gra flet"<<endl; void muzyka (const Instrument& i) i.graj ( ); Trabka t; Flet f; muzyka (t); muzyka (f); Na ekranie zobaczymy. Gra trabka Gra flet cout<<"gra trabka"<<endl; 29 [C++ Warning] Non-const function Instrument::graj() called for const object cout<<"gra trabka"<<endl; Na ekranie zobaczymy. [C++ Warning] 'Flet::graj()' hides virtual function 'Instrument::graj() const' 30 [C++ Warning] 'Trabka::graj()' hides virtual function 'Instrument::graj() const' Ten sam kod z dopisanym słowem const po deklaracji metody graj() w całej hierarchii dziedziczenia wygeneruje wynik Ten sam kod z dopisanym z argumentem przekazywanym przez wartość do funkcji muzyka virtual cout<<""<<endl; cout<<"gra flet"<<endl; void muzyka (const Instrument& i) i.graj ( ); Trabka t; Flet f; muzyka (t); muzyka (f); virtual cout<<""<<endl; cout<<"gra flet"<<endl; void muzyka (const Instrument i) i.graj ( ); Trabka t; Flet f; muzyka (t); muzyka (f); cout<<"gra trabka"<<endl; Na ekranie zobaczymy. Gra trabka Gra flet 31 cout<<"gra trabka"<<endl; Na ekranie zobaczymy. 32
Metodę zadeklarowaną ze słowem kluczowym virtual, nazywamy metodą wirtualną. Wirtualne mogą być tylko metody klasy. Wirtualny nie może być konstruktor i metody statyczne. Słowo virtual umieszczamy w treści klasy w momencie deklaracji metody, w definicji metody słowa virtual nie powtarzamy. Funkcja wirtualna w klasie podstawowej jest również wirtualna we wszystkich jej klasach pochodnych. W treści klasy pochodnej w deklaracji można powtórzyć słowo virtual, ale nie jest to konieczne. Wróćmy do treści funkcji muzyka. Linia z programu z metodami wirtualnymi i.graj ( ); produkuje różne wyniki, takie zachowanie nazywamy polimorficznym, a zjawisko polimorfizmem. Pisząc w treści funkcji main muzyka (t); // wywołujemy metodę i.trabka::graj ( ); w funkcji muzyka muzyka (f); // wywołujemy metodę i.flet::graj ( ); w funkcji muzyka Mechanizm wywołania metody jako metody wirtualnej ujawnia się tylko wtedy, gdy wywołujemy metodę wirtualną za pomocą adresu tzn. referencji lub wskaźnika do obiektu klasy podstawowej. O tym którą wersję funkcji wywołamy decyduje nie typ referencji lub wskaźnika, ale to, z czym referencja jest związana lub na co pokazuje 33 wskaźnik. Wyprowadzając nowe typy danych (np. Fortepian, Gitara) ze wspólnej klasy podstawowej (u nas Instrument), nie musimy wprowadzać jakichkolwiek zmian w funkcjach, odwołujących się do interfejsu klasy podstawowej (u nas funkcja muzyka), tak by mogły one obsługiwać nowo utworzone klasy. Domyślnie metody klas nie są wirtualne (tak jak np. w Javie) bo: (a) obiekty takich klas, są trochę większe, a programy pracujące z takimi klasami trochę wolniejsze; (b) nie można by też było stosować funkcji inline. Metody wirtualne mogą być typu inline, ale w sytuacji gdy wywołanie metody odbywa się na rzecz obiektu klasy pochodnej pokazywanego wskaźnikiem (referencją) do klasy podstawowej wtedy inline jest ignorowane. Natomiast w przypadku jawnego wywołania oraz wywołania na rzecz obiektu inline nie jest ignorowane. Klasa pochodna nie musi koniecznie definiować swojej wersji metody wirtualnej, zawsze może korzystać z wersji odziedziczonej z klasy podstawowej. Przedefiniowując metodę wirtualną w klasie pochodnej, należy pamiętać o zgodności nazwy, typów argumentów i typu wartości, o ile chcemy korzystać z mechanizmu obsługującego metody wirtualne. Jedyną akceptowalną zmianą jest zastąpienie typu wartości metody wirtualnej, typem, który niejawnie daje się przekonwertować na typ pojawiający się w pierwszej deklaracji funkcji wirtualnej w hierarchii dziedziczenia, dotyczy to tylko typów definiowanych przez użytkownika. 34 class Moja virtual Moja& funkcja ( ) return *this; class Pochodna : public Moja Pochodna& funkcja ( ) return *this; class Pochodna2 : public Moja Moja& funkcja ( ) return *this; rzutowanie w górę Problemy mogą się pojawić w momencie wykorzystywania zwracanej wartości, mianowicie: Pochodna a; Pochodna c=a.funkcja( ); Pochodna2 A; // Pochodna2 C=A.funkcja( ); Pochodna2 C=dynamic_cast<Pochodna2&>(A.funkcja( 35)); Typ statyczny wskaźnika (referencji) określa sposób dostępu do metody wirtualnej. W określaniu dostępu typ dynamiczny wskaźnika (referencji) nie jest brany pod uwagę. class Moja virtual void funkcja ( ); class Pochodna1 : public Moja void funkcja ( ); class Pochodna2 : public Pochodna1 void funkcja ( ); Moja *M=new Pochodna1; M->funkcja ( ); // dostęp publiczny do Pochodna1::funkcja () Moja *N=new Pochodna2; N->funkcja ( ); // dostęp publiczny do Pochodna2::funkcja () Pochodna1 *P=new Pochodna2; P->funkcja ( ); //błąd dostęp chroniony do Pochodna2::funkcja () 36
Klasa zawierająca metody wirtualne powinna zawierać destruktor wirtualny. #include <iostream> using std::cout; using std::endl; class Moja virtual void funkcja ( ) cout<<"moja "; virtual ~Moja ( ) cout<<"destr.m. <<endl; Moja *M=new Pochodna1; M->funkcja ( ); Moja *N=new Pochodna2; N->funkcja ( ); delete M; delete N; Klasyczne metody od metod wirtualnych różnią się sposobem wiązania tzn. łączenia wywołania metody z jej treścią, tzn.: w klasycznych metodach już na etapie kompilacji odbywa się powiązanie wywołań metod z adresem określającym, gdzie te metody się znajdują, natomiast w przypadku metod wirtualnych, kompilator generuje kod dzięki któremu decyzja o powiązaniu wywołania metody z określoną wersją metody wirtualnej jest podejmowana na etapie wykonania programu, tak więc mamy dwa sposoby wiązania: wczesne wiązanie na etapie kompilacji późne wiązanie na etapie wykonania programu. class Pochodna1 : public Moja void funkcja ( ) cout<<"pochodna1 "; ~Pochodna1 ( ) cout<<"destr.p1. <<endl; class Pochodna2 : public Pochodna1 void funkcja ( ) cout<<"pochodna2 "; ~Pochodna2 ( ) cout<<"destr.p2. <<endl; Pochodna1 Pochodna2 destr.p1. destr.m. destr.p2. destr.p1. destr.m. 37 class Moja virtual void funkcja ( ); Moja A; A.funkcja ( ); //. Moja *wsk; //.. wsk->funkcja ( ); wsk->moja::funkcja ( ); 38 W jaki sposób język C++ realizuje późne wiązanie? Typowy kompilator, dla każdej klasy zawierającej metody wirtualne tworzy pojedynczą tablicę VTABLE. W tablicy VTABLE umieszczane są adresy metod wirtualnych klasy. W każdym obiekcie reprezentującym klasę posiadającą metody wirtualne, niejawnie umieszczany jest wskaźnik wirtualny VPTR wskazujący na tablicę VTABLE tej klasy. Wszystkie obiekty klasy podstawowej, a także jej klas pochodnych, posiadają wskaźnik VPTR na tym samym miejscu (często na początku obiektu). Wszystkie adresy metod wirtualnych zawartych w tablicy VTABLE są ułożone w tym samym porządku, niezależnie od konkretnego typu obiektu. Wskaźnik VPTR jest inicjowany niejawnie w konstruktorze klasy. W momencie wywołania metody wirtualnej za pomocą wskaźnika (referencji) do obiektów klasy podstawowej, kompilator niejawnie wstawia kod, pobierający wskaźnik VPTR i odnajdujący adres metody wirtualnej w tablicy VTABLE, co umożliwia wywołanie odpowiedniej metody w trakcie pracy programu. To wszystko dla nas wykonuje kompilator. Wniosek Jeśli class Moja int a; virtual void funkcja ( ); class MOJA int a; void funkcja ( ); to sizeof (Moja) >sizeof (MOJA). 39 Przykład nieco dłuższy #include <iostream> #include <string> using std::cout; using std::endl; using std::string; virtual cout<<""<<endl; virtual string co ( ) const return "Instrument"; virtual void napraw ( ) virtual ~Instrument( ) cout<<":-("; cout<<"gra flet"<<endl; string co ( ) const return "Flet"; void napraw ( ) cout<<"naprawiam flet"<<endl; class Flet_prosty : public Flet cout<<"gra flet prosty"<<endl; string co ( ) const return "Flet prosty"; cout<<"gra trabka"<<endl; string co ( ) const return "Trabka"; void napraw ( ) cout<<"naprawiam trabke"<<endl; 40
Instrument *A[ ]=new Trabka, new Flet, new Flet_prosty for (int i=0; i<sizeof (A)/sizeof (*A) ; ++i) A[i]->graj ( ); for (int i=0; i<sizeof (A)/sizeof (*A) ; ++i) A[i]->napraw ( ); for (int i=0; i<sizeof (A)/sizeof (*A) ; ++i) delete A[i]; Na ekranie zobaczymy Gra trabka Gra flet Gra flet prosty Naprawiam trabke Naprawiam flet Naprawiam flet :-(:-(:-( 41 Przykład. Klasa abstrakcyjna. virtual =0; virtual string co ( ) const = 0; virtual void napraw ( ) =0; virtual ~Instrument( ) =0; cout<<"gra flet"<<endl; string co ( ) const return "Flet"; void napraw ( ) cout<<"naprawiam flet"<<endl; class Flet_prosty : public Flet cout<<"gra flet prosty"<<endl; string co ( ) const return "Flet prosty"; cout<<"gra trabka"<<endl; string co ( ) const return "Trabka"; void napraw ( ) cout<<"naprawiam trabke"<<endl; void Instrument::graj ( ) const cout<<""<<endl; string Instrument::co ( ) const Program z poprzedniego slajdu będzie return "Instrument"; 42 działał identycznie. Instrument ::~Instrument( ) cout<<":-("; Abstrakcyjne klasy podstawowe i metody czysto wirtualne. Klasa abstrakcyjna to klasa, która nie reprezentuje, żadnego konkretnego obiektu (np. klasa Ssak, klasa Figura, klasa Instrument). Metoda czysto wirtualna, to funkcja która oprócz słowa kluczowego virtual posiada adnotacje =0 na końcu deklaracji. Klasa z metodami czysto wirtualnymi jest klasą abstrakcyjną. Kompilator nie pozwoli nikomu na utworzenie obiektu takiej klasy, gdyż klasa abstrakcyjna to jakby niedokończona klasa. Dokończenie jej jest realizowane przez jej klasy pochodne. Klasy abstrakcyjne określają wspólny interfejs dla klas pochodnych. Klasy abstrakcyjne wprowadza się po to żeby je dziedziczyć. 43