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 

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 

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