ďťż

Blog literacki, portal erotyczny - seks i humor nie z tej ziemi


LEKCJA 35: O ZASTOSOWANIU DZIEDZICZENIA.
________________________________________________________________
Z tej lekcji dowiesz się, do czego w praktyce programowania
szczególnie przydaje się dziedziczenie.
________________________________________________________________

Dzięki dziedziczeniu programista może w pełni wykorzystać gotowe

biblioteki klas, tworząc własne klasy i obiekty, jako klasy
pochodne wazględem "fabrycznych" klas bazowych. Jeśli bazowy
zestw danych i funkcji nie jest adekwatny do potrzeb, można np.
przesłonić, rozbudować, bądź przebudować bazową metodę dzięki
elastyczności C++. Zdecydowana większość standardowych klas
bazowych wyposażana jest w konstruktory. Tworząc klasę pochodną
powinniśmy pamiętać o istnieniu konstruktorów i rozumieć sposoby

przekazywania argumentów obowiązujące konstruktory w przypadku
bardziej złożonej struktury klas bazowych-pochodnych.

PRZEKAZANIE PARAMETRÓW DO WIELU KONSTRUKTORÓW.

Klasy bazowe mogą być wyposażone w kilka wersji konstruktora.
Dopóki nie przekażemy konstruktorowi klasy bazowej żadnych
argumentów - zostanie wywołany (domyślny) pusty konstruktor i
klasa bazowa będzie utworzona z parametrami domyślnymi. Nie
zawsze jest to dla nas najwygodniejsza sytuacja.

Jeżeli wszystkie, bądź choćby niektóre z parametrów, które
przekazujemy konstruktorowi obiektu klasy pochodnej powinny
zostać przekazane także konstruktorowi (konstruktorom) klas
bazowych, powinniśmy wytłumaczyć to C++. Z tego też powodu,
jeśli konstruktor jakiejś klasy ma jeden, bądź więcej
parametrów, to wszystkie klasy pochodne względem tej klasy
bazowej muszą posiadać konstruktory. Dla przykładu dodajmy
konstruktor do naszej klasy pochodnej Cpochodna:


class CBazowa1
{
public:
CBazowa1(...); //Konstruktor
};

class CBazowa2
{
public:
CBazowa2(...); //Konstruktor
};

class Cpochodna : public CBazowa1, CBazowa2 //Lista klas
{
public:
Cpochodna(...); //Konstruktor
};

main()
{
Cpochodna Obiekt(...); //Wywolanie konstruktora
...

W momencie wywołania kostruktora obiektu klasy pochodnej
Cpochodna() przekazujemy kostruktorowi argumenty. Możemy (jeśli
chcemy, nie koniecznie) przekazać te argumenty konstruktorom
"wcześniejszym" - konstruktorom klas bazowych. Ta możliwość
okazuje się bardzo przydatna (niezbędna) w środowisku obiektowym

- np. OWL i TVL. Oto prosty przykład definiowania konstruktora w

przypadku dziedziczenia. Rola konstruktorów będzie polegać na
trywialnej operacji przekazania pojedynczego znaku.

class CBazowa1
{
public:
CBazowa1(char znak) { cout << znak; }
};

class CBazowa2
{
public:
CBazowa2(char znak) { cout << znak; }
};

class Cpochodna : public CBazowa1, CBazowa2
{
public:
Cpochodna(char c1, char c2, char c3);
};

Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2),
CBazowa2(c3)
{
cout << c1;
}

Konstruktor klasy pochodnej pobiera trzy argumenty i dwa z nich:

c2 --> przekazuje do konstruktora klasy CBazowa1
c3 --> przekazuje do konstruktora klasy CBazowa2
Sposób zapisu w C++ wygląda tak:

Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2),
CBazowa2(c3)

Możemy zatem przekazać parametry "w tył" do konstruktorów klas
bazowych w taki sposób:

kl_pochodna::kl_pochodna(lista):baza1(lista), baza2(lista), ...

gdzie:
lista - oznacza listę parametrów odpowiedniego konstruktora.

W takiej sytuacji na liście argumentów konstruktorów klas
bazowych mogą znajdować się także wyrażenia, przy założeniu, że
elementy tych wyrażeń są widoczne i dostępne (np. globalne
stałe, globalne zmienne, dynamicznie inicjowane zmienne globalne

itp.). Konstruktory będą wykonywane w kolejności:

CBazowa1 --> CBazowa2 --> Cpochodna

Dzięki tym mechanizmom możemy łatwo przekazywać argumenty
"wstecz" od konstruktorów klas pochodnych do konstruktorów klas
bazowych.

FUNKCJE WIRTUALNE.

Działanie funkcji wirtualnych przypomina rozbudowę funkcji
dzięki mechanizmowi overloadingu. Jeśli, zdefiniowaliśmy w
klasie bazowej funkcję wirtualną, to w klasie pochodnej możemy
definicję tej funkcji zastąpić nową definicją. Przekonajmy się o

tym na przykładzie. Zacznijmy od zadeklarowania funkcji
wirtualnej (przy pomocy słowa kluczowego virtual) w klasie
bazowej. Zadeklarujemy jako funkcję wirtualną funkcję oddychaj()

w klasie CZwierzak:

class CZwierzak
{
public:
void Jedz();
virtual void Oddychaj();
};

Wyobraźmy sobie, że chcemy zdefiniować klasę pochodną CRybka
Rybki nie oddychają w taki sam sposób, jak inne obiekty klasy
CZwierzak. Funkcję Oddychaj() trzeba zatem będzie napisać w dwu
różnych wariantach. Obiekt Ciapek może tę funkcję odziedziczyć
bez zmian i sapać spokojnie, z Sardynką gorzej:

class CZwierzak
{
public:
void Jedz();
virtual void Oddychaj() { cout << "Sapie..."; }
};

class CPiesek : public CZwierzak
{
char imie[30];
} Ciapek;

class CRybka
char imie[30];
public:
void Oddychaj() { cout << "Nie moge sapac..."; }
} Sardynka;


Zwróć uwagę, że w klasie pochodnej w deklaracji funkcji słowo
kluczowe virtual już nie występuje. W klasie pochodnej funkcja
CRybka::Oddychaj() robi więcej niż w przypadku "zwykłego"
overloadingu funkcji. Funkcja CZwierzak::Oddychaj() zostaje
"przesłonięta" (ang. overwrite), mimo, że ilość i typ
argumentów. pozostaje bez zmian. Taki proces - bardziej
drastyczny, niż overloading nazywany jest przesłanianiem lub
nadpisywaniem funkcji (ang. function overriding). W programie
przykładowym Ciapek będzie oddychał a Sardynka nie.

[P127.CPP]

# include

class CZwierzak
{
public:
void Jedz();
virtual void Oddychaj() {cout << "\nSapie...";}
};

class CPiesek : public CZwierzak
{
char imie[30];
} Ciapek;

class CRybka
char imie[30];
public:
void Oddychaj() {cout << "\nSardynka: A ja nie oddycham.";}
} Sardynka;

void main()
{
Ciapek.Oddychaj();
Sardynka.Oddychaj();
}

Funkcja CZwierzak::Oddychaj() została w obiekcie Sardynka
przesłonięta przez funkcję CRybka::Oddychaj() - nowszą wersję
funkcji-metody pochodzącą z klasy pochodnej.

Overloading funkcji zasadzał się na "typologicznym pedantyźmie"
C++ i na dodatkowych informacjach, które C++ dołącza przy
kompilacji do funkcji, a które dotyczą licznby i typów
argumentów danej wersji funkcji. W przypadku funkcji wirtualnych

jest inaczej. Aby wykonać przesłanianie kolejnych wersji funkcji

wirtualnej w taki sposób, funkcja we wszystkich "pokoleniach"
musi mieć taki sam prototyp, tj. pobierać taką samą liczbę
parametrów tych samych typów oraz zwracać wartość tego samego
typu. Jeśli tak się nie stanie, C++ potraktuje różne prototypy
tej samej funkcji w kolejnych pokoleniach zgodnie z zasadami
overloadingu funkcji. Zwróćmy tu uwagę, że w przypadku funkcji
wirtualnych o wyborze wersji funkcji decyduje to, wobec którego
obiektu (której klasy) funkcja została wywołana. Jeśli wywołamy
funkcję dla obiektu Ciapek, C++ wybierze wersję
CZwierzak::Oddychaj(), natomiast wobec obiektu Sardynka zostanie

zastosowana wersja CRybka::Oddychaj().

W C++ wskaźnik do klasy bazowej może także wskazywać na klasy
pochodne, więc zastosowanie funkcji wirtualnych może dać pewne
ciekawe efekty "uboczne". Jeśli zadeklarujemy wskaźnik *p do
obiektów klasy bazowej CZwierzak *p; a następnie zastosujemy ten

sam wskaźnik do wskazania na obiekt klasy pochodnej:

p = &Ciapek; p->Oddychaj();
...
p = &Sardynka; p->Oddychaj();

zarządamy w taki sposób od C++ rozpoznania właściwej wersji
wirtualnej metody Oddychaj() i jej wywołania we właściwym
momencie. C++ może rozpoznać, którą wersję funkcji należałoby
zastosować tylko na podstawie typu obiektu, wobec którego
funkcja została wywołana. I tu pojawia się pewien problem.
Kompilator wykonując kompilcję programu nie wie, co będzie
wskazywał pointer. Ustawienie pointera na konkretny adres
nastąpi dopiero w czasie wykonania programu (run-time).
Kompilator "wie" zatem tylko tyle:

p->Oddychaj()(); //która wersja Oddychaj() ???

Aby mieć pewność, co w tym momencie będzie wskazywał pointer,
kompilator musiałby wiedzieć w jaki sposób będzie przebiegać
wykonanie programu. Takie wyrażenie może zostać wykonane "w
ruchu programu" dwojako: raz, gdy pointer będzie wskazywał
Ciapka (inaczej), a drugi raz - Sardynkę (inaczej):

CZwierzak *p;
...
for(p = &Ciapek, int i = 0; i < 2; i++)
{
p->Oddychaj();
p = &Sardynka;
}

lub inaczej:

if(p == &Ciapek) CZwierzak::Oddychaj();
else CRybka::Oddychaj();

Taki efekt nazywa się polimorfizmem uruchomieniowym (ang.
run-time polymorphism).

Overloading funkcji i operatorów daje efekt tzw. polimorfizmu
kompilacji (ang. compile-time), to funkcje wirtualne dają efekt
polimorfizmu uruchomieniowego (run-time). Ponieważ wszystkie
wersje funkcji wirtualnej mają taki sam prototyp, nie ma innej
metody stwierdzenia, którą wersję funkcji należy zastosować.
Wybór właściwej wersji funkcji może być dokonany tylko na
podstawie typu obiektu, do którego należy wersja funkcji-metody.

Różnica pomiędzy polimorfizmem przejawiającym się na etapie
kompilacji i poliformizmem przejawiającym się na etapie
uruchomienia programu jest nazywana również wszesnym albo póżnym

polimorfizmem (ang. early/late binding). W przypadku wystąpienia

wczesnego polimorfizmu (compile-time, early binding) C++ wybiera

wersję funkcji (poddanej overloadingowi) do zastosowania już
tworząc plik .OBJ. W przypadku późnego polimorfizmu (run-time,
late binding) C++ wybiera wersję funkcji (poddanej przesłanianiu

- overriding) do zastosowania po sprawdzeniu bieżącego kontekstu

i zgodnie z bieżącym wskazaniem pointera.

Przyjrzyjmy się dokładniej zastosowaniu wskaźników do obiektów w

przykładowym programie. Utworzymy hierarchię złożoną z klasy
bazowej i pochodnej w taki sposób, by klasa pochodna zawierała
jakiś unikalny element - np. nie występującą w klasie bazowej
funkcję.

class CZwierzak
{
public:
void Jedz();
virtual void Oddychaj() {cout << "\nSapie...";}
};

class CPiesek : public CZwierzak
{
char imie[20];
void Szczekaj() { cout << "Szczekam !!!"; }
} Ciapek;

Jeśli teraz zadeklarujemy wskaźnik do obiektów klasy bazowej:

CZwierzak *p;

to przy pomocy tego wskaźnika możemy odwołać się także do
obiektów klasy pochodnej oraz do elementów obiektu klasy
pochodnej - np. do funkcji p->Oddychaj(). Ale pojawia się tu
pewien problem. Jeśli zechcelibyśmy wskazać przy pomocy pointera

taki element klasy pochodnej, który nie został odziedziczony i
którego nie ma w klasie bazowej? Rozwiązanie jest proste -
wystarczy zarządać od C++, by chwilowo zmienił typ wskaźnika z
obiektów klasy bazowej na obiekty klasy pochodnej. W przypadku
funkcji Szczekaj() w naszym programie wyglądałoby to tak:

CZwierzak *p;
...
p->Oddychaj();
p->Szczekaj(); //ŹLE !
(CPiesek*)p->Szczekaj(); //Poprawnie
...

Dzięki funkcjom wirtualnym tworząc klasy bazowe pozwalamy
późniejszym użytkownikom na rozbudowę funkcji-metod w
najwłaściwszy ich zdaniem sposób. Dzięki tej "nieokreśloności"
dziedzicząc możemy przejmować z klasy bazowej tylko to, co nam
odpowiada. Funkcje w C++ mogą być jeszcze bardziej
"nieokreślone" i rozbudowywalne. Nazywają się wtedy funkcjami w
pełni wirtualnymi.
  • zanotowane.pl
  • doc.pisz.pl
  • pdf.pisz.pl
  • qualintaka.pev.pl
  •