Dziedziczenie
Wykład 6
Na czym polega?
Jest to technika w programowaniu obiektowym, która
pozwala na definiowanie nowej klasy czyli nowego typu
zmiennej obiektowej, za pomocą klasy czyli typu
zdefiniowanego wcześniej.
Przykład {Grebosz t.3}:
Załóżmy, że zdefiniowaliśmy klasę:
class punkt {
public:
float x,y;
punkt(float,float);
//
konstruktor
void wypisz(); //
metoda wypisywania informacji o
obiekcie
void
przesun(float,float); //
metoda
przesuwania
punktu
};
Załóżmy teraz, że pojawiła się potrzeba zastosowania
klasy definiującej nieco odmienny typ obiektów, w
których powinna pojawić się nowa zmienna
char opis[10];
oraz inna wersja funkcji wypisz (), taka, która wypisze
nie tylko współrzędna punktu, ale także umieszczona w
tablicy nazwę punktu.
Taką nowa klasę należy zdefiniować następująco:
class opisany_ punkt: public punkt {
public:
char opis[10]
opisany_ punkt(float=0, float=0, char*=NULL);
//
konstruktor
//
void wypisz();
//
dodana funkcja opisu obiektu
};
Opis nowej klasy po dwukropku public punkt wskazuje na
pochodzenie nowej klasy poprzez mechanizm dziedziczenia od już
istniejącej klasy punkt. To powoduje, że klasa opisany_punkt
dziedziczy zmienne x,y oraz funkcję void przesun(float,float)
Są one traktowane tak, jak gdyby były składnikami także tej nowej
klasy.
Słowo-etykieta public wskazuje na sposób dostępu do tych
zmiennych. W ramach mechanizmu dziedziczenia może być
stosowana także etykieta protected. Klasa opisany_punkt jest
klasą
pochodną
i słowo public pokazuje na charakter dostępu do
odziedziczonych zmiennych.
Istnieje tu jedno ważne ograniczenie. Jeżeli w klasie podstawowej
istniały składniki typu private to pomimo dziedziczenia nie będzie do
nich dostępu. Jeżeli w klasie pochodnej zastosujemy taką samą
nazwę zmiennej lub funkcji jak w klasie podstawowej, to w
działaniach programu będzie używana zmienna lub funkcja z klasy
pochodnej w zakresie ważności klasy pochodnej. Jest to
mechanizm przesłaniania. W podanym przykładzie funkcja
wypisz() z klasy pochodnej przesłania funkcję wypisz () z klasy
podstawowej. Pomimo wystąpienia funkcji o takiej samej nazwie nie
jest to mechanizm przeładowania nazwy funkcji bo każda z nich ma
inny zakres ważności. Przeładowanie dotyczy tylko występowania
funkcji o takiej samej nazwie w tym samym zakresie ważności.
Każdy składnik obydwu klas ma ważność tylko w zakresie
swojej klasy. Dlatego w mechanizmie przesłaniania funkcje mogą
mieć identyczne listy argumentów formalnych i aktualnych.
Składnik przesłaniany nie jest wyłączany z
działania
. Jest do niego dostęp i może być używany
także w zakresie klas pochodnych. Dostęp do takiego
składnika klasy musi być określany operatorem zakresu ::
. W podanym przykładzie powinno to być wykonywane
następująco:
opisany_punkt obiekt; //definicja obiektu
obiekt.wypisz();
//wywołanie funkcji z klasy
pochodnej do wykonania czynności na rzecz obiektu
obiekt.punkt::wypisz(); // wywołanie funkcji z klasy
podstawowej do wykonania czynności na rzecz tego
samego obiektu
Należy pamiętać, że dziedziczenie dotyczy klas czyli
typów zmiennej, a nie obiektów
Dziedziczenie i deklaracje dostępu
public, protected, private.
Dostęp do dziedziczonych składników klasy jest
ograniczany poprzez typ dostępu zdefiniowany etykietami
w klasie podstawowej. Do składników, które w klasie
podstawowej zostały podane jako private nie ma dostępu
mimo dziedziczenia. Tylko wprowadzenie funkcji typu
friend może dla niej udostępnić takie składniki ale funkcji
zaprzyjaźnionej nie dziedziczy się czyli mechanizm
dziedziczenia nie przełamuje ograniczeń etykiety private.
Składniki typu public i protected są dostępne w klasie
pochodnej. Pomimo, że składnik protected dla całego
zakresu poza klasą podstawową jest niedostępny, to w
zakresie klasy pochodnej jest dostępny poprzez
mechanizm dziedziczenia. Czyli o sposobie dostępu do
składników dziedziczonych decydujemy w klasie
podstawowej poprzez zastosowanie etykiet.
Klasa pochodna może ograniczyć dostęp do składników
dziedziczonych poprzez zakwalifikowanie w swoim
zakresie, że dziedziczony składnik public w jej zakresie
będzie protected albo private. Należy przy tym
pamiętać, że dostęp możemy zachować lub
ograniczyć, a nie możemy rozszerzyć. Zmiana dostępu
w zakresie klasy pochodnej jest realizowana poprzez
dodanie etykiet dostępu do samych nazw składników:
class lepszy_ punkt: public punkt {
public:
char opis[10]
protected:
punkt::przesun(float,float);
//
konstruktor
punkt::punkt(….);
public:
lepszy_ punkt(float=0, float=0, char*=NULL);
//
void wypisz();
//
dodana funkcja
};
Ograniczenia mechanizmu
dziedziczenia
Nie dziedziczy się automatycznie konstruktorów,
destruktorów i operacji przypisania zdefiniowanych w
klasie podstawowej. Jeśli chcemy wykorzystać
konstruktor z klasy nadrzędnej musimy to wyraźnie
wskazać ten zamiar poprzez operator zakresu.
Obiektem klasy pochodnej powinien być (bo taki jest cel
dziedziczenia) wzbogacony o „coś jeszcze” obiekt klasy
podstawowej.
Dlatego
gdyby
konstruktor
był
dziedziczony
to
przeniesienie
dotyczyłoby
tylko
składników w nim zawartych, a więc składników klasy
podstawowej. Toteż dziedziczenie konstruktorów jest
sprzeczne
z
podstawowym
celem
mechanizmu
dziedziczenia. Ponieważ destruktor jest powiązany z
konstruktorem to z tego samego powodu nie może być
automatycznie dziedziczony. W konstruktorze obiektów
klasy pochodnej musimy więc uruchomić dodatkowo
mechanizm dziedziczenia aby przekazać mu metody i
zmienne obiektów z klasy podstawowej.
Dlaczego nie jest dziedziczony skutek działania operatora
przypisania? Dziedziczenie powoduje, że każdy obiekt
klasy pochodnej ma faktycznie dwuczęściową budowę.
Jedna część to składniki odziedziczone, a druga to
zdefiniowane lub dodane w klasie pochodnej. Operacje
przypisania także rozdzieliłyby się na dwie części. W
takiej sytuacji przypisanie, które oznacza posługiwanie
się w operacjach wytworzona do tego celu kopią
wielkości przypisywanej musi jeszcze rozróżnić, gdzie
jest zakres klasy podstawowej, a gdzie zakres klasy
pochodnej przy tworzeniu przypisywanych kopii. To
mogłoby wywołać spore zamieszanie w gospodarowaniu
pamięcią operacyjną.
Odwołanie się do zmiennej przez wartość w
przypadku dziedziczenia jest praktycznie
niewykonalne.
Skoro przypisanie nie jest dziedziczone to jak się
realizuje w klasie pochodnej? Możemy wyróżnić dwie
odmienne sytuacje.
Pierwsza: klasa pochodna nie definiuje swoich operacji
przypisania. Wtedy kompilator tam, gdzie to wynika z
dziedziczenia automatycznie wygeneruje przypisanie
poprzez
kopiowanie
adresów
w
zakresie
klasy
podstawowej:
klasa&klasa::operator=(klasa&)
Mamy tu mechanizm przeciążenia operatora. Nie obejmie
to składników typu const oraz referencji, bo do nich nic
nie możemy przypisywać.
Taki zapis „wewnątrz” kompilatora działa składnik po
składniku. Podobnie zostanie przeniesiony konstruktor
kopiujący jeśli nie oznaczony etykietą private.
Druga: klasa pochodna definiuje swój operator
przypisania oraz swój konstruktor kopiujący. Wówczas w
klasie pochodnej definiujemy operacje przypisania lub
konstruktora kopiującego w zwykły sposób.
Dziedziczenie kilkupokoleniowe
Klasa pochodna może być klasą podstawową dla
kolejnej klasy pochodnej. Nazywa się ją klasą
podstawową pośrednią, a pierwszą, wyjściową
klasę podstawową nazywa się w takiej sytuacji
przodkiem. Przy takim wielopokoleniowym
dziedziczeniu widoczny jest silny i sterujący
dostępem do danych czyli składników klas
wpływ etykiet public, protected i private.
Dziedziczenie wielokrotne i ryzyko
wieloznaczności
Klasa może wywodzić się od więcej niż jednej klasy-przodka. Takie
dziedziczenie nazywamy wielokrotnym. Klas dziedziczących od tego
samego przodka także może być wiele. Pozwala to na powiązanie
niezależnych od siebie klas. Etykiety dostępu działają tu tak samo
jak w dziedziczeniu jednokrotnym tj. przy jednej klasie podstawowej.
Przykład:
class pojazd {
////...
}
class jacht {
///…}
class
amfibia
:
public
pojazd,
public
jacht
{
///...
}
Klasa amfibia dziedziczy jednocześnie z klasy pojazd i z
klasy jacht. Powstaje w takim przypadku ryzyko
niejednoznaczności. Niech w każdej z klas jacht i pojazd
będzie zadeklarowana zmienna tego samego typu i o
takiej samej nazwie, np.:
int silnik;
Klasa amfibia odziedziczy każdą z tych zmiennych. Co
będzie kiedy w jednej z funkcji składowych klasy amfibia
odwołamy się do zmiennej silnik? Kompilator stwierdzi
niejednoznaczność adresu zmiennej i zgłosi błąd. Aby
takiego błędu uniknąć odnosimy się do zmiennej silnik
poprzez operator zakresu odpowiednio do zamierzeń:
pojazd::silnik
Albo:
jacht::silnik
Pamiętajmy, że przy dziedziczeniu wielokrotnym kompilator najpierw
sprawdza jednoznaczność, a potem dostęp do zmiennej.
Operator zakresu nie jest rozwiązaniem uniwersalnym, bo klasy
pochodne względem klasy amfibia będą miały taki sam problem
wieloznacznego silnik.
Skuteczniej jest posłużyć się definicją zmiennej o takiej samej nazwie
wewnątrz klasy amfibia. Wykonamy w ten sposób przesłanianie
zmiennej silnik z każdej z klas podstawowych i w klasach
pochodnych względem klasy amfibia problem wieloznaczności
zostanie ucięty.
Przykład:
int amfibia::silnik() //
funkcja typu void przejmuje role zmiennej
silnik
{
///
return pojazd::silnik;
}
Teraz zawsze zmienna silnik będzie wybierana jednoznacznie z klasy
samochód, a kompilator traktuje ją jako przesłoniętą.
Konwersje czyli dostęp do
obiektów
Obiekt jest zmienną czyli może być argumentem funkcji
w programie. Jeśli chcemy do funkcji przesłać obiekt
przez wartość i dotyczy to raz obiektu klasy
podstawowej, a drugi raz obiektu klasy pochodnej to
obiekt klasy pochodnej nie da się przesłać.
Kompilator potraktuje to jako błąd.
Jednak obiekty klas pochodnych mogą być traktowane
tak, jak obiekty klas podstawowych wtedy, kiedy
pracujemy na ich adresach. Czyli możliwe będzie
przesyłanie obiektu z klasy pochodnej przez referencje
albo wskaźnik.
Mechanizm dostępu do obiektu klasy pochodnej
poprzez wskaźnik do obiektu klasy podstawowej
nazywa się konwersją standardową. Ta sama nazwa i
mechanizm dotyczy dostępu poprzez referencję.
Czyli jeśli mamy funkcję, która przyjmuje referencję do
obiektu klasy podstawowej, to można ją wywołać także
dla obiektu klasy pochodnej.
Przykład:
class samochod {
public:
int zbiornik;
};
class VW: public samochod {
///
};
void stacja_benzynowa(samochod
& klient
)
{
klient.zbiornik = 50;
}
/////////////////
main()
{
samochod pewien_samochod; //
obiekt klasy samochod
stacja_benzynowa(pewien_samochod); //funkcja
VW golf;
stacja_benzynowa(golf); //
przyjęta została referencja do obiektu
golf klasy VW
///
}
Podczas przyjmowania referencji do obiektu
klasy pochodnej zostaje wykonana niejawna
konwersja standardowa taka: referencja do
obiektu klasy pochodnej VW zostanie zamieniona
na referencje do obiektu klasy samochód, czyli
tak, jak gdyby zapis zawierał:
stacja_benzynowa((samochod&)golf);
Tak samo to zadziała kiedy zamiast referencji
użyjemy wskaźnika.
Zadziała to tylko przy
dziedziczeniu publicznym!!!
Co daje konwersja standardowa?
W klasycznym języku C funkcja mogła być wywołana z
argumentem będącym wskaźnikiem do jakiegoś obiektu:
void narysuj (struct plansza *wskaz);
Jeżeli zastosowaliśmy funkcje narysuj do innej struktury
np. nazwanej menu, to musieliśmy ponownie wywołać
całą funkcję. W C++ zdefiniujemy menu jako klasę
pochodną klasy plansza i funkcję narysuj stosować
będziemy poprzez konwersje do dowolnej z klas.
Inicjowanie konstruktora
przy dziedziczeniu
Podczas tworzenia obiektu kompilator zapewnia wywołanie
konstruktorów dla wszystkich jego obiektów podrzędnych.
Dobrze to działa, kiedy konstruktory są domyślne. Co
jednak, kiedy niektóre konstruktory nie są domyślne tylko
jawne, a ponadto inicjalizują niektóre zmienne obiektów?
Jest to problem trudny, bo konstruktor nie ma dostępu do
składników prywatnych klas podrzędnych i nie może ich
bezpośrednio inicjalizować. Trzeba wówczas wywołać
konstruktor dla klasy podrzędnej. Jeśli na przykład klasa
MojaLodowka korzysta z klasy mojBarek, to omawiana
konstrukcja w C++ ma postać:
MojaLodowka::MojaLodowka(int i): mojBarek(i) {//…
Oczywiście jeśli klasa mojBarek posiada konstruktor ,
pobierający pojedynczy typ integer.
Kompozycja i łączenie z
dziedziczeniem
• Związki między klasami: „jest” i
„zawiera”
pojazd
Pojazd
silnikow
y
rower
Wóz konny
silnik
zawiera
samochód
motocykl
Pojazd silnikowy to szczególny rodzaj (podgrupa) pojazdu
Motocykl to szczególny rodzaj (podgrupa) pojazdów silnikowych
Kompozycja a
dziedziczenie
• Kompozycje stosuje się wtedy, gdy
między klasami zachodzi relacja typu
• „całość -> cześć” tzn. nowa klasa
zawiera w sobie istniejąca klasę.
• Dziedziczenie stosuje się wtedy, gdy
miedzy klasami zachodzi relacja
• „generalizacja -> specjalizacja” tzn.
nowa klasa jest szczególnym rodzajem
juz istniejącej klasy.
Kompozycja
• Kompozycje uzyskujemy poprzez definiowanie w
nowej klasie pól, które są obiektami istniejących
klas.
Przykład:
Klasa Osoba zawiera:
pola nazwisko i imie, które należą do klasy String.
Klasa Ksiazka zawiera:
pole autor należące do klasy osoba,
pole tytul należące do klasy String,
pole cena typu double.
Kompozycja cd. Klasa
Osoba
• class Osoba
• { private String nazwisko;
• private String imie;
• public Osoba(String nazwisko, String imie)
• { this.nazwisko = nazwisko;
• this.imie = imie;
• }
• public String podajNazwisko()
• { return nazwisko;
• }
• public String podajImie()
• { return imie;
• }
• }
Kompozycja cd. Klasa
Książka
•
class Ksiazka
•
{ private Osoba autor;
•
private String tytul;
•
double cena;
•
public Ksiazka(Osoba autor, String tytul, double cena)
•
{ this.autor = autor;
•
this.tytul = tytul;
•
this.cena = cena;
•
}
•
public Osoba podajAutor()
•
{ return autor;
•
}
•
public String podajTytul()
•
{ return tytul;
•
}
•
public double podajCena()
•
{ return cena;
•
}
•
}
Kompozycja cd.
• Czyli podczas kompozycji osadzamy obiekty
prywatne jednej klasy w innej klasie.
• Może być stosowana obok dziedziczenia
zgodnie z zasadami odróżniającymi te dwie
formy.
• Dziedziczenie wykorzystuje niejawne
rzutowanie typu w górę (w dół nie jest
bezpieczne).
• Dzięki nim uzyskujemy możliwość
programowania przyrostowego.
Funkcje wirtualne i polimorfizm
Wykład 7
Polimorfizm i funkcje
wirtualne
• Kapsułkowanie (hermatyzacja) oddziela interfejs
od implementacji czyniąc szczegóły prywatnymi.
• Dziedziczenie pozwala traktować obiekt tak, jak
by był typu swojego albo typu podstawowego,
czyli umożliwia schowanie wielu typów danych
pod jednym.
• Funkcje wirtualne pozwalają typowi danych na
odróżnienie swojej odrębności od innego,
podobnego, pod warunkiem, że obydwa
wyprowadzają się od tego samego typu
podstawowego
Wiązanie wywołania
funkcji
• Połączenie wywołania funkcji z jej ciałem (binding)
może by dokonane przez kompilator przed
uruchomieniem programu. Jest to tzw. wiązanie
wczesne znane z programowania proceduralnego.
• Wiązanie wykonywane w trakcie realizacji programu na
podstawie typu obiektu jest wiązaniem późnym i jest
charakterystyczne dla programowania obiektowego.
Wiązanie ma mechanizmy zależne od języka. W języku
C++ wiązanie jest zapewnione z jednoczesnym
rzutowaniem w dół za pomocą słowa kluczowego
virtual.
Mechanizm omawiany w ramach tematu funkcje
wirtualne decyduje o jednej z przewag programowania
obiektowego nad strukturalnym. Rozważmy dziedziczenie
w ramach klas (Grębosz ale wczesniej Eckel w Thinking
in Java):
Instrument: trąbka, bęben, fortepian
Przykład: plik nagłówkowy zawierający klasę z
wirtualną funkcją składową.
#include <iostream>
class instrument {
public:
void
virtual
wydaj_dzwiek()
{
cout<<”nieokreslony pisk!\n”;
}
};
//
class trabka: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”tra-ta-ta-ta\n”;
}
};
class beben: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”bum-bum-bum\n”;
}
};
class fortepian: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”plim-plim-plim\n”;
}
};
void muzyk(instrument &instrument);
main()
{
instrument jakis_instrument;
trabka zlota_trabka;
fortepian steinway;
beben beben_dobosza;
cout<<“wywolanie funkcji skladowych na rzecz
obiektow\n“;
}
jakis_instrument.wydaj_dzwiek();
zlota_trabka.wydaj_dzwiek();
steinway.wydaj_dzwiek();
beben_dobosza.wydaj_dzwiek();
///// powinien zadzialac mechanizm przeslaniania
cout<<”wywolanie funkcji na rzecz obiektu\n
pokazanego wskaznikiem instrumentu\n”;
instrument *wskinstr;
//deklaracja wskaznika
//ustawianie wskaznika
wskinstr=&jakis_instrument;
//przypisanie pod wskaźnik
referencji na obiekt
wskinstr-> wydaj_dzwiek(); // wywołanie funkcji na rzecz zawartosci
wyłuskiwanej spod wskaźnika
cout<<”okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej”;
wskinstr=& zlota_trabka;
wskinstr-> wydaj_dzwiek();
wskinstr=& steinway;
wskinstr-> wydaj_dzwiek();
wskinstr=& beben_dobosza;
wskinstr-> wydaj_dzwiek();
cout<<”albo na referencje do funkcji”;
muzyk(jakis_instrument);
//obiekt jest tu abstarkcyjna zmienna
muzyk(zlota_trabka);
muzyk(steinway);
muzyk(beben_dobosza);
}
/////
void muzyk(instrument &pysk);
{
pysk.wydaj_dźwięk();
}
Po uruchomieniu programu trzymamy na ekranie:
wywolanie funkcji skladowych na rzecz obiektow
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
wywolanie funkcji na rzecz obiektu
pokazanego wskaznikiem instrumentu
nieokreslony pisk!
okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
albo na referencje do funkcji
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
Gdyby jednak usunąć słowo virtual przy funkcji
wydaj_dzwiek w klasie podstawowej, to na ekranie pojawi
się następujący wynik:
wywolanie funkcji skladowych na rzecz obiektow
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
wywolanie funkcji na rzecz obiektu
pokazanego wskaznikiem instrumentu
nieokreslony pisk!
okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
albo na referencje do funkcji
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
Czyli po wywołaniu funkcji wydaj_dźwięk na rzecz
obiektów z poszczególnych klas wykonała się po prostu
funkcja z każdej z tych klas zgodnie z zaleceniem:
obiekt.wydaj_dźwięk();
Tu działał ukryty wskaźnik this oraz mechanizm
przesłaniania.
Dalej wprowadziliśmy definiowany wskaźnik, który
pokazywał na obiekty klasy instrument. Przy tym
pokazuje na jakis_instrument czyli dowolny obiekt klasy
instrument.
Następnie kierujemy wskaźnik na funkcję, co powoduje,
ze jest ona wykonana na rzecz wskazanego wcześniej
obiektu:
wskaźnik->wydaj_dźwięk();
Potem ustawiliśmy wskaźnik na obiekty klas pochodnych.
Mogliśmy to zrobić bo przy dziedziczeniu następuje
konwersja typów obiektu i wskaźnikiem do obiektu
klasy podstawowej możemy pokazać na obiekt klasy
pochodnej.
Wprawdzie typ wskaźnika jest przy dziedziczeniu ogólnie
różny od typu obiektu ale konwersja działa w ramach
mechanizmu dziedziczenia. Dlaczego jednak kompilator
wybiera właściwą obiektowi funkcję mimo takiej samej
nazwy funkcji? Sprawcą takiego zachowania kompilatora
jest słowo virtual przy funkcji składowej klasy
podstawowej.
To
ono
sprawia,
że
konwersja
przekierowuje kompilator inteligentnie także do funkcji
dla obiektu pokazanego wskaźnikiem.
Gdy słowo virtual zostało usunięte to mechanizm
prawidłowego wykonania funkcji przypisanej obiektowi
nie zadziałał i wykonywała się funkcja tylko z klasy
podstawowej.
Kompilatory języków niezorientowanych
obiektowo używają tzw. wczesnego wiązania
funkcji. Kompilator generuje wywołanie funkcji a
linker zamienia to wywołanie na bezwzględny
adres kodu, który ma być wykonany.
Kompilatory w językach obiektowych stosują tzw.
późne wiązanie. Kod przy takim wiązaniu jest
wywoływany dopiero podczas wykonywania.
Kompilator tylko sprawdza poprawność i
obecność poszczególnych składników w
wiązaniu. W języku C++ takie wywołanie
powoduje słowo kluczowe virtual.
Jak realizuje się późne
wiązanie
• Kompilator generuje tablicę wirtualnych wskaźników
VTABLE do każdej klasy zawierającej funkcje wirtualne.
Umieszcza w niej adresy funkcji wirtualnych zawartych w
klasie.
• W każdej klasie zawierającej funkcje wirtualne lokowany
jest wirtualny wskaźnik (virtual pointer) VPTR wskazujący
tablicę VTABLE tej klasy.
• Gdy za pośrednictwem wskaźnika obiektu klasy
podstawowej wywołuje się funkcję wirtualną, kompilator
niejawnie wstawia kod, pobierający wskaźnik VPTR i
odnajdujący adres funkcji w tablicy VTABLE.
• W niektórych językach (Java) wirtualności realizuje się
stale.
Polimorfizm
Dzięki terminowi virtual fragment kodu funkcji muzyk
podany w formie
&wydaj_dźwięk();
wykonuje się w formie stosownej do zakresu klasy, z
której wskazujemy adresem obiekt:
&instrument::wydaj_dźwięk()
&trabka::wydaj_dźwięk()
&fortepian::wydaj_dźwięk()
zależnie od sytuacji. Czyli funkcja muzyk wykonała się
różnie mimo tej samej formy. To się nazywa
polimorfizmem, co oznacza wielość form. Zastosowanie
funkcji wirtualnej pozwoliło na uzyskanie wielości form.
Dodatkową cechą klasy zawierającej składową funkcję
wirtualną jest to, że
zadziała uniwersalnie dla każdej
klasy pochodnej
wywołującej funkcje wydaj_dźwięk():
#include”instrum.h” // nasze defincje do klasy
instrument zawrzemy w pliku head
/////
class sluchacz:public instrument{
public:
void wydaj_dzwiek();
{
cout<<”jazz-jazz”;
}
////
main()
{
sluchacz bzzzzz;
muzyk(bzzzzz);
}
to na ekranie otrzymamy:
jazz-jazz
Dlaczego? Dlatego, że instrukcja z funkcji muzyk ma
teraz formę:
instrument.sluchacz::wydaj_dźwięk();
Nietrudno zauważyć, że daje to zupełnie nowe możliwości
modyfikacji działania programu w ramach polimorfizmu.
Dlaczego w takim razie nie uznać wszystkich funkcji jako
wirtualnych w trybie domyślnym? Głównie dlatego, że
funkcje wirtualne zabierają znacznie więcej miejsca w
pamięci niż zwykłe funkcje składowe i ich uruchamianie
trwa znacząco dłużej.
Należy pamiętać, że:
•wirtualna może być tylko funkcja składowa, a nie
funkcja globalna;
•słowo virtual występuje tylko przy deklaracji funkcji w
klasie, a ciało funkcji już nie musi go zawierać;
•jeśli klasa pochodna nie zdefiniuje swojej wersji funkcji
wirtualnej, to będzie ona wywoływana z klasy
podstawowej w jej zakresie ważności;
•funkcja wirtualna nie może być funkcją typu static bo
wtedy nie może być stosowana wirtualnie na wielu
obiektach a tylko na tym, na którym jest przypisana jako
static;
•funkcja wirtualna może być funkcją zaprzyjaźnioną ale
straci wówczas możliwość polimorficznego działania czyli
możemy ja zaprzyjaźnić ale za ceną utraty polimorfizmu
Abstrakcyjne klasy
podstawowe
• Polimorfizm umożliwia zbudowanie klasy, która jest interfejsem
wszystkich swoich klas pochodnych. Bardzo to ułatwia projektowanie
i tworzy przejrzyste strukturalne diagramy UML. W takiej klasie
tworzymy funkcje (metody obiektów) czysto wirtualne.
• Utworzenie funkcji czysto wirtualnej pozwala na umieszczenie jej
jako funkcji składowej w interfejsie klasy, bez konieczności tworzenia
kodu, stanowiącego ciało tej funkcji. Definicje funkcji dostarczają
klasy pochodne.
• Składnia deklaracji funkcji czysto wirtualnej jest jak poniżej:
virtual void f() = 0;
• Przy takiej deklaracji kompilator zarezerwuje miejsce w VTABLE, ale
nie umieści w nim żadnego konkretnego adresu.
• Nie będzie można utworzy obiektu tej klasy, bo VPTR nie znjadzie
adresu.