POLIMORFIZAM Objektno orijentirano programiranje prof. dr. prof. dr. sc. Ivo Mateljan M ateljan doc. dr. sc. Marjan Sikora
SADRŽAJ •
polimorfizam
•
virtualne članske funkcije
•
apstraktne klase
•
primjer apstraktnog sintaktičkog stabla za proračun aritmetičkih izraza
2/41
POLIMORFIZAM •
•
•
•
polimorfizam je svojstvo promjenljivosti oblika u polimorfnom programu se algoritmi, odnosno funkcije mogu primijeniti na objekte različitih klasa do sada smo obradili jedan mehanizam meh anizam koji omogućava omogućava polimorfizam - predloške
drugi mehanizam u C++ jeziku je mehanizam virtualnih funkcija
3/41
POLIMORFIZAM – DVIJE VRSTE •
•
•
•
•
prilagođavanje objektu se može dijelom izvršiti već prilikom kompajliranja programa to se naziva statičko povezivanje s objektom – eng. static binding osim toga povezivanje se može vršiti i tijekom izvršenja programa takvo povezivanje s objektom se radi pomoću pokazivača i virtual virtualnih nih fun funkc kcija ija
to se naziva izvršno povezivanje – eng. run-time binding
4/41
PRIMJER “IS A” ODNOSA OBJEKATA •
klasa Trokut nasljeđuje klasu Poligon
•
ovo je primjer “is-a” odnosa
#include
using namespace std;
return (sirina * visina / 2); } };
class Poligon { protected: int sirina, visina; public: void init (int a, int b) { sirina=a; visina=b; } int Sirina() {return sirina;} int Visina() {return visina;} }; class Pravokutnik: public Poligon { public: int Povrsina (void) { return (sirina * visina); } }; class Trokut: public Poligon { public: int Povrsina (void) {
void IspisDimenzija(Poligon& p) { cout << "sirina = " << p.Sirina() << endl; cout << "visina = " << p.Visina() << endl; } int main () { Pravokutnik pr; Trokut tr; pr.init (4,5); tr.init (4,5); cout << "Pravokutnik:" << endl; IspisDimenzija(pr); cout << "povrsina =" << pr.Povrsina() << endl << endl; cout << "Trokut:" << endl; IspisDimenzija(tr); cout << "povrsina =" << tr.Povrsina() << endl; return 0; }
5/41
“IS A” ODNOS OBJEKATA •
“is-a” svojstvo ima slijedeću posljedicu: –
•
•
•
pokazivači i reference koji se deklariraju pomoću temeljne klase, mogu se koristiti i za manipuliranje s objektima izvedenih klasa
funkcija IspisDimenzija() je definirana s parametrom koji označava referencu objekta temeljne klase Poligon dakle, objektu klase Trokut može se pristupati kao da se radi o objektu klase Poligon pri pozivu ove funkcije kao argument prosljeđuje se objekt izvedene klase 6/41
IMPLICITNE PRETVORBE •
četiri standardne pretvorbe su moguće između objekata izvedene i temeljne javne klase: 1. objekt izvedene klase može se implicitno pretvoriti u objekt javne temeljne klase 2. referenca na objekt izvedene klase može se implicitno pretvoriti u referencu objekta javne temeljne klase 3. pokazivač na objekt izvedene klase može se implicitno pretvoriti u pokazivač objekta javne temeljne klase 4. pokazivač na člana objekta izvedene klase može se implicitno pretvoriti u pokazivač člana objekta javne temeljne klase
7/41
PRETVORBE #include
};
using namespace std; void IspisDimenzija(Poligon& p) { class Poligon {
cout << "sirina = " << p.Sirina() << endl;
protected:
cout << "visina = " << p.Visina() << endl;
int sirina, visina;
}
public: void init (int a, int b) {
int main () {
sirina=a; visina=b;
Pravokutnik pravokutnik;
}
Trokut trokut;
int Sirina() {return sirina;}
Poligon* pPol1 = &pravokutnik;
int Visina() {return visina;}
Poligon* pPol2 = &trokut;
};
pPol1->init (4,5); pPol2->init (4,5);
class Pravokutnik: public Poligon { public: int Povrsina (void) {
//cout << pPol1->Povrsina() << endl; //cout << pPol2->Povrsina() << endl;
return (sirina * visina); }
cout << pravokutnik.Povrsina() << endl;
};
cout << trokut.Povrsina() << endl;
class Trokut: public Poligon {
return 0;
public:
}
int Povrsina (void) { return (sirina * visina / 2); }
8/41
VIRTUALNE ČLANSKE FUNKCIJE •
u oba prethodna primjera samo je djelomično iskazan princip polimorfizma
•
korišten je samo za pristup članskoj funkciji init
•
ona je definirana u temeljnoj i izvedenim klasama
•
•
da bi princip polimorfizma mogli potpuno koristiti potreban je mehanizam kojim bi omogućio i poziv funkcije Povrsina to nije bilo moguće ostvariti jer ta funkcija nije definirana u temeljnoj klasi Poligon
9/41
VIRTUALNE ČLANSKE FUNKCIJE •
•
•
•
•
pitanje je kako omogućiti i poziv funkcije Povrsina
to se može napraviti tako da se ta funkcija virtualno definira u temeljnoj klasi virtualna funkcija je način „rezerviranja mjesta” on se vrši u temeljnoj klasi za pravu funkciju koja će biti definirana u izvedenoj klasi
u temeljnoj klasi se deklarira funkcija Povrsina s prefiksom
virtual
10/41
VIRTUALNE ČLANSKE FUNKCIJE #include
return (sirina * visina / 2);
using namespace std;
} };
class Poligon { protected:
void IspisDimenzija(Poligon& p) {
int sirina, visina;
cout << "sirina = " << p.Sirina() << endl;
public: void init (int a, int b) {
cout << "visina = " << p.Visina() << endl; }
sirina=a; visina=b; }
int main () {
int Sirina() {return sirina;}
Pravokutnik pravokutnik;
int Visina() {return visina;}
Trokut trokut;
virtual int Povrsina (void) { return (0); }
Poligon* pPol1 = &pravokutnik;
};
Poligon* pPol2 = &trokut; pPol1->init (4,5);
class Pravokutnik: public Poligon {
pPol2->init (4,5);
public:
cout << pPol1->Povrsina() << endl;
int Povrsina (void) {
cout << pPol2->Povrsina() << endl;
return (sirina * visina); } };
return 0; }
class Trokut: public Poligon { public: int Povrsina (void) { 11/41
POZIV VIRTUALNIH FUNKCIJA •
•
•
•
vidimo da je sada pomoću pokazivača moguće pristupiti svim funkcijama neke klase pitanje: na koji način je prepoznato koja funkcija treba biti pozvana?
poziv virtualnih funkcija vrši se drukčije nego poziv regularnih članskih funkcija
kada se u temeljnoj klasi neka funkcija označi kao virtualna tada se i sve funkcije istog imena u izvedenim klasama tretiraju kao virtualne funkcije
12/41
V-TABLICA I VPTR •
•
za svaki objekt koji ima virtualne funkcije kompajler generira posebnu tablicu (V-tablicu) u koju upisuje adresu virtualnih funkcija uz članove klase zapisuje i pokazivač na ovu tablicu - vptr
13/41
POZIV VIRTUALNE FUNKCIJE •
poziv virtualne funkcije se vrši na slijedeći način: 1. adresa nekog objekta pridijeljena je nekom pokazivaču (ili je referenca) 2. najprije se dobavlja adresa tablice pokazivača virtualnih funkcija - zapisana je u pokazivaču vptr
3. zatim se iz tablice dobavlja adresa te funkcije 4. konačno se indirekcijom tog pokazivača vrši poziv funkcije
14/41
PREDNOSTI I MANE •
•
•
izvršenje programa s virtualnim funkcijama je sporije nego izvršenje programa s regularnim funkcijama
vrijeme se gubi vrijeme na dobavu adrese virtualne funkcije bez obzira na ovu činjenicu rad s virtualnim funkcijama je od velike koristi
•
pomoću njih se postiže potpuni polimorfizam
•
to je najvažniji element objektno orijentiranog programiranja
15/41
ČISTA VIRTUALNA FUNKCIJA •
•
•
•
•
apstraktne temeljne klase su klase u kojima je definirana bar jedna čista virtualna funkcija sintaksa čiste virtualne funkcije: virtual deklaracija_funkcije = 0;
klase koje sadrže čiste virtualne funkcije ne mogu se koristiti za deklaraciju objekata pomoću njih se može deklarirati pokazivač na objekte
u svim klasama koje se izvode iz apstraktne temeljne klase mora obavezno biti implementirana ta funkcija
16/41
APSTRAKTNE TEMELJNE KLASE •
klasa Poligon se može tretirati kao apstraktna temeljna klasa, jer nije predviđeno da se njome deklarira statičke objekte
class Poligon { protected: int sirina, visina; public: void init(int a, int b) { sirina = a; visina = b; } int Sirina() { return sirina; } int Visina() { return visina; } virtual int Povrsina(void) = 0; }; 17/41
PRIMJER PRISTUPA VIRTUALNIM ČLANSKIM FUNKCIJAMA #include
}
using namespace std; }; class Poligon { protected:
class Trokut : public Poligon {
int sirina, visina;
public:
public:
int Povrsina(void) {
void init(int a, int b){
return (sirina * visina / 2);
sirina = a; visina = b;
}
}
};
int Sirina() { return sirina;
int main() {
}
Pravokutnik pravokutnik;
int Visina() {
Trokut trokut;
return visina; }
Poligon* pPol1 = &pravokutnik;
virtual int Povrsina(void) = 0;
Poligon* pPol2 = &trokut;
void PrintPovrsina( void) { cout << this->Povrsina() << endl;
pPol1->init(4, 5);
}
pPol2->init(4, 5);
};
pPol1->PrintPovrsina(); pPol2->PrintPovrsina();
class Pravokutnik : public Poligon { public:
return 0;
int Povrsina(void) {
}
return (sirina * visina);
18/41
PRISTUP VIRTUALNIM FUNKCIJAMA •
•
•
•
pitanje: kako pozvati virtualnu funkciju iz druge članske funkcije temeljne klase
u funkciji temeljne klase se pristup virtualnoj funkciji vrši pomoću this pokazivača to osigurava da će biti pozvana funkcija Povrsina koja pripada aktivnom objektu razlog je što this pokazivač uvijek pokazuje na aktivni objekt
19/41
PRIMJENA APSTRAKTNIH KLASA •
pomoću apstraktnih klasa možemo u niz pohraniti objekte
različitih klasa •
•
to nam omogućava da u petlji prolazimo kroz kolekcije različitih objekata što do sada nije bilo moguće osim niza to možemo napraviti i za generičke spremnike poput vektora
20/41
APSTRAKTNE KLASE I POLIMORFIZAM #include
}
#include
};
using namespace std;
class Trokut : public Poligon { public:
class Poligon {
Trokut(int a, int b) { init(a, b); };
protected:
int Povrsina(void) {
int sirina, visina;
return (sirina * visina / 2);
public:
}
void init(int a, int b){
};
sirina = a; visina = b; }
int main() {
int Sirina() {
vector vec;
return sirina; }
vec.push_back( new Trokut(4, 5));
int Visina() {
vec.push_back( new Pravokutnik(4, 5));
return visina;
vec.push_back( new Trokut(4, 5));
}
vec.push_back( new Trokut(4, 5));
virtual int Povrsina(void) = 0;
vec.push_back( new Pravokutnik(4, 5));
void PrintPovrsina( void) { cout << this->Povrsina() << endl;
for (auto i : vec) {
}
i->PrintPovrsina();
};
}
class Pravokutnik : public Poligon {
return 0;
public:
}
Pravokutnik( int a, int b) { init(a, b); }; int Povrsina(void) { return (sirina * visina);
21/41
PROBLEM S DESTRUKTOROM •
polimorfni program možemo napisati i u sljedećem obliku :
int main() { Poligon * pPol = new Pravokutnik; pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; // dealociraj memoriju Pravokutnika pPol = new Trokut; // ponovo koristi pokazivač pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; return 0; } 22/41
PROBLEM S DESTRUKTOROM •
•
•
•
u prvoj liniji se alocira memorija za objekt klase Pravokutnik tom se objektu dalje pristupa pomoću pokazivača na temeljni objekt klase Poligon kada se obave radnje s ovim objektom oslobađa se zauzeta memorija zatim se isti pokazivač koristi za rad s objektom klase Trokut
int main() { Poligon * pPol = new Pravokutnik; pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; // dealociraj memoriju Pravokutnika pPol = new Trokut; // ponovo koristi pokazivač pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; return 0; } 23/41
PROBLEM S DESTRUKTOROM •
na prvi pogled sve izgleda uredu i većina kompajlera će izvršiti ovaj program bez greške
•
ipak, u njemu krije jedna ozbiljna greška
•
greška je to da se neće se izvršiti poziv destruktora
•
•
što je razlog tome?
da bi odgovorili na ovo pitanje prisjetimo se što se događa kad se pozove operator delete: –
najprije se poziva destruktor objekta
–
zatim se pozivaju destruktori svih klasa od kojih on
nasljeđuje –
na kraju se vrši dealociranje memorije
24/41
PROBLEM S DESTRUKTOROM int main() { Poligon* pPol = new Pravokutnik; pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; // dealociraj memoriju Pravokutnika pPol = new Trokut; // ponovo koristi pokazivač pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; return 0; }
•
•
u promatranom programu pokazivač pPol je deklariran kao pokazivač na temeljni objekt tipa Poligon zato se neće izvršiti poziv destruktora Pravokutnika i
Trokuta 25/41
VIRTUALNI DESTRUKTOR int main() { Poligon * pPol = new Pravokutnik; pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; // dealociraj memoriju Pravokutnika pPol = new Trokut; // ponovo koristi pokazivač pPol->init(4, 5); pPol->PrintPovrsina(); delete pPol; return 0; }
• •
•
na sreću u ovom programu to nema neke posljedice razlog je što u klasama Pravokutnik i Trokut destruktor nema nikakav učinak da bi se omogućio poziv destruktora izvedenih klasa pomoću pokazivača na temeljnu klasu potrebno je da se destruktor temeljne klase deklarira kao virtualna funkcija 26/41
VIRTUALNI DESTRUKTOR #include using namespace std; class Superclass { public: Superclass () {cout << "Konstruktor temeljne klase\n";} /*virtual*/ ~Superclass()
{cout << "Destruktor temeljne klase\n";}
}; class Subclass : public Superclass { public: Subclass () {cout << "Konstruktor izvedene klase\n";} ~Subclass() {cout << "Destruktor izvedene klase\n";} }; int main () { Superclass* p = new Subclass; delete p; return 0; }
27/41
ZAKLJUČAK •
•
•
•
•
uvijek je korisno deklarirati funkcije temeljne klase kao virtualne funkcije time se omogućuje polimorfizam klasa destruktor temeljne klase treba deklarirati kao virtualnu funkciju
kada se kasnije u radu pokaže da neku člansku funkciju nije potrebno koristiti polimorfno, tada se funkcija može deklarirati kao nevirtualna članska funkcija time se dobiva nešto brže izvršenje poziva funkcija
28/41
POLIMORFIZAM NA DJELU •
pokazat ćemo nešto kompliciraniji primjer
•
to je program kojim se računaju aritmetički izrazi
•
•
aritmetički izrazi se računaju tako da se na operande primijeni računska operacija definirana zadanim operatorom aritmetički izrazi se sastoje od više članova i faktora, koji mogu biti grupirani unutar zagrada
29/41
RAČUNANJE ARITMETIČKIH IZRAZA •
članovi i faktori izraza sadrže operande i operatore
•
izrazi oblika -3 ili +7 se nazivaju unarni izrazi
•
•
izrazi oblika 3 + 7 ili 6.7 / 2.0 se nazivaju binarni izrazi izraz u zagradama se tretira kao jedinstveni operand
30/41
APSTRAKTNO SINTAKTIČKO STABLO •
razmotrimo primjer kompleksnog izraza:
-5 * (3+4) •
•
•
ovakav izraz najprije je potrebno na adekvatan način zapisati u memoriji redoslijed izvršenja operacija treba biti točno zapisan
tako se pravilno može primijeniti pravilo asocijativnosti i prioriteta djelovanja operatora
31/41
APSTRAKTNO SINTAKTIČKO STABLO •
•
za prezentaciju izraza u memoriji potrebno je koristiti neku strukturu podataka kod modernih kompajlera i intepretera za internu prezentaciju izraza koristi se zapis koji se naziva apstraktno sintaktičko stablo
-5 * (3+4) •
gornji izraz se može apstraktno predstaviti u obliku:
32/41
APSTRAKTNO SINTAKTIČKO STABLO •
•
stablo može imati više čvorova i grana u čvorovima su zapisani sintaktčiki entiteti: operatori i operandi
•
čvor iz kojeg počinje grananje stabla naziva se korijen stabla
•
čvorovi koji nemaju grane nazivaju se lišće stabla
•
svi ostali čvorovi se nazivaju unutarnji čvorovi
•
unutarnji čvorovi i lišće su pod-stabla stabla
•
oni su povezani korijenom stabla
33/41
APSTRAKTNO SINTAKTIČKO STABLO •
apstraktno sintaktičko stablo aritmetičkih izraza se može izgraditi na slijedeći način: 1. u korijen stabla se zapisuje operator najvećeg prioriteta 2. zatim se crtaju dvije grane koje povezuju čvor lijevog i desnog operanda 3. ako su ti operandi ponovo neki izrazi, proces izgradnje se nastavlja tako da se u te čvorove ponovo upisuje operator najvećeg prioriteta u pod-izrazima 4. ako su operandi brojevi, u čvor se upisuje vrijednost broja 5. proces izgradnje završava tako da se u lišću nalaze brojevi, a u višim čvorovima se nalaze operatori
34/41
APSTRAKTNO SINTAKTIČKO STABLO •
u programu se sintaktičko stablo može zapisati pomoću strukture koja sadrži: –
informaciju čvora
–
pokazivače na podstabla sa oprandima
•
•
•
svi čvorovi ne sadrže istu informaciju u unutarnjim čvorovima je sadržana informacija o operatorima u čvorovima lišća nalaze se operandi 35/41
APSTRAKTNO SINTAKTIČKO STABLO •
•
čvorovi koji sadrže binarne operatore trebaju imati dva pokazivača za vezu s lijevim i desnim operandom
čvorovi koji sadrže unarni operator trebaju samo jedan
pokazivač •
čvorovi lišća ne trebaju ni jedan pokazivač
•
očito je da čvorove treba tretirati polimorfno 36/41
Polimorfizam na djelu – izvršenje aritmetičkih izraza #include
const int op; // tip operatora ('+', '-', '*' ili '/')
using namespace std;
ExprNode * left; // pokazivac na lijevi operand ExprNode * right; // pokazivac na desni operand
class ExprNode // temeljna klasa za sve tipove izraza
public:
{
BinaryOpNode (const int a, ExprNode *b, ExprNode *c): friend ostream& operator<< (ostream&, const ExprNode *);
op (a), left (b), right (c) { }
public:
~BinaryOpNode() {delete left; delete right;}
ExprNode() {}
double execute() const;
virtual ~ExprNode() { }
void print (ostream& out) const
virtual void print (ostream&) const = 0; virtual double execute() const = 0; };
{ out << "(" << left << (char)op << right << ")"; } }; double BinaryOpNode:: execute() const { switch(op) {
ostream& operator<<(ostream& out, const ExprNode *p) {
case '+': return left->execute() + right->execute();
p->print(out); // virtualni poziv, vrijedi za sve subklase
case '-': return left->execute() - right->execute ();
return out;
case '*': return left->execute() * right->execute();
}
case '/': // provjeri dijeljenje s nulom { double val = right->execute();
class NumNode: public ExprNode {
if(val != 0)
double n;
return left-> execute() / val;
public:
else
NumNode (double x): n (x) { }
return 0;
~NumNode() { };
}
void print (ostream& out) const { out << n; }
default: return 0;
double execute() const {return n;} };
} }
class UnaryOpNode: public ExprNode { const int op; // tip operatora ('+' ili '-')
int main() { // formiraj stablo za izraz -5 * (3+4)
ExprNode * oprnd; // pokazivac na operanda
ExprNode* t= new BinaryOpNode('*',
public:
new UnaryOpNode ('-',
UnaryOpNode (const int a, ExprNode * b): op (a), oprnd (b)
new NumNode(5)),
{ }
new BinaryOpNode('+',
~UnaryOpNode() {delete oprnd;}
new NumNode(3),
double execute() const { return (op=='-')? -oprnd->execute() : oprnd->execute();}
new NumNode(4))); // ispiši i izvrši izraz
void print (ostream& out) const { out << "(" << (char)op << oprnd << ")";}
cout << t << "=" << t-> execute() << endl; // dealociraj memoriju
};
delete t; return 0;
class BinaryOpNode: public ExprNode {
}
private:
37/41
POLIMORFIZAM NA DJELU – IZVRŠENJE ARITMETIČKIH IZRAZA •
za sve tipove čvorova koristi temeljna virtualna klasa
ExprNode •
•
•
•
•
pomoću nje ćemo definirati klase BinaryOpNode , i UnaryOpNode NumNode
u temeljnoj klasi ExprNode definirane su dvije čiste virtualne funkcije print() i execute() funkcija print() ispisuje sadržaj stabla funkcija execute() izvršava, odnosno računa aritmetički izraz koji je definiran pod-stablom operator << omogućuje da se ispiše sadržaj stabla iz poznatog pokazivača na stablo (ili pod-stablo)
38/41
POLIMORFIZAM NA DJELU – IZVRŠENJE ARITMETIČKIH IZRAZA •
klasa NumNode se izvodi iz ExprNode
•
ona sadrži numeričke operande tipa double
•
funkcija print() ispisuje numeričku vrijednost
•
funkcija execute() vraća vrijednost čvora
•
•
•
•
•
naime to je vrijednost aritmetičkog izraza koji sadrži samo jedan broj klasa UnaryOpNode se izvodi iz ExprNode ona sadrži unarni operator + ili -, te pokazivač na čvor koji je operand konstruktor formira čvor tako da prima argumente: operator, te pokazivač na operanda
destruktor dealocira memoriju koju zauzima operand 39/41
Polimorfizam na djelu – izvršenje aritmetičkih izraza •
•
•
•
•
•
•
klasa BinaryOpNode se također izvodi iz ExprNode ona sadrži binarni operator ( +, -, * ili /), te pokazivače na čvorove koji je predstavljaju lijevi i desni operand.
u ovom slučaju funkcija print() ispisuje, unutar zagrada, lijevi operand, operator i desni operand funkcija execute() vraća vrijednost koja se dobije primjenom operatora na lijevi i desni operand posebno je analiziran slučaj operacije dijeljenja kako bi se izbjeglo dijeljenje s nulom konstruktor formira čvor tako da prima argumente: operator, te pokazivač na čvorove desnog i lijevog operanda
destruktor dealocira memoriju koju zauzimaju operandi 40/41