wyklad5 dziedziczenie kompozycja polimorfizm

background image

Dziedziczenie

Wykład 6

background image

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

};

background image

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

 

};

background image

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.

background image

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

background image

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.

background image

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

};

background image

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.

background image

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.

background image

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.

background image

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.

background image

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

{
///...
}

background image

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

background image

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ą.

background image

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.

background image

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

///
}

background image

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!!!

background image

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.

background image

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.

background image

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

background image

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.

background image

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.

background image

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;

}

}

background image

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;

}

}

background image

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.

background image

Funkcje wirtualne i polimorfizm

Wykład 7

background image

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

background image

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.

background image

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”;
}
};
//

background image

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);

background image

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

 

background image

//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();
 

background image

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();
}

background image

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!
 

background image

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
 

background image

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!
 

background image

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!

background image

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.

background image

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.

background image

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.

background image

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.

background image

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.

background image

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);
}

background image

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.

background image

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

background image

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.


Document Outline


Wyszukiwarka

Podobne podstrony:
wyklad5 dziedziczenie kompozycja polimorfizm
Wyklad Jezyk C# Dziedziczenie Wyklad4 DziedziczeniePolimorfizm
wykład12 dziedzicz pozajądrowe
Programowanie obiektowe, wyklad5, Dziedziczenie
Dziedziczenie a kompozycja
Programowanie obiektowe, wyklad6-czesc1, Dziedziczenie wielobazowe
gospodarka wykład III, Zanieczyszczenia środowiska jest to dziedzina ochrony środowiska, którą zaczę
Wykłady z genetyki (Choroby, dziedziczność) by Kusy
wykład 5 Kompozyty
MEL polimery i kompozyty prof wykład
15 Enkapsulacja, dziedzicznie i polimorfizm w programowaniu obiektowym
wyklad 7-genetyka czlowieka, far, genetyka, dziedziczenie związane z płcią 7 ćwiczenie
Wykład 6 Kompozyty (2012)
wykład 1-3, administracja, Reszta, STARE, Elementy zarządzania dziedzictwem kulturowym, dziedzictwo
aa kliniczna wyklady, KLINICZ1, Psychologią kliniczna będziemy się zajmowali dlatego, że z niej wyro

więcej podobnych podstron