8. Klasy i funkcje wirtualne
Dziedziczenie mnogie moe by rodkiem dla organizacji bibliotek wokó prostszych klas z mniejsz liczb zalenoci pomidzy klasami, ni w przypadku dziedziczenia pojedynczego. Gdyby ograniczy dziedziczenie do pojedynczego, to kada biblioteka byaby jednym drzewem dziedziczenia, w ogólnoci bardzo wysokim i rozgazionym. Mechanizm dziedziczenia mnogiego pozwala budowa biblioteki w postaci *lasu mieszanego”, w którym drzewa i grafy dziedziczenia mog mie zmienn liczb poziomów i rozgazie. Z przeprowadzonej w r. 7 dyskusji wynika, e takie struktury mona tworzy stosunkowo atwo, gdy klasa pochodna dziedziczy wasnoci kilku niezalenych (rozcznych) klas bazowych, nie majcych wspólnej superklasy. Jeeli jednak bezporednie klasy bazowe danej klasy pochodnej s zalene, naley zastosowa omówione niej mechanizmy jzykowe.
8.1. Wirtualne klasy bazowe
Przedstawiony w p.7.3 przykad ilustruje niejednoznacznoci, jakie mog si pojawi w hierarhii dziedziczenia, gdy klasa pochodna dziedziczy t sam klas bazow kilkakrotnie, idc po rónych krawdziach grafu dziedziczenia. Odwoania do elementów skadowych takiej klasy bazowej s wówczas moliwe, ale kopotliwe (np. obiekt.Pochodna1::a). Jzyk C++ oferuje tutaj mechanizm, dziki któremu “klasy siostrzane” wspódziel informacj (w tym przypadku jeden obiekt wspólnej klasy bazowej) bez wpywu na inne klasy w grafie dziedziczenia. Mechanizm ten polega na potraktowaniu wspólnej klasy bazowej jako klasy wirtualnej w klasach “siostrzanych”, a przywouje si go, piszc sowo kluczowe virtual przed lub po specyfikatorze dostpu, a przed nazw klasy bazowej. Wirtualno wspólnej klasy bazowej jest wasnoci stosowanego schematu dziedziczenia, a nie samej klasy, która poza tym niczym si nie róni od klasy niewirtualnej. Jeeli przy dziedziczeniu mnogim klasa pochodna dziedziczy t sam klas bazow jako wirtualn i * idc po innej gazi * jako niewirtualn, to oczywicie niejednoznacznoci nie usuniemy. Ilustracj tego jest rysunek 8-1, który pokazuje schemat dziedziczenia z wirtualnymi i niewirtualnymi klasami bazowymi.
Rys. 8-1 Dziedziczenie mnogie z wirtualnymi klasami bazowymi
W prezentowanym grafie dziedziczenia leca najniej w hierarchii klasa pochodna Z dziedziczy cechy szeciu swoich klas bazowych, przy czym klasy F, C, D i E s jej bezporednimi klasami bazowymi, za A i B * porednimi. Klasy B i E wspódziel jeden obiekt klasy A, poniewa klasa A jest w kadej z nich deklarowana jako wirtualna klasa bazowa. Natomiast kady obiekt klas C i D bdzie zawiera wasn kopi zmiennych skadowych klasy A. W rezultacie kady obiekt klasy Z bdzie zawiera trzy kopie zmiennych skadowych klasy A: jedn przez dwie gazie wirtualne (przez E i B/F) i po jednej z gazi C i D.
Pokazany schemat mona opisa przykadowymi deklaracjami:
class A {
public:
void f() { cout << "A::f()\n"; }
};
class B: virtual public A { };
class c: public A { };
class D: public A { };
class E: virtual public A { };
class F: public B { };
class Z: public F, public C, public D, public E { };
Gdyby zadeklarowa obiekt klasy Z:
Z obiekt;
to kade bezporednie wywoanie funkcji f() z tego obiektu
Z.f();
bdzie niejednoznaczne, a wic bdne.
Wywoania funkcji f() mona uczyni jednoznacznymi, odwoujc si do niej poprzez obiekty klas porednich, które zawieraj dokadnie po jednej kopii obiektu klasy A:
obiekt.C::f();
obiekt.D::f();
obiekt.E::f();
obiekt.F::f();
Niejednoznaczne bdzie równie wywoanie za pomoc wskanika do klasy Z:
Z* wsk = new Z;
wsk->f();
chocia i w tym przypadku moemy woa funkcj f() poprzez adresy obiektów klas porednich:
wsk->C::f();
wsk->D::f();
wsk->E::f();
wsk->F::f();
Wszystkie powysze wywoania porednie maj skadni raczej mao zachcajc. Gdyby w klasie A zadeklarowa zmienne skadowe, to odwoania do nich byyby podobne.
Oczywistym sposobem usunicia niejednoznacznoci z dyskutowanego schematu byoby zadeklarowanie klasy A jako wirtualnej klasy bazowej w pozostaych klasach porednich, tj. C i D. Takie wanie zaoenie przyjto w prezentowanym niej programie, który korzysta ze znacznie prostszego schematu dziedziczenia.
Przykad 8.1.
Schemat dziedziczenia: Bazowa
/ \
/ \
Pochodna1 Pochodna2
\ /
\ /
DwieBazy
#include <iostream.h>
class Bazowa {
public:
Bazowa(): a(0) {}
int a;
};
class Pochodna1: virtual public Bazowa {
public:
Pochodna1(): b(0) {}
int b;
};
class Pochodna2: virtual public Bazowa {
public:
Pochodna2(): c(0) {}
int c;
};
class DwieBazy: public Pochodna1, public Pochodna2 {
public:
DwieBazy() {}
int iloczyn() { return a*b*c; }
};
int main() {
DwieBazy obiekt;
obiekt.a = 4; obiekt.b = 5; obiekt.c = 6;
cout << "Iloczyn wynosi: "
<< obiekt.iloczyn() << endl;
return 0;
}
Dyskusja. Instrukcja deklaracji DwieBazy obiekt; wywouje konstruktor domylny DwieBazy() {}. Konstruktor ten najpierw wywouje konstruktor Bazowa(){ a = 0; }, a nastpnie konstruktory domylne Pochodna1() i Pochodna2(). W rezultacie obiekt klasy DwieBazy bdzie zawiera po jednym pod-obiekcie klas Bazowa, Pochodna1 i Pochodna2.
Pozostaa cz programu nie wymaga obszerniejszego komentarza. Zauwamy jedynie, e w definicji funkcji iloczyn() wyraenie a*b*c jest równowane:
Bazowa::a*Pochodna1::b*Pochodna2::c.
Równie poprawny byby zapis
obiekt.Bazowa::a, ale duszy od obiekt.a.
•
Jeeli wirtualna klasa bazowa zawiera konstruktory, to jeden z nich musi by konstruktorem domylnym, albo konstruktorem z inicjalnymi wartociami domylnymi dla wszystkich argumentów. Konstruktor domylny bdzie woany bez argumentów, jeeli aden konstruktor klasy bazowej nie jest wywoywany jawnie z listy inicjujcej konstruktora klasy pochodnej. Ponadto dla wirtualnej klasy bazowej obowizuj nastpujce reguy:
Konstruktor wirtualnej klasy bazowej musi by wywoywany z tej klasy pochodnej, która faktycznie tworzy obiekt; wywoania z porednich klas bazowych bd ignorowane.
Jeeli deklaruje si wskanik do obiektów wirtualnej klasy bazowej, to nie mona go przeksztaci we wskanik do obiektów klasy pochodnej, poniewa w klasie bazowej nie ma informacji o obiektach klas pochodnych. Natomiast konwersja wskanika w kierunku odwrotnym, tj. z klasy pochodnej do klasy bazowej jest dopuszczalna, gdy kady obiekt klasy pochodnej zawiera wskanik do wirtualnej klasy bazowej. Ta wasno wskaników jest bardzo wana, poniewa odgrywa ona kluczow rol przy definiowaniu i wykorzystaniu funkcji polimorficznych, nazywanych w jzyku C++ funkcjami wirtualnymi.
•
Podany niej przykad ilustruje wymienione cechy wirtualnych klas bazowych. Klasa Bazowa jest teraz wyposaona w konstruktor z domyln wartoci argumentu, za wszystkie klasy pochodne maj konstruktory domylne. Konstrukcja obiektu klasy DwieBazy zaczyna si od wywoania konstruktora DwieBazy():Bazowa(300){}, który najpierw wywouje konstruktor klasy Bazowa, a nastpnie konstruktory klas Pochodna1 i Pochodna2. aden z tych konstruktorów nie wywouje konstruktora klasy Bazowa, poniewa podobiekt tej klasy zosta ju utworzony po wywoaniu konstruktora klasy Bazowa z bloku DwieBazy(). Sprawdzeniu tego faktu suy instrukcja cout << obiekt.a << endl;, która wydrukuje warto 300. Deklaracja wskanika wskb suy do ilustracji konwersji z Pochodna1* do Bazowa*, za potraktowana jako komentarz instrukcja wskp = (Pochodna1*)wskb; ilustruje brak konwersji z typu Bazowa* do Pochodna1*. Wynika to bezporednio z arytmetyki wskaników: wartoci wyraenia wskb++ byby adres nastpnego obiektu klasy Bazowa, za wskp++ powinno si odnosi do nastpnego obiektu klasy Pochodna.
Zauwamy, e gdyby doda deklaracje:
Pochodna1 obiekt1;
Pochodna2 obiekt2;
to kady z tych obiektów zawieraby podobiekt klasy Bazowa z a==100 i a==200, poniewa za kadym razem z bloku konstruktora klasy pochodnej byby wywoany konstruktor klasy Bazowa.
Przykad 8.2.
#include <iostream.h>
class Bazowa {
public:
Bazowa(int i = 0): a(i) {}
int a;
};
class Pochodna1: virtual public Bazowa {
public:
Pochodna1(): Bazowa(100) {}
int b;
};
class Pochodna2: virtual public Bazowa {
public:
Pochodna2(): Bazowa(200) {}
int c;
};
class DwieBazy: public Pochodna1, public Pochodna2 {
public:
DwieBazy(): Bazowa(300) {}
};
int main() {
DwieBazy obiekt;
obiekt.b = 5; obiekt.c = 6;
cout << obiekt.a << endl;
Bazowa* wskb;// wskb jest typu Bazowa*
Pochodna1* wskp;//wskp jest typu Pochodna*
wskb = (Bazowa*)wskp;
//Brak konwersji z Bazowa* do Pochodna1*
// wskp = (Pochodna1*)wskb;
return 0;
}
8.2. Funkcje wirtualne
Omawiajc przecianie funkcji oraz operatorów stwierdzilimy, e mechanizm ten realizuje polimorfizm, rozumiany jako “jeden interfejs (operacja), wiele metod (funkcji)”. Jest to polimorfizm z wizaniem wczesnym (nazywanym take statycznym), poniewa rozpoznanie waciwego wywoania i ustalenie adresu funkcji przecionej nastpuje w fazie kompilacji programu. Dziki temu wywoania funkcji z wizaniem wczesnym nale do najszybszych. Wizanie wczesne zachodzi równie dla “zwykych” funkcji oraz nie-wirtualnych funkcji skadowych klasy i klas zaprzyjanionych.
W jzyku C++ istnieje ponadto bardziej finezyjny i gitki mechanizm, znany pod nazw funkcji wirtualnych. Mechanizm ten odnosi si do tzw. wizania pónego, czyli sytuacji, gdy adres wywoywanej funkcji nie jest znany w fazie kompilacji, lecz jest ustalany dopiero w fazie wykonania.
Wizanie wywoania z definicj funkcji wirtualnej nie jest moliwe w fazie kompilacji, poniewa funkcje wirtualne maj dokadnie takie same prototypy w caej hierarchii klas, a róni si jedynie ciaem funkcji.
W schemacie dziedziczenia wizane statycznie zwyke funkcje skadowe klasy równie mog mie takie same prototypy w klasach bazowych i pochodnych, a róni si tylko zawartoci swoich bloków. W takich przypadkach funkcja klasy pochodnej nie zastpuje funkcji klasy bazowej, lecz j ukrywa. Sytuacja ta jest podobna do ukrywania nazw zmiennych w blokach zagniedonych. Poniewa funkcje wirtualne nie speniaj kryteriów wymaganych dla funkcji przecionych, nie mog by rozrónione w omawianym w rozdziale 5 procesie rozpoznawania i dopasowania. Tym niemniej kompilator pozwala je rozróni dziki omawianej dalej regule dominacji; ponadto programista moe uy w tym celu operatora zasigu “::” dla klasy. Technika ta sprawdza si w przypadku dziedziczenia pojedynczego. Jednak dla dziedziczenia mnogiego, jak pokazano na pocztku tego rozdziau (nawet dla wirtualnych klas bazowych), kontrola wywoa staje si kopotliwa, a dla zoonych grafów dziedziczenia zawodzi.
Mechanizm funkcji wirtualnych mona wi okreli jako polimorfizm z wizaniem pónym, a programowanie, oparte o hierarchi klas i funkcje wirtualne jest czsto utosamiane z obiektowym stylem programowania.
Dogodnym narzdziem dla operowania funkcjami wirtualnymi s wskaniki i referencje do obiektów. Im te powicimy obecnie wicej uwagi.
8.2.1. Wskaniki i referencje w hierarchii klas
Wskaniki i referencje uywalimy wielokrotnie i w rónych kontekstach. Nie zwracalimy natomiast uwagi na szczególne wasnoci wskaników w hierarchii klas. Tymczasem dla efektywnego posugiwania si funkcjami wirtualnymi potrzebujemy takiego sposobu odwoywania si do obiektów rónych klas, który nie wymaga faktycznej zmiany obiektu, do którego si odwoujemy. Sposób taki istnieje i opiera si na nastpujcej wasnoci wskaników i referencji: zmienn wskanikow (referencyjn) zadeklarowan jako wskanik do klasy bazowej mona uy dla wskazania na dowoln klas pochodn od tej klasy bazowej bez uywania jawnej konwersji typu. Jeeli wic wemiemy deklaracje:
class Bazowa { /* ... */ };
class Pochodna: public Bazowa { /* ... */ };
Bazowa baz; Pochodna po;
to moemy zapisa nastpujce poprawne instrukcje:
Bazowa* wskb = &baz;
wskb = &po;
Bazowa& refb = po;
Przy tych samych deklaracjach poprawne bd równie instrukcje:
Bazowa* wskb = new Bazowa;
wskb = &po;
Przykad 8.3.
#include <iostream.h>
class Bazowa {
public:
void ustawx(int n) { x = n; }
void podajx() { cout << x << '\t'; }
private:
int x;
};
class Pochodna : public Bazowa {
public:
void ustawy(int m) { y = m; }
void podajy() { cout << y << '\t'; }
private:
int y;
};
int main() {
Bazowa *wsk; //wskaz do klasy Bazowa
Bazowa obiekt1; // Obiekt klasy Bazowa
Pochodna obiekt2; //Obiekt klasy Pochodna
wsk = &obiekt1; //Przypisz do wsk adres obiekt1
wsk->ustawx(10); //Ustaw x w obiekt1
obiekt1.podajx(); //Alternatywa: wsk->podajx()
wsk = &obiekt2; //Przypisz do wsk adres obiekt2
wsk->ustawx(20); //Ustaw x w podobiekcie obiekt2
wsk->podajx();
// wsk->ustawy(30); Nielegalna
obiekt2.ustawy(30);
// wsk->podajy(); Nielegalna
obiekt2.podajy();
return 0;
}
Wydruk z programu bdzie mia posta:
10 20 30
Komentarz. Instrukcje wsk->ustawy(30); oraz wsk->podajy(); byyby nielegalne, mimo e wskanik wsk jest ustawiony na adres obiektu klasy pochodnej. Jest to oczywiste, poniewa wsk pozwala na dostp tylko do tych skadowych obiektu klasy pochodnej, które s dziedziczone z klasy bazowej.
Zanotujmy w tym miejscu kilka uwag.
Wskaniki i referencje odwouj si do obiektów poprzez ich adresy, które maj ten sam rozmiar bez wzgldu na klas, do której naley wskazywany obiekt.
Z arytmetyki wskaników wynika, e zwikszenie o 1 wartoci wskanika brane jest w odniesieniu do zadeklarowanego typu danych. Tak wic, jeeli wskanik wskb typu Bazowa* wskazuje na obiekt klasy Pochodna, to warto wskb++ bdzie si odnosi do nastpnego obiektu klasy Bazowa, a nie klasy Pochodna.
Mimo, e moemy uywa wskanika wskb do wskazywania obiektu klasy pochodnej, to jednak w tym przypadku uzyskamy dostp jedynie do tych elementów (zmiennych i funkcji) klasy pochodnej, które odziedziczya od klasy bazowej (zarówno bezporedniej, jak i poredniej). Powodem jest to, e wskanik do klasy bazowej dysponuje jedynie informacj o tej klasie, a nie “wie” nic o elementach, dodanych przez klas pochodn.
Wprawdzie jest dopuszczalne, aby wskanik do klasy bazowej wskazywa obiekt klasy pochodnej, to jednak twierdzenie odwrotne nie jest prawdziwe. Jest to spowodowane faktem, e kompilatory nie maj mechanizmu sprawdzania dla fazy wykonania czy konwersje w instrukcjach przypisania: wskb = wskp; gdzie wskp jest wskanikiem do klasy pochodnej, pozostawiaj wynik, wskazujcy na obiekt oczekiwanego typu.
8.2.2. Deklaracje funkcji wirtualnych
Funkcja wirtualna jest to funkcja skadowa klasy, zadeklarowana w klasie bazowej i redefiniowana w klasach pochodnych. Deklaracja funkcji wirtualnej odrónia si od deklaracji zwykej funkcji skadowej jedynie tym, e przed nazw typu zwracanego umieszcza si sowo kluczowe virtual, np.
virtual void f();
virtual char* g() const;
Nazwy funkcji wirtualnych, wywoywanych za porednictwem wskaników lub referencji do obiektów, wizane s z ich adresami w fazie wykonania. Jest to omawiane wczeniej wizanie póne (dynamiczne). Natomiast funkcje zadeklarowane jako wirtualne, a wywoywane dla obiektów, s wizane w fazie kompilacji (wizanie wczesne, albo statyczne). Przyczyn tego jest fakt, e typ obiektu jest ju znany po kompilacji. Np. dla deklaracji:
class Test {
public:
virtual void ff();
};
Test t1;
wywoanie:
t1.ff();
bdzie wizane statycznie, a wi funkcja ff() dostanie adres w fazie kompilacji i straci cech wirtualnoci.
Prezentowany niej program wydrukuje warto x: 10. Zwrómy uwag na definicj funkcji podaj(): sowo kluczowe virtual wystpuje tylko w jej deklaracji; bdem syntaktycznym byoby umieszczenie go w definicji funkcji, umieszczonej poza ciaem klasy.
Przykad 8.4.
#include <iostream.h>
class Bazowa {
public:
int x;
Bazowa(int i): x(i) {}
virtual void podaj();
};
void Bazowa::podaj()
{ cout << "x: " << x << endl; }
int main() {
Bazowa *wsk;
Bazowa obiekt(10);
wsk = &obiekt;
wsk->podaj();
return 0;
}
Przykad 8.5.
#include <iostream.h>
class Bazowa {
public:
int x;
Bazowa(int i): x(i) {}
virtual void podaj();
};
void Bazowa::podaj()
{ cout << "x = "<< x << endl; }
class Pochodna1 : public Bazowa {
public:
Pochodna1(int x): Bazowa(x) {}
void podaj();
};
void Pochodna1::podaj()
{ cout << "x + x = "<< x + x << endl; }
class Pochodna2 : public Bazowa {
public:
Pochodna2(int x): Bazowa(x) {}
};
int main() {
Bazowa* wsk;
Bazowa obiekt(10);
Pochodna1 obiekt1(20);
Pochodna2 obiekt2(30);
wsk = &obiekt;
wsk->podaj();
wsk = &obiekt1;
wsk->podaj();
wsk = &obiekt2;
wsk->podaj();
return 0;
}
Dyskusja. Wirtualna funkcja skadowa podaj() jest redefiniowana jedynie w klasie pochodnej Pochodna1; w klasie Pochodna2 musi by uywana definicja z klasy Bazowa. W rezultacie wydruk z programu bdzie mia posta:
x = 10
x + x = 40
x = 30
W programie wszystkie wizania funkcji wirtualnej podaj() byy wizaniami dynamicznymi, realizowanymi w fazie wykonania programu. Gdyby wywoania funkcji podaj() zwiza z tworzonymi obiektami, a nie ze wskanikami do tych obiektów, np. obiekt1.podaj(), to wizania miayby miejsce w fazie kompilacji, a wi byyby wizaniami wczesnymi (statycznymi).
•
Dokonamy teraz przegldu podstawowych wasnoci funkcji wirtualnych.
•
Funkcje wirtualne mog wystpowa jedynie jako funkcje skadowe klasy. Zatem, podobnie jak funkcje skadowe rozwijalne i statyczne, nie mog by deklarowane ze specyfikatorem extern.
Specyfikatora virtual nie wolno uywa w deklaracjach statycznych funkcji skadowych. Nie wolno, poniewa wywoanie funkcji wirtualnej odnosi si do konkretnego obiektu, który uywa zredefiniowanej treci funkcji. Inaczej mówic, funkcja wirtualna wykorzystuje niejawny wskanik this, którego nie ma funkcja statyczna.
Funkcja wirtualna moe by zadeklarowana jako zaprzyjaniona (friend) w innej klasie.
Funkcja wirtualna, zdefiniowana w klasie bazowej, nie musi by redefiniowana w klasie pochodnej. W takim przypadku wywoanie z obiektu klasy pochodnej bdzie korzysta z definicji, zamieszczonej w klasie bazowej.
Jeeli definicja funkcji wirtualnej z klasy bazowej zostanie przesonita now definicj w klasie pochodnej, to funkcja przesaniajca jest równie uwaana za wirtualn.
Specyfikator virtual moe by uyty dla funkcji przesaniajcej w klasie pochodnej, ale bdzie to redundancja. Jednak taka rozwleko moe by korzystna dla celów dokumentacyjnych. Jeeli w trakcie czytania tekstu programu chcemy sprawdzi, czy dana definicja odnosi si do funkcji wirtualnej, czy te nie, specyfikator virtual zaoszczdzi nam czas na poszukiwanie.
Jzyk C++ wymaga dokadnej zgodnoci deklaracji pomidzy funkcj wirtualn w klasie bazowej a funkcj, która j przesania w klasie pochodnej. Inaczej mówic, funkcje te musz mie identyczne sygnatury, tj. liczb, kolejno i typy argumentów oraz ten sam typ zwracany. Ewentualne naruszenie tej zgodnoci spowoduje, e kompilator nie bdzie traktowa funkcji zredefiniowanej jako wirtualnej, lecz jako funkcj przecion, bd now funkcj skadow.
Ostatnio komitet normalizacyjny ANSI X3J16 rozluni powysze ograniczenie. Teraz dopuszcza si niezgodno typów funkcji w nastpujcym przypadku: jeeli oba typy zwracane s wskanikami lub referencjami do klas i jeeli klasa w typie zwracanym przez funkcj wirtualn klasy pochodnej jest publicznie dziedziczona od klasy w typie zwracanym przez funkcj wirtualn klasy bazowej. Dla tej nowej wykadni podane niej deklaracje (bdne przy poprzedniej) bd poprawne:
class X { };
class Y : public X { };
class A {
public:
virtual X& fvirt();
};
class B : public A {
public:
virtual Y& fvirt();
};
Podsumujmy powysze uwagi. Skadnia wywoania funkcji wirtualnej jest taka sama, jak skadnia wywoania zwykej funkcji skadowej. Interpretacja wywoania funkcji wirtualnej zaley od typu obiektu, dla którego jest woana; jeeli np. mamy klas Test z zadeklarowan w niej funkcj wirtualn
virtual void ff();
to cig instrukcji
Test* wsk = new Test;
Test t1;
Test& tref = t1;
wsk->ff();
tref.ff();
mówi: “hej, adresowany obiekcie, wybierz swoj wasn funkcj ff() i wykonaj j.”
Inaczej mówic: poniewa obiekty s powoywane do ycia w fazie wykonania, zatem wywoanie funkcji wirtualnej musi by wizane z jedn z jej definicji (metod) dopiero w fazie wykonania.
Mona w tym miejscu postawi pytanie: w jaki sposób informacja o typie obiektu dla wywoania funkcji wirtualnej jest przekazywana przez kompilator do rodowiska wykonawczego? Odpowied na to pytanie nie moe abstrahowa od implementacji jzyka C++.
Przyjtym w jzyku C++ rozwizaniem jest implementacja funkcji wirtualnych za pomoc tablicy wskaników do funkcji wirtualnych. Np. przy dziedziczeniu pojedynczym kada klasa, w której zadeklarowano funkcje wirtualne, utrzymuje tablic wskaników do funkcji wirtualnych, a kady obiekt takiej klasy bdzie zawiera wskanik do tej tablicy. Wemy dla ilustracji nastpujcy schemat dziedziczenia:
class X {
public:
virtual void f();
virtual void g(int);
virtual void h(char*);
private:
int a;
};
class Y {
public:
void g(int);
virtual void r(Y*);
private:
int b:
};
class Z {
public:
void h(char*);
virtual void s(Z*)
private:
int c;
};
Przy powyszych deklaracjach struktura obiektu klasy Z bdzie podobna do pokazanej na rysunku 8-2.
Rys. 8-2 Wskanik do tablicy funkcji wirtualnych
Kady obiekt klasy Z bdzie zawiera ukryty wskanik do tablicy funkcji wirtualnych, nazywany w implementacjach vptr. Wywoanie funkcji wirtualnej jest transformowane przez kompilator w wywoanie porednie. Np. wywoanie funkcji g() z bloku funkcji f
void f(Z* wsk) { wsk->g(10); }
wygeneruje kod w rodzaju:
(*(wsk->vptr[1]))(wsk,10);
Zasada jest taka, e przy dziedziczeniu pojedynczym dla kadej klasy z funkcjami wirtualnymi utrzymywana jest dokadnie jedna tablica funkcji wirtualnych. Przy dziedziczeniu mnogim klasa z funkcjami wirtualnymi, pochodna np. od dwóch klas bazowych bdzie miaa dwie takie tablice; obiekt tej
klasy bdzie mia odpowiednio dwa ukryte wskaniki, po jednym do kadej z tablic. Powód jest oczywisty: kady obiekt klasy pochodnej od kilku klas bazowych bdzie zawiera podobiekty tych klas, a z kadym podobiektem bdzie skojarzona jego wasna tablica funkcji wirtualnych.
8.2.3. Zasig i regua dominacji
Jak pamitamy, kada klasa wyznacza swój wasny zasig, a jej zmienne i funkcje skadowe mieszcz si w zasigu swojej klasy. Nazwy w zasigu klasy mog by dostpne dla kodu zewntrznego w stosunku do klasy, jeeli uyjemy do tego celu operatora zasigu “::”.
Zasig odgrywa kluczow rol w mechanizmie dziedziczenia. Z punktu widzenia klasy pochodnej dziedziczenie wprowadza nazwy z zasigu klasy bazowej w zasig klasy pochodnej. Inaczej mówic, nazwy zadeklarowane w klasach bazowych s dziedziczone przez klasy pochodne.
Nazwy w zasigu klasy pochodnej s w podobnej relacji do nazw w klasie bazowej, jak nazwy w bloku wewntrznym do nazw w bloku go otaczajcym. W bloku wewntrznym zawsze moemy uy nazwy zadeklarowanej w bloku zewntrznym. Jeeli w bloku wewntrznym zdefiniujemy tak sam nazw, jak zdefiniowana w bloku zewntrznym, to nazwa w bloku wewntrznym ukryje nazw z bloku zewntrznego.
Przy dziedziczeniu mnogim tworzenie obiektu klasy pochodnej zaczyna si zawsze od utworzenia podobiektów wczeniej zadeklarowanych klas bazowych. W tym przypadku zasig klasy pochodnej jest zagniedony w zasigach wszystkich jej klas bazowych. Jeeli dwie klasy bazowe zawieraj t sam nazw, wtedy albo jedna nazwa musi dominowa nad drug, albo klasa pochodna musi usun niejednoznaczno przez ukrycie deklaracji z klas bazowych. Dopuszczalno przesaniania nazw z rónych gazi drzewa lub grafu dziedziczenia wymaga sformuowania zasady okrelajcej, jakie kombinacje mog by akceptowane, a jakie naley odrzuci jako bdne.
Zasada taka, nazywana regu dominacji, zostaa sformuowana przez B.Stroustrupa i A.Koeniga; brzmi ona nastpujco:
“Nazwa B::f dominuje nad nazw A::f, jeeli klasa B, w której f jest skadow, jest klas pochodn od klasy A. Jeeli pewna nazwa dominuje nad inn, to nie ma midzy nimi kolizji. Nazwa dominujca zostanie uyta wtedy, gdy istnieje wybór.”
Zauwamy, e regua dominacji stosuje si zarówno do funkcji, jak i zmiennych skadowych. Prezentowany niej przykad ilustruje dziaanie tej zasady w dziedziczeniu pojedynczym.
Przykad 8.6.
// Dominacja w dziedziczeniu pojedynczym
// bez klas i funkcji wirtualnych
#include <iostream.h>
class Bazowa {
public:
void f() { cout << "Bazowa::f()\n"; }
void g() { cout << "Bazowa::g()\n" << endl; }
};
class Pochodna1: public Bazowa {
public:
void f() { cout << "Pochodna1::f()\n"; }
};
class Pochodna2: public Bazowa {
public:
void g() { cout << "Pochodna2::g()\n" << endl; }
};
int main() {
Pochodna1 po;
po.f();
po.g();
return 0;
}
Z programu otrzymuje si wydruk o postaci:
Pochodna1::f()
Bazowa::g()
Dyskusja. W instrukcji po.f(); wywoywana jest funkcja f() klasy Pochodna1, poniewa nazwa Pochodna1::f dominuje nad nazw Bazowa::f. Podobny efekt daoby wywoanie funkcji g() dla obiektu klasy Pochodna2. Natomiast instrukcja po.g(); wywouje Bazowa::g(), poniewa w klasie Pochodna1 nazwa g nie wystpuje, ale klasa ta ma dostp do funkcji skadowej g() klasy Bazowa.
Przykad 8.7.
/* Dominacja w dziedziczeniu mnogim z klasami
wirtualnymi, lecz bez funkcji wirtualnych
*/
#include <iostream.h>
class Bazowa {
public:
void f() { cout << "Bazowa::f()\n"; }
};
class Pochodna2: virtual public Bazowa {
public:
void f() { cout << "Pochodna2::f()\n"; }
};
class Pochodna1:
virtual public Bazowa,
virtual public Pochodna2 { };
int main() {
Pochodna1 po1;
po1.f();
return 0;
}
Dyskusja. Wydruk z programu ma posta: Pochodna2::f(). Nazwa f nie wystpuje w deklaracji klasy Pochodna1, natomiast wystpuje w klasach Bazowa i Pochodna2, które s publicznymi wirtualnymi klasami bazowymi klasy Pochodna1. Z reguy dominacji wynika, e nazwa f w klasie Pochodna2 dominuje nad t sam nazw w jej wirtualnej klasie bazowej.
Nastpny przykad ilustruje w peni korzyci, wynikajce z reguy dominacji. Schemat dziedziczenia jest tutaj grafem acyklicznym, w którym dwie klasy dziedzicz od wspólnej publicznej, wirtualnej klasy bazowej. Klasy te s bezporednimi publicznymi klasami bazowymi dla najniszej w hierarchii klasy pochodnej. Przyjty schemat dziedziczenia, wraz z deklaracjami funkcji wirtualnych we wspólnej klasie bazowej i wywoaniami tych funkcji przez wskaniki zapewnia jednoznaczno odwoa.
Przykad 8.8.
//Dominacja: klasy i funkcje wirtualne
#include <iostream.h>
class Bazowa {
public:
virtual void f() { cout << "Bazowa::f()\n"; }
virtual void g() { cout << "Bazowa::g()\n"; }
};
class Pochodna1: virtual public Bazowa {
public:
void g() { cout << "Pochodna1::g()\n"; }
};
class Pochodna2: virtual public Bazowa {
public:
void f() { cout << "Pochodna2::f()\n"; }
};
class Pochodna12:
public Pochodna1, public Pochodna2 { };
int main() {
Pochodna12 obiekt;
Pochodna12* wsk12 = &obiekt;
wsk12->f();
wsk12->g();
Pochodna1* wsk1 = wsk12;
wsk1->f();
Pochodna2* wsk2 = wsk12;
wsk2->g();
return 0;
}
Wykonanie programu da wydruk o postaci:
Pochodna2::f
Pochodna1::g()
Pochodna2::f()
Pochodna1::g()
Analiza programu. Wywoanie wsk12->f() zostanie rozpoznane jako odnoszce si do nazwy f w klasie Pochodna2, co wynika z reguy dominacji i z faktu, e funkcja f() zostaa zadeklarowana jako wirtualna. To samo odnosi si do pozostaych wywoa.
8.2.4. Wirtualne destruktory
Destruktor jest, podobnie jak konstruktor, specjaln funkcj skadow klasy. Jak wiadomo, zadaniem konstruktora jest inicjowanie zmiennych skadowych przy tworzeniu obiektu; destruktor wykonuje wszelkie czynnoci zwizane z usuwaniem obiektu, jak dealokacja uprzednio przydzielonej pamici operatorem new, itp.
Jeeli w klasie nie zadeklarowano destruktora, to bdzie niejawnie wywoywany konstruktor domylny generowany przez kompilator. Wywoanie to ma miejsce, gdy koczy si okres ycia obiektu, np. gdy sterowanie opuszcza blok, w którym zadeklarowano obiekt lokalny lub dla obiektów statycznych gdy koczy si cay program.
Destruktor moe by równie zdefiniowany w klasie; wówczas bdzie on wywoywany niejawnie zamiast destruktora generowanego przez kompilator. Taki destruktor mona te wywoywa jawnie; np. dla deklaracji:
class Test {
public:
Test() { };
~Test() { };
};
destruktor ~Test() moe by wywoany dla obiektu klasy Test z kwalifikatorem zawierajcym nazw klasy i operator zasigu lub bez, zalenie od kontekstu:
Test t1;
t1.~Test();
t1.Test::~Test();
Konieczno definiowania wasnych destruktorów zachodzi wtedy, gdy przy tworzeniu obiektu s alokowane jakie oddzielnie zarzdzane zasoby, np. gdy wewntrz obiektu jest tworzony w pamici swobodnej podobiekt. Wtedy zadaniem destruktora bdzie zwolnienie tego obszaru pamici.
Jawne wywoanie destruktora na rzecz jakiego obiektu powoduje jego wykonanie, ale niekoniecznie zwalnia pami przydzielon zmiennym obiektu i nie zawsze oznacza cakowite zakoczenie ycia obiektu; co wicej, niewaciwie zaprojektowany destruktor moe próbowa zwalnia ju uprzednio zwolnione zasoby. Podany niej przykad ilustruje tak wasnie sytuacj.
Przykad 8.9.
#include <iostream.h>
class Test {
public:
enum { n = 10 };
double* wsk;
Test() { wsk = new double[n]; }
~Test()
{
if(wsk)
{
delete [] wsk;
cout << "Zniszczenie wsk\n";
// wsk = NULL;
}
else cout << "wsk==NULL\n";
}
};
int main() {
Test t1;
t1.~Test();
return 0;
}
Dyskusja. Jeeli instrukcja wsk = NULL; bdzie traktowana jako komentarz, to wykonanie programu da wydruk:
Zniszczenie wsk
Zniszczenie wsk
W tym przypadku destruktor by wywoywany dwukrotnie: raz jawnie instrukcj t1.~Test(); i drugi raz niejawnie przy ostatecznym usuwaniu obiektu t1 tu przed zakoczeniem programu. Dopuszczenie do takiej sytuacji jest oczywistym bdem programistycznym.
Jeeli z programu usuniemy symbol komentarza przed instrukcj wsk = NULL; to zostanie ona wykonana, a wydruk bdzie mia posta:
Zniszczenie wsk
wsk==NULL
Teraz destrukcja obiektu t1 przebiega poprawnie: przy pierwszym wywoaniu destruktor zwalnia pami zajmowan przez tablic dziesiciu liczb typu double i nastpnie przypisuje wskanikowi wsk adres pusty. Przy drugim wywoaniu drukuje napis wsk==NULL i usuwa to, co jeszcze pozostao z obiektu t1 (w t czynno nie wchodzimy, poniewa zaley ona od implementacji, a wic mieci si na niszym poziomie abstrakcji w stosunku do klas i obiektów).
Problemy z destrukcj obiektów uzyskuj dodatkowy wymiar, gdy uwzgldnimy dziedziczenie. Ilustruje to nastpny przykad.
Przykad 8.10.
#include <iostream.h>
class Bazowa {
public:
Bazowa() { cout << "Konstruktor klasy bazowej\n"; }
~Bazowa() { cout << "Destruktor klasy bazowej\n"; }
};
class Pochodna: public Bazowa {
public:
Pochodna()
{ cout << "Konstruktor klasy pochodnej\n"; }
~Pochodna()
{ cout << "Destruktor klasy pochodnej\n"; }
};
int main() {
Bazowa* wskb = new Pochodna;
delete wskb;
return 0;
}
Analiza programu. W przykadzie posuono si wskanikiem wskb do klasy bazowej, któremu przypisano obiekt klasy pochodnej. Wydruk z programu ma posta nieoczekiwan:
Konstruktor klasy bazowej
Konstruktor klasy pochodnej
Destruktor klasy bazowej
Konstrukcja obiektu klasy pochodnej przebiega prawidowo (najpierw jest tworzony obiekt klasy bazowej, a nastpnie pochodnej). Natomiast po wykonaniu instrukcji delete wskb; w sytuacji gdy wskanik by ustawiony na adres obiektu klasy pochodnej mona si byo spodziewa wywoania destruktora klasy
pochodnej, a nie bazowej. Rzecz w tym, e wywoanie delete “nic nie wie” o tym, i obiekt jest klasy Pochodna, a w klasie Bazowa nie ma adnej wskazówki, e wywoania destruktora maj przeszukiwa hierarchi klas.
•
Konsekwencje opisanej wyej sytuacji zostay ju zasygnalizowane wczeniej. Jeeli konstruktor klasy pochodnej przydzieli obiektowi tej klasy pewne zasoby, które mia zwolni jej destruktor, to dziaanie takie nie zostanie wykonane i zasoby te stan si tzw. “nieuytkami” ( ang. garbage).
Rozwizaniem tego problemu jest wprowadzenie destruktorów wirtualnych. Nastpny przykad ilustruje sposób definiowania destruktora wirtualnego w klasach bazowej i pochodnej oraz skadni jego wywoania.
Przykad 8.11.
// Destruktor wirtualny
#include <iostream.h>
class Bazowa {
public:
Bazowa()
{ cout << "Konstruktor klasy bazowej\n"; }
virtual ~Bazowa()
{ cout << "Destruktor klasy bazowej\n"; }
};
class Pochodna: public Bazowa {
public:
Pochodna()
{ cout << "Konstruktor klasy pochodnej\n"; }
~Pochodna()
{ cout << "Destruktor klasy pochodnej\n"; }
};
int main() {
Bazowa* wskb = new Pochodna;
delete wskb;
return 0;
}
Dyskusja. Program wydrukuje nastpujce napisy:
Konstruktor klasy bazowej
Konstruktor klasy pochodnej
Destruktor klasy pochodnej
Destruktor klasy bazowej
Teraz konstrukcja i destrukcja obiektu przebiega poprawnie. Dzieje si to za spraw destruktora klasy bazowej, zadeklarowanego ze sowem kluczowym virtual. Wirtualny destruktor klasy bazowej zosta zredefiniowany w klasie pochodnej; musielimy uy w tym celu nazwy klasy pochodnej, czyli zrobi odstpstwo od zasad definiowania funkcji wirtualnych. Jest to (na szczcie) odstpstwo dopuszczalne, jeli chcemy zachowa zasady nazewnictwa konstruktorów i destruktorów. Funkcja wirtualna, jak jest wirtualny destruktor, jest woana dla wskanika do obiektu; zatem jej definicja bdzie wizana z wywoaniem dynamicznie, w fazie wykonania. Poniewa wskanik wskb adresuje obiekt klasy Pochodna, zatem instrukcja delete wskb; wywoa destruktor tej klasy. Oczywicie natychmiast po tym zostanie wywoany destruktor klasy Bazowa, zgodnie z obowizujc kolejnoci wywoania destruktorów dla klas pochodnych.
•
W przykadach destrukcji obiektów w drzewie dziedziczenia pominlimy spraw zwalniania dodatkowych zasobów, alokowanych dla obiektu przez konstruktor. Uczyniono tak w celu uproszczenia zapisu, aby pokaza inny aspekt problemu destrukcji. Oczywicie i w przypadku dziedziczenia kopoty ze zwalnianiem zasobów, czy te usuwaniem zasobów ju usunitych, s podobne, a czsto zwielokrotnione wskutek zastosowania mechanizmu dziedziczenia. Innym pominitym aspektem, o którym warto wspomnie, jest problem rozpoznania obiektu, dla którego jest wywoywany destruktor klasy bazowej. Jeeli usuwamy
obiekt klasy pochodnej, to najpierw jest woany destruktor tej klasy, a nastpnie destruktor klasy bazowej.
Tak wic w chwili wywoania destruktora klasy bazowej obiekt klasy pochodnej ju nie istnieje; istniej tylko jego skadowe, odziedziczone z klasy bazowej. W tej fazie destruktor nie moe si dowiedzie, czy nale one do obiektu klasy pochodnej, czy bazowej...
8.2.5. Klasy abstrakcyjne i funkcje czysto wirtualne
Funkcje wirtualne, definiowane w klasach bazowych “na szczycie” hierarchii klas, bardzo czsto s jedynie makietami, umieszczonymi z myl o napisaniu dla nich sensownych definicji w klasach pochodnych. Jest to sytuacja typowa, poniewa klasa bazowa czsto jest projektowana jako pewien prototyp dla klas pochodnych. Zwyke funkcje skadowe mog mie w takiej klasie puste bloki definicji. Jeeli funkcja wirtualna w klasie bazowej nie wykonuje adnego dziaania, to kada klasa pochodna musi przesoni t funkcj. Dla takiego przypadku przewidziano w jzyku C++ funkcje czysto wirtualne. W klasie bazowej wymagana jest jedynie deklaracja, wprowadzajca prototyp funkcji czysto wirtualnej (nie ma definicji). Ma ona posta:
virtual typ nazwa(wykaz parametrów) = 0;
Istotn czci tej deklaracji jest zwizanie ciaa (bloku) funkcji ze wskanikiem zerowym. Jest to informacja dla kompilatora, e nie istnieje ciao tej funkcji dla klasy bazowej. Po otrzymaniu takiej informacji kompilator bdzie wymusza redefinicje funkcji czysto wirtualnej w kadej klasie pochodnej, która ma mie wystpienia (obiekty), poniewa funkcja czysto wirtualna jest dziedziczona jako czysto wirtualna.
Klasa bazowa, która zawiera conajmniej jedn funkcj czysto wirtualn, jest nazywana abstrakcyjn klas bazow. Nazwa jest uzasadniona tym, e taka klasa nie moe mie swoich wystpie, tj. obiektów; natomiast mona jej uywa dla tworzenia klas pochodnych. Klasa abstrakcyjna nie moe mie swoich wystpie, poniewa w sensie technicznym nie jest kompletnym typem ze wzgldu na brak definicji funkcji czysto wirtualnej. Zauwamy take, e jeli w klasie pochodnej od klasy abstrakcyjnej nie redefiniuje si odziedziczonej funkcji czysto wirtualnej, to taka klasa pochodna bdzie równie klas abstrakcyjn, pozbawion moliwoci tworzenia wasnych obiektów. Ponadto mona wymieni nastpujce wasnoci klas abstrakcyjnych:
Dopuszcza si deklarowanie wskaników i referencji do klas abstrakcyjnych.
Klasa abstrakcyjna nie moe by uyta jako typ argumentu, jako typ zwracany oraz jako typ w jawnej konwersji (zmiennymi, bd staymi tych typów musz by obiekty).
Funkcja czysto wirtualna w deklaracji abstrakcyjnej klasy bazowej wystpuje jedynie w postaci specyficznego prototypu. Tym niemniej mona zdefiniowa ciao tej funkcji na zewntrz deklaracji tej klasy i wywoywa j za pomoc operatora zasigu.
Przykad 8.12.
#include <iostream.h>
class Bazowa {
public:
virtual void czysta() = 0;
};
class Pochodna: public Bazowa {
public:
Pochodna() { }
void czysta()
{
cout << "Pochodna::czysta" << endl;
}
void ff() { Bazowa::czysta(); }
};
void Bazowa::czysta()
{ cout << "Bazowa::czysta" << endl; }
int main() {
Pochodna obiekt;
obiekt.Bazowa::czysta();
obiekt.ff();
obiekt.czysta();
Bazowa* wsk = &obiekt;
wsk->czysta();
return 0;
}
Wydruk z programu ma posta:
Bazowa::czysta
Bazowa::czysta
Pochodna::czysta
Pochodna::czysta
•
Podany niej rysunek i przykad ilustruje konstrukcj i wartociowanie wyrae arytmetycznych, reprezentowanych przez tzw. drzewa wyrae.
W informatyce drzewem nazywa si struktur zoon z wzów i gazi. Takie “informatyczne” drzewo przedstawia si graficznie w taki sposób, e najwyszym wzem u góry jest korze drzewa, za wzy u samego dou to jego licie, a wic odwrotnie, ni to czynimy przy rysowaniu drzew, wystpujcych w przyrodzie. Drzewa wyrae nale do podstawowych struktur, stosowanych przy konstrukcji kompilatorów i interpretatorów. Jeeli odniesiemy t struktur do terminologii obiektowej, to stwierdzimy, e korze drzewa odpowiada pewnej pierwotnej klasie bazowej, a kolejne wzy bd klasami pochodnymi, powizanymi w hierarchi dziedziczenia. Poniewa ywe drzewo rozgazia si w ten sposób, e zawsze z gazi grubszej wyrasta jedna lub wicej gazi cieszych, zatem w naszym “drzewie” bdziemy mie schemat dziedziczenia pojedynczego.
Na rysunku 8-3 pokazano kilka przykadowych drzew wyrae.
Rysunek 8-3 Przykady drzew wyrae
Najprostszym wyraeniem jest staa, np. 5. Jej reprezentacj w drzewie wyraenia jest cz (a) rysunku 8-3. Drzewo staej skada si z jednego wza, który jest zarazem korzeniem i liciem. Proste wyraenie arytmetyczne, 5 + 10, jest reprezentowane przez drzewo 8-3(b), w którym operator “+” odpowiada obiektowi klasy Suma, a stae 5 i 10 obiektom klasy constant. Czci (c) i (d) rysunku prezentuj wyraenia o rosncym stopniu zoonoci.
Przykad 8.13.
#include <iostream.h>
class Wrn {
public:
virtual int wart() = 0;
};
class Constant: public Wrn {
public:
Constant(int k): x(k) {}
int wart() { return x; }
private:
int x;
};
class Suma: public Wrn {
public:
Suma(Wrn* l, Wrn* p) { lewy = l; prawy = p; }
int wart() { return lewy->wart()+prawy->wart(); }
private:
Wrn* lewy;
Wrn* prawy;
};
class Odejm: public Wrn {
public:
Odejm(Wrn* l, Wrn* p) { lewy = l; prawy = p; }
int wart() { return lewy->wart()-prawy->wart(); }
private:
Wrn* lewy;
Wrn* prawy;
};
class Iloczyn: public Wrn {
public:
Iloczyn(Wrn* l, Wrn* p) { lewy = l; prawy = p; }
int wart() { return lewy->wart() * prawy->wart(); }
private:
Wrn* lewy;
Wrn* prawy;
};
class Iloraz: public Wrn {
public:
Iloraz(Wrn* l, Wrn* p) { lewy = l; prawy = p; }
int wart() { return lewy->wart() / prawy->wart(); }
private:
Wrn* lewy;
Wrn* prawy;
};
int main() {
Constant a(5);
Constant b(10);
Constant c(15);
Constant d(20);
Iloczyn e(&a, &b);
Iloczyn f(&c, &d);
Suma g(&e, &f);
Iloraz h(&f, &e);
cout << "a*b + c*d = " << g.wart() << endl;
cout << "c*d / a*b = " << h.wart() << endl;
return 0;
}
Wydruk z programu ma posta:
a*b + c*d = 350
c*d / a*b = 6
Dyskusja. W powyszym przykadzie starano si pokaza:
Zastosowanie abstrakcyjnej klasy bazowej (tutaj klasa Wrn) z czysto wirtualn funkcj skadow (funkcja wart()) do konstruowania drzewa dziedziczenia.
Zastosowanie wskaników do tak okrelonej klasy bazowej; zauwamy, e wskaniki do klasy Wrn w klasach pochodnych s skadowymi tych klas.
Konstrukcj drzewa wyraenia za pomoc struktury powizanej wskanikami, w której wzami s obiekty, a gaziami wskaniki. Dla tworzenia rónych wyrae zadeklarowano klasy pochodne Constant, Suma, Odejm, Iloczyn, Iloraz.
Wartociowanie wyraenia arytmetycznego za pomoc przekazywania komunikatów; komunikat (wywoanie funkcji) przesany do obiektu-korzenia drzewa wyzwala przesyanie kolejnych komunikatów do odpowiednich wzów drzewa. Po zakoczeniu wszystkich wywoa obiekt-korze jest w stanie odpowiedzie na pocztkowy komunikat, podajc warto wyraenia.
Klasa Wrn zawiera funkcj czysto wirtualn wart(), która jest redefiniowana w kadej z klas pochodnych. Dla dowolnego obiektu klasy pochodnej funkcja wart() zwraca warto wyraenia reprezentowanego przez swój obiekt i jego obiekty potomne. Oznacza to, e jeeli wywoamy wart()dla korzenia drzewa lub poddrzewa, to funkcja zwróci warto wyraenia, reprezentowanego przez cae drzewo lub poddrzewo.