Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

Dr.

eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Predavanje 13_b
Jedna od tipinih primjena kasnog povezivanja i polimorfizma je kreiranje heterogenih nizova (ili
heterogenih vektora, ili, jo openitije, heterogenih kontejnerskih objekata) koji kao svoje elemente
mogu prividno sadravati objekte razliitih tipova. Striktno posmatrano, za sada njihovi elementi nee
jo uvijek zaista sadravati objekte razliitih tipova, nego e pokazivati na objekte razliitih tipova, ali u
veini konceptualnih razmatranja, ovaj implementacioni detalj moemo ignorirati (moemo rei da oni
sadre pokazivae razliitih dinamikih tipova). Na primjer, razmotrimo sljedeu sekvencu naredbi:
Student *studenti[5];
studenti[0] = new Student("Paja Patak", 1234);
studenti[1] = new DiplomiraniStudent("Miki Maus", 3412, 2004);
studenti[2] = new Student("Duko Dugouko", 4123);
studenti[3] = new Student("Tom Maak", 2341);
studenti[4] = new DiplomiraniStudent("Deri Mi", 4321, 1997);

Strogo posmatrano, ove naredbe deklariraju niz nazvan studenti od pet pokazivaa na objekte
tipa Student, ijim elementima kasnije dodjeljujemo adrese pet dinamiki stvorenih objekata, od
kojih su tri tipa Student, a dva tipa DiplomiraniStudent (ovo je posve legalno, s obzirom da se
pokazivau na objekat neke klase smije legalno dodijeliti pokaziva na objekat ma koje klase nasljeene
iz te klase). Meutim, sjetimo se da se nizovi pokazivaa na primjerke neke klase mogu koristiti skoro
identino kao da se radi o nizovima iji su elementi primjerci te klase, samo to za pristup atributima i
metodama umjesto operatora . koristimo operator ->. Dalje, kako je metoda Ispisi virtualna,
njena primjena nad pojedinim pokazivaima dovodi do razliitog dejstva u zavisnosti od toga na kakav
objekat konkretan pokaziva pokazuje. Stoga e naredba poput
for(int i = 0; i < 5; i++) {
studenti[i]->Ispisi(); std::cout << endl;
}

dovesti do sljedeeg ispisa:


Student Paja Patak ima indeks 1234
Student Miki Maus ima indeks 3412, a diplomirao je 2004. godine
Student Duko Dugouko ima indeks 4123
Student Tom Maak ima indeks 2341
Student Deri Mi ima indeks 4321, a diplomirao je 1997. godine
Ako zanemarimo sitni detalj da koristimo operator -> umjesto operatora ., vidimo da se niz
studenti zapravo ponaa kao heterogeni niz iji lanovi mogu biti kako obini, tako i diplomirani
studenti, odnosno ponaa se kao da se radi o nizu iji su elementi razliitog tipa (uskoro emo vidjeti
kako moemo realizirati heterogene nizove bez pokazivaa). Primijetimo da ovdje presudnu ulogu igraju
virtualne metode. Da metoda Ispisi nije bila virtualna, svi elementi niza bi se tretirali na identian
nain, kao da se radi o objektima klase Student, u skladu sa nainom na koji je niz deklariran.
U praksi je, kad god se radi sa dinamiki alociranim objektima, uvijek mnogo sigurnije raditi sa
pametnim pokazivaima. Za to postoji nekoliko razloga. Prvo, ukoliko ne koristimo pametne pokazivae,
sve dinamiki alocirane objekte moramo runo obrisati. Neko e rei da nije prevelika muka dodati
naredbu poput
for(int i = 0; i < 5; i++) delete studenti[i];

koja e obrisati alocirane objekte. Meutim, ak i ako zanemarimo injenicu da je lako zaboraviti
eksplicitno obrisati ove objekte, moe nastati problem ukoliko vie lanova niza pokazuje na isti objekat.
Recimo, posve naivna naredba poput studenti[4] = studenti[2], kojoj smo eljeli rei da
elementi studenti[2] i studenti[4] predstavljaju istog studenta dovee kasnije do greke
dvostrukog brisanja kada se izvri gore napisana petlja (s obzirom da e se dva puta izvriti brisanje
preko dva razliita pokazivaa, ali koji pokazuju na isti objekat). Konano, u sluaju da dinamika
1

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

alokacija nekog od objekata ne uspije, imaemo curenje memorije ukoliko odmah ne izvrimo hvatanje
odgovarajueg izuzetka, pri emu emo u obradi tog izuzetka izvriti uklanjanje objekata koji su bili
alocirani prije nego to je dolo do bacanja izuzetka. Svih ovih nevolja oslobaa nas koritenje pametnih
pokazivaa. Sreom, vrlo je lako prei na pametne pokazivae. Sve to trebamo uraditi je deklarirati niz
studenti kao niz pametnih pokazivaa (tj. kao std::shared_ptr<Student> studenti[5]) i
zamijeniti pozive operatora new pozivom funkcije make shared (tj. koristiti konstrukcije poput
std::make shared<Student>("Paja Patak", 1234)).
Mada se virtualne funkcije obino koriste za realizaciju polimorfizma, sam koncept virtualnih
funkcija i kasnog povezivanja nije nuno vezan za polimorfizam (mada je uvijek vezan za nasljeivanje).
Kako se radi o veoma vanom konceptu, koji ini jezgro objektno orijentirane filozofije, razmotriemo
jedan ilustrativan primjer koji uvodi virtualne funkcije nevezano od polimorfizma, a zatim emo isti
primjer proiriti upotrebom polimorfizma. Pretpostavimo da elimo deklarirati nekoliko klasa koje
opisuju razne geometrijske likove, na primjer krugove, pravougaonike i trouglove. Svim likovima je
zajedniko da posjeduju obim i povrinu. Stoga moemo deklarirati klasu nazvanu ApstraktniLik
koja e sadravati samo svojstva zajednika za sve likove, kao to je atribut naziv koji e sadravati
naziv lika (Krug, Trougao, itd.), metode DajObim i DajPovrsinu, kao i metodu Ispisi
koja ispisuje osnovne podatke o liku (naziv, obim i povrina). Mogue je dodati i jo nekih atributa koji
bi bili zajedniki za sve vrste likova (npr. koordinate teita, s obzirom da svi likovi posjeduju teite,
bez obzira o kojoj se vrsti lika radi), ali radi jednostavnosti ovdje emo se zadrati samo na atributu koji
predstavlja naziv lika. Naziv ApstraktniLik smo odabrali zbog injenice da ova klasa ne opisuje
nikakav konkretan lik i nije namijenjena da se koristi samostalno, nego joj je uloga da slui iskljuivo
kao bazna klasa za klase Krug, Pravougaonik i Trougao, koje e opisivati konkretne likove.
Dakle, ove klase emo naslijediti iz klase ApstraktniLik, ime zapravo izraavamo injenicu da
krugovi, pravougaonici i trouglovi jesu likovi. Pored toga, definiraemo i klasu Kvadrat nasljeenu
iz klase Pravougaonik, koja odraava injenicu da su kvadrati specijalna forma pravougaonika. Na
taj nain dobijamo cijelu hijerarhiju klasa, kao na sljedeoj slici:
ApstraktniLik

Krug

Pravougaonik

Trougao

Kvadrat

Odmah na poetku moramo istai da se mogu postaviti izvjesne dileme da li se klasa Kvadrat
treba definirati kao klasa naslijeena iz klase Pravougaonik. S jedne strane, neosporna je injenica
da kvadrati jesu pravougaonici, tako da je prvi uvjet za svrsihodnost nasljeivanja ispunjen. Mnogo je
spornije da li se kvadrati mogu koristiti u svim kontekstima kao i pravougaonici. Openito gledano,
odgovor na ovo pitanje je negativan. Kasnije emo vidjeti kakve implikacije unosi ovaj zakljuak.
Razmotrimo sada mogue definicije ovih klasa. Krenimo prvo od definicije osnovne klase
ApstraktniLik. Kako ova klasa ne predstavlja nikakav konkretan lik, ne moemo specificirati
nikakve konkretne atribute, osim onih koji su zajedniki za sve vrste likova (pri emu smo, radi
jednostavnosti, uzeli da je naziv lika jedini takav atribut), niti ikakve konkretne postupke za raunanje
obima i poluprenika (s obzirom da se ti postupci razlikuju za razliite vrste likova). Meutim, kako
emo u klasi ApstraktniLik definirati metodu Ispisi, koju e sve ostale klase naslijediti, a koja
poziva metode za raunanje obima i povrine, te metode obavezno moramo definirati i u klasi
ApstraktniLik, pa makar i ne radile nita smisleno. Usvojiemo zasada da ove metode za klasu
ApstraktniLik prosto vraaju nulu kao rezultat, a u svakoj od nasljeenih klasa definiraemo prave
postupke za raunanje obima i povrine, u skladu sa konkretnom vrstom lika. Stoga e prva verzija
definicije klase ApstraktniLik izgledati ovako (kasnije emo uvidjeti da e biti potrebne izvjesne
modifikacije u deklaraciji ove klase da bi sve radilo kako treba):

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

class ApstraktniLik {
protected:
std::string naziv;
public:
ApstraktniLik(string naziv) : naziv(naziv) {}
double DajObim() const { return 0; }
double DajPovrsinu() const { return 0; }
void Ispisi() const {
std::cout << "Lik: " << naziv << " Obim: " << DajObim()
<< " Povrina: " << DajPovrsinu() << std::endl;
}
};

Sada moemo definirati i klasu Krug, naslijeenu iz klase ApstraktniLik. U njoj emo
deklarirati atribut r koji uva poluprenik kruga, konstruktor koji inicijalizira ovaj atribut, kao i
metode DajObim i DajPovrsinu koje raunaju obim i povrinu kruga, a koje treba da zamijene
istoimene metode iz bazne klase. U ovoj klasi definirali smo i statiki konstatntni atribut PI koji
predstavlja vrijednost konstante sa onolikom preciznou koliko to tip double doputa. Pri tome
treba naglasiti da je ovakav nain inicijalizacije atributa podran tek od C++11 (inae, ranije verzije
jezika C++ dozvoljavali su da se na licu mjesta inicijaliziraju jedino cjelobrojni statiki konstantni
atributi, i to iskljuivo nekom konstantnom vrijednou, a na rezultatom funkcije). Atribut naziv i
metodu Ispisi ova klasa nasljeuje iz bazne klase Lik:
class Krug : public ApstraktniLik {
double r;
static const PI(4 * std::atan(1));
public:
Krug(double r) : ApstraktniLik("Krug"), r(r) {}
double DajObim() const { return 2 * PI * r; }
double DajPovrsinu() const { return r * r * PI; }
};

S obzirom da u ovoj klasi mijenjamo funkcije DajObim i DajPovrsinu u odnosu na njihove


izvedbe u baznoj klasi, ove funkcije bi trebalo oznaiti kljunom rijei override (ovo vrijedi za
C++11, jer ranije verzije jezika C++ nisu imale ovu kljunu rije). Ukoliko to uinimo, kompajler e se
odmah pobuniti da bi ove funkcije u baznoj klasi morale biti virtuelne. Mada je to posve tano, mi
namjerno ovdje neemo stavljati kljunu rije override i neemo (zasad) proglaavati ove funkcije
za virtuelne, upravo sa ciljem da uoimo probleme zbog kojih emo shvatiti da zaista mora biti tako.
Na slian nain emo definirati i klase Pravougaonik i Trougao. Klasa Pravougaonik
posjedovae dva, a klasa Trougao tri atributa koji uvaju duine stranica. Tu su i odgovarajui
konstruktori sa dva odnosno tri parametra za inicijalizaciju tih atributa, kao i odgovarajue definicije
metoda za raunanje obima i povrine (za raunanje povrine trougla koristimo Heronov obrazac):
class Pravougaonik : public ApstraktniLik {
double a, b;
public:
Pravougaonik(double a, double b) : ApstraktniLik("Pravougaonik"),
a(a), b(b) {}
double DajObim() const { return 2 * (a + b); }
double DajPovrsinu() const { return a * b; }
};
class Trougao : public ApstraktniLik {
double a, b, c;
public:
Trougao(double a, double b, double c) : ApstraktniLik("Trougao"),
a(a), b(b), c(c) {}
double DajObim() const { return a + b + c; }
double DajPovrsinu() const {
double s((a + b + c) / 2);
return std::sqrt(s * (s - a) * (s - b) * (s - c)) ;
}
};

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Sada emo iz klase Pravougaonik izvesti klasu Kvadrat, koja opisuje kvadrat kao specijalan
sluaj pravougaonika. Jedino to emo promijeniti u ovoj klasi u odnosu na njenu baznu klasu
Pravougaonik je konstruktor (koji se svakako ne nasljeuje), s obzirom da se kvadrat opisuje sa
jednim, a ne sa dva parametra. Ovaj konstruktor postavie oba atributa koji opisuju pravougaonik na iste
vrijednosti, s obzirom da je kvadrat upravo pravougaonik sa jednakim stranicama, i promijeniti naziv
objekta na Kvadrat (s obzirom da e konstruktor klase Pravougaonik, koji svakako moramo
pozvati iz konstruktora klase Kvadrat, postaviti naziv objekta na Pravougaonik). Svi ostali
elementi (ukljuujui i metode za raunanje obima i povrine) mogu se prosto naslijediti, jer se obim i
povrina kvadrata mogu raunati na isti nain kao i za pravougaonik (uz jednake duine stranica):
class Kvadrat : public Pravougaonik {
public:
Kvadrat(double a) : Pravougaonik(a, a) { naziv = "Kvadrat"; }
};

Primijetimo da bismo napravili veliku konceptualnu greku da smo prvo definirali klasu Kvadrat
a zatim iz nje izveli klasu Pravougaonik. Do ovakvog pogrenog rezonovanja mogli bismo doi
ukoliko bismo brzopleto zakljuili da se kvadrat opisuje jednim atributom (duinom stranice), a
pravougaonik sa dva atributa (duinama dvaju stranica), tako da pravougaonik zahtijeva vie atributa za
opis nego kvadrat. Meutim, prava je injenica da i kvadrat isto tako posjeduje sve atribute koje
posjeduje i pravougaonik, samo su oni meusobno jednaki. Kvadrat je specijalan sluaj pravougaonika, a
ne obrnuto, tako da klasa Kvadrat treba da bude izvedena a klasa Pravougaonik bazna klasa. Svi
kvadrati su pravougaonici, ali svi pravougaonici nisu kvadrati.
Ovim smo definirali sve potrebne klase. Meutim, ukoliko bismo poeljeli da isprobamo napisane
klase, veoma brzo bismo vidjeli da neto nije u redu. Na primjer, pretpostavimo da smo sa napisanim
klasama izvrili sljedee naredbe:
Pravougaonik p(5, 4);
Krug k(3);
p.Ispisi();
std::cout << "O = " << p.DajObim() << " P = " << p.DajPovrsinu()
<< std::endl;
k.Ispisi();
std::cout << "O = " << k.DajObim() << " P = " << k.DajPovrsinu()
<< std::endl;

Ove naredbe dovele bi do sljedeeg ispisa na ekranu:


Lik: Pravougaonik Obim: 0 Povrina: 0
O = 18 P = 20
Lik: Krug Obim: 0 Povrina: 0
O = 18.849556 P = 28.274334
Vidimo da metode DajObim i DajPovrsinu same za sebe rade korektno, ali neto nije u redu
sa metodom Ispisi. ta se zapravo deava? Problem ponovo lei u ranom povezivanju. Naime, mada
klasa Pravougaonik posjeduje metodu Ispisi koja je naslijeena iz klase ApstraktniLik,
nastaje problem zbog toga to klasa ApstraktniLik sadri metode DajObim i DajPovrsinu
koje vraaju nulu kao rezultat, a kompajler je ve u fazi prevoenja povezao metodu Ispisi sa
metodama DajObim i DajPovrsinu iz iste klase. Stoga metoda Ispisi zapravo poziva metode
DajObim i DajPovrsinu iz klase ApstraktniLik, bez obzira to su ove metode izmijenjene u
klasi Pravougaonik odnosno Krug! Moemo rei da je klasa Pravougaonik naslijedila
metodu Ispisi koja je ve povezana sa neodgovarajuim metodama DajObim i DajPovrsinu!
Da bismo rijeili ovaj problem, potrebno je odluku o tome koje verzije metoda DajObim i
DajPovrsinu treba pozvati iz metode Ispisi odgoditi do samog trenutka njihovog pozivanja,
odnosno koristiti kasno povezivanje. To zapravo znai da ove metode u baznoj klasi moraju biti
virtualne. Na taj nain e kada pozovemo metodu Ispisi nad objektom p koji je tipa
Pravougaonik, unutar metode Ispisi biti pozvane upravo metode DajObim i DajPovrsinu
iz klase Pravougaonik, s obzirom da se u tom trenutku zna da radimo nad objektom tipa
4

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Pravougaonik (to se na osnovu same deklaracije klase ApstraktniLik nije moglo znati).
Naime, u trenutku takvog poziva, dinamiki tip izraza *this e biti Pravougaonik, bez obzira to
je ova metoda definirana u klasi ApstraktniLik, a virtualne metode se pozivaju u skladu sa
dinamikim tipom. Slino, kada pozovemo metodu Ispisi nad objektom k koji je tipa Krug,
unutar metode Ispisi e biti pozvane metode DajObim i DajPovrsinu iz klase Krug. Kasno
povezivanje je neophodno jer se oigledno u vrijeme prevoenja programa ne moe znati nad kojim e
objektom metoda Ispisi biti pozvana, pa prema tome ni koje metode DajObim i DajPovrsinu
treba da se iz nje pozovu. Stoga u deklaraciji klase ApstraktniLik treba nainiti sljedee izmjene:
class ApstraktniLik {
...
virtual ~ApstraktniLik() {}
virtual double DajObim() const { return 0; }
virtual double DajPovrsinu() const { return 0; }
...
};

Uz ovakve izmjenu sve e raditi u skladu sa oekivanjima. Ovdje smo iskoristili priliku i da
definiramo virtualni destrtuktor, koji e nam kasnije biti neophodan za realizaciju polimorfizma.
Meutim, ovdje je posebno interesantno da se potreba za kasnim povezivanjem i virtualnim metodama
javila neovisno od polimorfizma, koji se u ovom primjeru ne koristi. Da smo prilikom izmjene metoda
DajObim i DajPovrsinu stavili kljunu rije override, kompajler bi nas odmah upozorio na
ovu nunost. Ovo je dobar razlog da sad izvrimo takvu modifikaciju i dodamo ovu kljunu rije tamo
gdje treba (da se odmah naviknemo na njenu upotrebu).
Iz izloenog primjera moemo vidjeti da virtualne metode donose jednu sutinsku (po mnogima i
revolucionarnu) novost u odnosu na sve sa ime smo se ranije susretali. Naime, klasine funkcije su
omoguavale da novonapisani dijelovi programa mogu da koriste stare dijelove programa (koristei
pozive funkcija), bez ikakve potrebe da mijenjamo ve napisane dijelove programa. Meutim, virtualne
funkcije nam nude upravo obrnuto: da stari (tj. ve napisani) dijelovi programa bez ikakve potrebe za
izmjenama mogu pozivati dijelove programa koji e tek biti napisani! Zaista, posmatrajmo metodu
Ispisi iz prethodnog primjera. Ova metoda poziva metode DajObim i DajPovrsinu, ali kako
se radi o virtualnim metodama, unaprijed se ne zna na koje se metode DajObim i DajPovrsinu (tj.
iz koje klase) ti pozivi odnose sve do samog trenutka poziva, kada e to biti odreeno objektom nad
kojim se metoda Ispisi pozove. Sasvim je mogue da se kasnije odluimo da u program dodamo
podrku za nove likove (npr. elipse, krune isjeke, poligone, itd.). Ukoliko ove likove implementiramo
kao klase naslijeene iz klase ApstraktniLik i definiramo odgovarajue metode za raunanje obima
i povrine, metoda Ispisi (koja je ve davno napisana) e u sluaju potrebe pozivati novonapisane
metode, bez potrebe da vrimo ikakve izmjene u samoj definiciji metode Ispisi. Na taj nain,
virtualne funkcije omoguavaju ve napisanim dijelovima programa da se, na izvjestan nain,
automatski adaptiraju na nove okolnosti koje mogu nastati usljed proirivanja programa!
Mogunost da stari dijelovi programa bez ikakve potrebe za izmjenama mogu pozivati
novonapisane dijelove programa, a koju nam nude virtualne funkcije, lei u osnovi onoga to se naziva
objektno orijentirani pristup. Sve dok u programu ne ponemo koristiti virtualne funkcije (ime program
pripremamo da se automatski adaptira na eventualna proirenja, to je znak da ispravno razmiljamo o
budunosti) ne moemo govoriti o objektno orijentiranom programu (ak i ukoliko u programu
intenzivno koristimo klase i ostala naela objektno orijentirane filozofije), ve samo o objektno
baziranom programu (tj. programu zasnovanom na objektima). Objektno orijentirani programi obavezno
se zasnivaju na hijerarhiji klasa, odnosno ideji da klase koje dijele neke zajednike osobine (poput klasa
Krug i Pravougaonik iz prethodnog primjera) trebaju obavezno biti izvedene iz neke osnovne
klase koja sadi upravo ono to je zajedniko svim tim klasama (poput klase ApstraktniLik iz
prethodnog primjera, koja upravo opisuje injenicu da je svim krugovima i pravougaonicima zajedniko
to to su oni likovi). Jedino uz potovanje takve hijerarhijske organizacije moemo iskoristiti sve
prednosti koje donosi metodologija objektno orjentiranog programiranja.
Ako malo paljivije razmislimo, sjetiemo se da smo se i ranije na jednom mjestu ipak susreli sa
jednim mehanizmom koji omoguava da neka funkcija poziva drugu funkciju ne znajui o kojoj se
5

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

funkciji radi sve do samog trenutka poziva. Taj mehanizam ostvaren je kroz prenos funkcija kao
parametara u druge funkcije, odnosno, jo generalnije, preko pokazivaa na funkcije. Zaista, funkcija
NulaFunkcije koju smo demonstrirali kada smo govorili o prenosu funkcija kao parametara u
funkcije, pozivala je funkciju-parametar nazvanu fun, pri emu se sve do trenutka poziva funkcije
NulaFunkcije ne zna na koju se stvarnu funkciju oznaka funkcije fun odnosi (stvarna funkcija
koju predstavlja fun zadaje se kao parametar funkcije NulaFunkcije). Tako, jednom napisana
funkcija NulaFunkcije bez ikakve izmjene moe pozivati svaku funkciju koja se napie u
budunosti, pod uvjetom da joj se ona prenese kao parametar. Slian sluaj imamo i sa funkcijama iz
biblioteke algorithm koje primaju funkcije kao parametre. Na primjer, funkcija sort kao
opcionalni trei parametar prima ime funkcije kriterija za koju pojma nema kako izgleda niti ta radi, i
koja definitivno nije bila napisana u doba kada je napisana funkcija sort (funkciju kriterija pie
programer koji koristi funkciju sort). Dakle, funkcijski parametri i, openitije, pokazivai na funkcije
(kao i jo generalniji polimorfni funkcijski omotai), takoer na neki nain nude mogunost da stari
dijelovi programa bez ikakve izmjene pozivaju novonapisane dijelove programa, samo to je upotreba
virtualnih funkcija jednostavnija. Ovo otkrie ne treba da nas pretjerano udi. Naime, na kraju ovog
predavanja emo vidjeti da su virtualne funkcije na neki nain zapravo prerueni pokazivai na funkcije,
isto kao to su reference prerueni pokazivai na objekte.
Neko e se vjerovatno zapitati zbog ega smo uope definirali klasu ApstraktniLik kao baznu
klasu za klase Krug, Pravougaonik i Trougao. Naime, jedan zajedniki atribut (naziv lika) i
jedna metoda sa zajednikom definicijom (metoda za ispis podataka sa likom) i nisu neki osobit razlog
da izvedemo ba ovakvo nasljeivanje, umjesto da klase Krug, Pravougaonik i Trougao prosto
napiemo neovisno od ikakve zajednike bazne klase, s obzirom da je uteda koju smo ostvarili
smjetanjem ovih zajednikih elemenata u klasu ApstraktniLik neznatna u odnosu na situaciju koja
bi nastala kada bismo ove elemente posebno definirali u svakoj od ovih klasa. Utede bi mogle biti vee
kada bi izvedeni objekti posjedovali vei broj zajednikih elemenata koje bismo mogli smjestiti u baznu
klasu. Meutim, postoji jedan mnogo jai razlog zbog ega smo se odluili za ovakvu hijerarhijsku
strukturu, a to je mogunost polimorfizma. Naime, ukoliko definiramo neku promjenljivu koja je
pokaziva (obini ili pametni) na klasu ApstraktniLik, takvoj promjenljivoj emo moi dodijeliti
pokaziva na bilo koju od klasa koje su izvedene iz klase ApstraktniLik (tj. na bilo koji lik). Isto
tako, referenca na klasu ApstraktniLik moi e se vezati na bilo koji objekat koji predstavlja ma
kakav konkretan lik. Pored toga, kako su metode DajObim i DajPovrsinu virtualne, pozivi ovih
metoda nad takvim pokazivaem ili referencom proizvodie razliite efekte u zavisnosti od toga na
kakav konkretno lik pokaziva pokazuje, odnosno na kakav je konkretan lik referenca vezana. Drugim
rijeima, imaemo polimorfnu promjenljivu. Na primjer:
std::shared_ptr<ApstraktniLik> lik;
lik = std::make_shared<Pravougaonik>(5, 4);
lik->Ispisi();
std::cout << "O = " << lik->DajObim() << " P = " << lik->DajPovrsinu()
<< std::endl;
lik = std::make_shared<Krug>(3);
lik->Ispisi();
std::cout << "O = " << lik->DajObim() << " P = " << lik->DajPovrsinu()
<< std::endl;

U navedenom primjeru, pametni pokaziva lik je iskoriten kao polimorfna promjenljiva


(pametni pokaziva smo upotrijebili umjesto obinog da se ne zamaramo oko upravljanja memorijom).
Da metode DajObim i DajPovrsinu nisu deklarirane kao virtualne, u ovom primjeru ne bi
ispravno radio ak i njihov samostalni poziv nad promjenljivom lik (a ne samo posredni poziv iz
metode Ispisi), jer bi zbog ranog povezivanja kompajler zakljuio da treba pozvati metode
DajObim i DajPovrsinu iz klase ApstraktniLik (s obzirom da je promjenljiva lik
deklarirana kao pokaziva na ApstraktniLik), odnosno odluka koje metode treba pozvati donijela
bi se prema statikom tipu promjenljive lik, a ne njenom dinamikom tipu.
Moemo zakljuiti da su virtualne metode uvijek potrebne kada elimo koristiti polimorfizam (ili
barem neka simulacija virtualnih metoda, koja se moe postii pomou pokazivaa na funkcije), ali s
druge strane, prethodni primjer nas je uvjerio da one mogu biti potrebne i neovisno od polimorfizma (s

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

obzirom da nam se potreba za virtualnim metodama pojavila i prije nego to smo se odluili da koristimo
polimorfizam). One su zapravo neophodne kad god neka od metoda treba da poziva druge metode, pri
emu se u trenutku pisanja klase ne zna na koje se tano metode (tj. na koje metode iz mnotva
istoimenih metoda koje mogu biti definirane u klasama izvedenim iz te klase) ti pozivi odnose.
Primijetimo da je u jeziku C++ polimorfizam izvorno ogranien samo na tipove koji su izvedeni iz
iste bazne klase (mada postoje napredne programerske tehnike kojima se ovo ogranienje moe
prevazii, ali o tome ne moemo ovdje govoriti). Stoga, iako se polimorfne promjenljive mogu ponaati
kao promjenljive koje tokom ivota mogu imati razliitie tipove, uglavnom se radi o manje ili vie
srodnim tipovima. Recimo, u prethodnom primjeru, promjenljiva lik moe sadravati (preciznije,
pokazivati na) razliite vrste likova, ali samo na likove, a ne i npr. neto drugo (recimo studente).
Upravo zbog toga nam je i bila potrebna klasa ApstraktniLik. Da sve klase koje opisuju razliite
likove nisu kao svog zajednikog pretka imale zajedniku klasu ApstraktniLik, polimorfizam ne bi
bio mogu (bez upotrebe nekih vrlo naprednih tehnika, o kojima neemo govoriti). Ovakve bazne klase
koje slue samo da bi druge klase bile izvedene iz njih u cilju omoguavanja polimorfizma nazivaju se
apstraktne bazne klase. Drugim rijeima, one definiraju samo kostur odnosno ope karakteristike neke
familije objekata, dok e specifinosti svakog pripadnika te familije biti definirane u klasama koje su iz
nje izvedene.
U normalnim okolnostima, primjerci apstraktnih baznih klasa se nikada ne deklariraju (mada se
deklariraju pokazivai ili reference na takve bazne klase, ali se oni uvijek povezuju sa objektima koji
pripadaju nekoj od klasa koje su iz njih izvedeni). Isto tako, primjerci apstraktnih baznih klasa se nikada
ne kreiraju dinamiki. Meutim, do sada nas niko nije spreavao da uradimo tako neto, npr. da izvrimo
definicije poput sljedeih (u posljednje dvije definicije mogla se koristiti i auto deklaracija):
ApstraktniLik lik("Nesto");
ApstraktniLik *p lik(new ApstraktniLik("Nesto"));
std::<ApstraktniLik> pp_lik(std::make_shared<ApstraktniLik>("Nesto"));

Jasno je da su ovakve deklaracije posve beskorisne, jer se sa primjercima klase ApstraktniLik ne


moe uraditi nita korisno. Stoga postoji nain da se ovakve deklaracije formalno zabrane. Za tu svrhu
dovoljno je u klasi ApstraktniLik neke od virtualnih metoda (najbolje sve) proglasiti apstraktnim
(to znai da je te metode besmisleno precizno specificirati unutar te klase). Da bismo to uradili, umjesto
tijela metode trebamo prosto napisati oznaku = 0, kao u sljedeoj deklaraciji ove klase:
class ApstraktniLik {
...
virtual double DajObim() const = 0;
virtual double DajPovrsinu() const = 0;
...
};

// Apstraktna metoda
// Apstraktna metoda

Ovom oznakom govorimo da metoda vjerovatno nee biti implementirana niti unutar klase, niti
izvan klase, nego tek eventualno u nekoj od klasa koje nasljeuju klasu ApstraktniLik. Samo
virtualne metode mogu ostati neimplementirane, s obzirom da se one pozivaju mehanizmom kasnog
povezivanja (tako da se zapravo nikada nee ni pozvati virtuelna metoda iz bazne klase, nego neka
metoda iz neke od naslijeenih klasa, za koje pretpostavljamo da e biti implementirane). Stoga je jasno
da ostavljanje virtualnih metoda neimplementiranim ima smisla samo u apstraktnim baznim klasama
(koje e svakako biti naslijeene). U skladu sa tim, esto se uvodi i formalna definicija apstraktne bazne
klase kao klase koja posjeduje barem jednu apstraktnu virtualnu funkciju, ili kako se to jo esto kae,
isto virtualnu funkciju (engl. pure virtual function). Primijetimo da oznaka = 0 umjesto tijela metode
nije ekvivalentna pisanju praznog tijela funkcije, tj. praznog para vitiastih zagrada {}. Praznim
tijelom funkcije, mi funkciju ipak implementiramo, bez obzira to njena implementacija ne radi nita.
Ukoliko smo definirali apstraktnu baznu klasu (tj. klasu sa isto virtualnim funkcijama) kompajler nam
nee dozvoliti da deklariramo niti jedan primjerak takve klase, niti da pozivom operatora new
dinamiki kreiramo primjerak takve klase. S druge strane, bie dozvoljeno da deklariramo pokaziva na
takvu klasu (jer jedino tako moemo ostvariti polimorfizam), ali e on uvijek pokazivati iskljuivo na
primjerke neke konkretne klase naslijeene iz nje. Inae, u naelu je dozvoljeno da se implementiraju i
7

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

apstraktne (isto virtualne) metode, ali se to vrlo rijetko radi, s obzirom da se takve metode ne mogu
pozivati nad primjercima klase unutar koje su definirane (s obzirom da se primjerci apstraktnih klasa
uope ne mogu kreirati), ve jedino eventualno iz neke od metoda neke od klasa koje su izvedene iz nje.
Na ovom mjestu trebamo rei nekoliko rijei o tome zbog ega nije sigurno da li klasu Kvadrat
treba naslijediti iz klase Pravougaonik. Naime, bez obzira na injenicu da kvadrati jesu ujedno i
pravougaonici, sve operacije koje se mogu primijeniti na pravougaonike ne moraju se nuno moi
primijeniti na kvadrate tako da oni ostanu kvadrati. U implementaciji klasa koju smo razvili, ne postoje
operacije koje su podrane za pravougaonike a koje se ne bi smjele bezbjedno primijeniti na kvadrate,
tako da je u ovom primjeru nasljeivanje opravdano. Meutim, lako je zamisliti realizacije kod kojih to
ne vrijedi. Kvadrat se, na primjer, ne moe izduiti po irini uz ouvanje iste visine, tako da on ostane i
dalje kvadrat (takva operacija e ga pretvoriti u pravougaonik koji nije kvadrat), dok e primjenom iste
operacija na pravougaonik on i dalje ostaje pravougaonik. Stoga, ukoliko bi klasa Pravougaonik
posjedovala i neku metodu koja omoguava takvu transformaciju, izvedba klase Kvadrat kao
naslijeene klase iz klase Pravougaonik bila bi vrlo loe rjeenje, s obzirom da bi se takva metoda
mogla primijeniti i na objekte tipa Kvadrat, to bi neizbjeno dovelo do promjene njihove prirode
(oni bi prestali biti kvadrati). Neko e rei da bi se takva metoda mogla promijeniti u klasi Kvadrat
recimo da baca izuzetak. Meutim, kako se nasljeivanjem omoguava da se objekti tipa Kvadrat
mogu koristiti u svakom kontekstu u kojem se mogu koristiti objekti tipa Pravougaonik, moglo bi
doi do velikih iznenaenja ukoliko bi se u neku funkciju koja radi sa pravougaonicima i koja poziva
takvu metodu poslao objekat tipa Kvadrat, s obzirom da ta funkcija oekuje da e dobiti
pravougaonik (ili makar neto to se ponaa na isti nain kao i pravougaonik), tako da takva funkcija
uope ne bi bila svjesna da poziv sporne metode moe i da ne radi.
O ovakvim detaljima treba dobro razmiljati prilikom donoenja odluke o hijerarhijskom dizajnu
klasa i odnosa izmeu njih. Teoretiari objektno orjentiranog programiranja ovu injenicu su ilustrativno
objasnili kroz hipotetiko pitanje da li klasa Noj treba da bude naslijeena iz bazne klase Ptica, s
obzirom da noj definitivno jeste ptica. Odgovor je vjerovatno negativan s obzirom da klasa Ptica
vjerovatno ima metodu Leti koju objekti klase Noj ne mogu podravati. S obzirom da bi se u
sluaju nasljeivanja objekti klase Noj mogli koristiti u svim kontekstima u kojima i objekti klase
Ptica, do problema bi sigurno dolo ukoliko bi se objekat klase Noj upotrijebio u kontekstu u
kojem se poziva metoda Leti. Pravo rjeenje bi bilo da se uvedu dvije klase PticaKojaLeti i
PticaKojaNeLeti od kojih su obje naslijeene iz klase Ptica, ali pri emu samo u klasi
PticaKojaLeti postoji metoda Leti. Nakon toga, klasa Noj treba naslijediti ne direktno iz
klase Ptica, nego iz klase PticaKojaNeLeti. Autor funkcije koja treba da radi sa nekom pticom
navee da joj je parametar tipa Ptica ukoliko mu nije bitno da li ta ptica leti ili ne, tj. ukoliko nee
pozivati metodu Leti (on to i ne moe uraditi, jer takva metoda ne postoji u klasi Ptica). S druge
strane, ukoliko mu je bitno da ta ptica moe letjeti, tada e specificirati da je parametar te funkcije tipa
PticaKojaLeti a ne prosto Ptica.
Zahvaljujui polimorfizmu, moemo deklarirati heterogeni niz likova, odnosno niz koji se ponaa
kao da su mu elementi likovi koji mogu biti razliitih tipova (mada, bez upotreba vrlo naprednih tehnika,
ti tipovi moraju biti meusobno srodni, tj. izvedeni direktno ili indirektno iz nekog zajednikog baznog
tipa). Sljedei primjer demonstrira jedan takav niz, u kojem smo radi jednostavnijeg upravljanja
memorijom iskoristili pametne pokazivae. Nakon deklaracije i inicijalizacije, nad svakim elementom
ovog niza poziva se metoda Ispisi, koja proizvodi razliite ispise u zavisnosti od toga koji lik sadri
koji element niza:
ApstraktniLik *likovi[5]{std::make_shared<Krug>(3),
std::make_shared<Pravougaonik>(5, 2), std::make_shared<Krug>(3.7),
std::make_shared<Kvadrat>(4), std::make_shared<Trougao>(3, 5, 6)};
for(int i = 0; i < 10; i++) likovi[i]->Ispisi();

Strogo reeno, likovi nije niz koji stvarno sadri objekte razliitih tipova, ve niz iji elementi
pokazuju na objekte razliitih tipova (mada emo uskoro vidjeti i kako moemo napraviti prave
heterogene nizove koji zaista sadre objekte razliitih tipova). Meutim, za nas je bitno da ovaj niz
moemo koristiti kao da se radi o nizu koji sadri objekte razliitih tipova. Zapravo, dinamiki tipovi
elemenata ovog niza se razlikuju od elementa do elementa.
8

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Ukoliko je neka metoda proglaena za virtualnu u nekoj klasi, ona e biti virtualna u svim klasama
koje su direktno ili indirektno izvedene iz nje, bez obzira da li pisali kljunu rije virtual ili ne u tim
izvedenim klasama. S druge strane, mogue je da neka metoda ne bude virtualna u baznoj klasi, a da
bude u izvedenoj (virtualnost se tada odnosi samo kada se ta izvedena klasa koristi kao bazna klasa za
neke druge izvedene klase). Pored toga, ukoliko smo u nekoj izvedenoj klasi izmijenili definiciju neke
virtualne funkcije i smatramo da je ta verzija konana (tj. ne elimo da se njena definicija moe mijenjati
u klasama koje bi se mogle izvoditi iz te klase), moemo takvu funkciju oznaiti sa kljunom rijei
final na kraju zaglavlja. Ovim se zabranjuju njene izmjene u klasama koje mogu biti naslijeene iz
nje. Mogue je i cijelu klasu proglasiti za konanu (npr. class Datum final), ime se potpuno
zabranjuje nasljeivanje iz takve klase. Mogunost ovih zabrana uvedena je tek u C++11.
Zanimljivo je razmotriti kreiranje heterogenih, kontejnerskih klasa, odnosno kontejnerskih klasa koje
mogu uvati kolekciju objekata razliitog tipa (naroito je interesantno pitanje kako izvesti konstrukor
kopije i preklopljeni operator dodjele u takvim klasama). Pretpostavimo, na primjer, da elimo razviti
klasu KolekcijaLikova, koja moe sadravati kolekciju raznih likova, odnosno objekata tipova
izvedenih iz apstraktne bazne klase ApstraktniLik. Kao demonstraciju, prikazaemo moguu
izvedbu takve klase sa minimalistikim interfejsom, konceptualno sline klasi Razred koju smo ranije
razvijali, a koja uva kolekciju uenika (tj. objekata tipa Ucenik). Da bismo bolje ovladali tehnikom
upravljanja memorijom u ovakvim sluajevima, za realizaciju neemo koristiti razne automatske
tipove poput vektora i pametnih pokazivaa, nego samo sredstva nieg nivoa:
class KolekcijaLikova {
int broj_likova, kapacitet;
ApstraktniLik **likovi;
public:
KolekcijaLikova(int kapacitet) : broj_likova(0),
kapacitet(kapacitet), likovi(new ApstraktniLik*[kapacitet]) {}
~KolekcijaLikova();
KolekcijaLikova(const KolekcijaLikova &k);
KolekcijaLikova &operator =(const KolekcijaLikova &k);
void DodajLik(ApstraktniLik *lik);
void DodajKrug(double r) { DodajLik(new Krug(r)); }
void DodajPravougaonik(double a, double b) {
DodajLik(new Pravougaonik(a, b));
}
void DodajTrougao(double a, double b, double c) {
DodajLik(new Trougao(a, b, c));
}
void IspisiKolekciju() const;
};

Klasa KolekcijaLikova interno uva dvojni pokaziva pomou kojeg se pristupa dinamiki
alociranom nizu koji uva pokazivae na objekte tipa ApstraktniLik (uvanje pokazivaa umjesto
samih objekata neophodno je za realizaciju polimorfizma). Kao i obino, atributi broj likova i
kapacitet predstavljaju respektivno broj likova u kolekciji i maksimalan broj likova koji kolekcija
moe primiti, a koji se zadaje putem konstruktora. Implementacija konstruktora je trivijalna, stoga
razmotrimo implementaciju destruktora. Ukoliko pretpostavimo da emo klasi KolekcijaLikova
povjeriti vlasnitvo nad svim likovima koji su u njoj pohranjeni, ona je tada odgovorna i za njihovo
brisanje, tako da bi destruktor mogao izgledati ovako:
KolekcijaLikova::~KolekcijaLikova() {
for(int i = 0; i < broj_likova; i++) delete likovi[i];
delete[] likovi;
}

Konstruktor kopije i preklopljeni operator dodjele su najproblematiniji dio ove klase, tako da emo
njih razmotriti na kraju. to se tie metode DodajLik, ona prosto prima pokaziva na lik koji elimo
dodati u kolekciju i upisuje ga u kolekciju, osim ukoliko je ona popunjena. Kako je brisanje likova
povjereno samoj kolekciji, jasno je da pokazivai koje prosljeujemo ovoj metodi moraju pokazivati na
dinamiki kreirane objekte, inae e brisanje uzrokovati probleme (recimo, poziv DodajLik(&k)
gdje je k neka promjenljiva tipa Krug definitivno bi uzrokovao velike probleme):
9

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

void KolekcijaLikova::DodajLik(ApstraktniLik *lik) {


if(broj_likova >= kapacitet) {
delete lik; throw std::range_error("Kolekcija popunjena!");
}
likovi[broj_likova++] = lik;
}

Pored ove univerzalne metode za dodavanje novih likova u kolekciju, predviene su i metode
DodajKrug, DodajPravougaonik i DodajTrougao koje na osnovu zadanih parametara u
hodu dinamiki kreiraju odgovarajui lik i upisuju ga u kolekciju (radi jednostavnosti, nismo
predvidjeli metodu DodajKvadrat, koje je lako dodati u sluaju potrebe). Posljednja predviena
metoda u ovoj minimalistikoj klasi je metoda IspisiKolekciju koja ispisuje podatke o svim
likovima u kolekciji, prostim pozivom metode Ispisi nad svakim objektom u kolekciji:
void KolekcijaLikova::IspisiKolekciju() const {
for(int i = 0; i < broj_likova; i++) likovi[i]->Ispisi();
}

U sluaju potrebe, lako je ovu klasu proiriti novim metodama. Meutim, razmotrimo sada kako
bismo izveli konstruktor kopije, koji e biti u stanju da formira potpunu kopiju itave kolekcije. Mada je
problem konceptualno slian problemu kopiranja objekata tipa Razred, ovdje nastaju dodatni
problemi zbog injenice da pokazivai unutar niza likovi mogu pokazivati na objekte razliitih
tipova. Zbog toga, sljedee prepisivanje rjeenja koje je radilo za klasu Razred ovdje nee raditi:
KolekcijaLikova::KolekcijaLikova(const KolekcijaLikova &k) :
likovi(new ApstraktniLik*[k.kapacitet]), kapacitet(k.kapacitet),
broj likova(k.broj likova) {
for(int i = 0; i < broj likova; i++)
likovi[i] = new ApstraktniLik(*k.likovi[i]);
// OVO NE RADI!!!
}

Zaista, mi ne moemo kreirati objekte tipa ApstraktniLik, a i da moemo, to nije ono to nam
treba, s obzirom da pokazivai ne pokazuju na objekte tipa ApstraktniLik nego na objekte nekog
konkretnog tipa izvedenog iz apstraktnog tipa ApstraktniLik. Na primjer, ukoliko pokaziva
konkretno pokazuje na objekat tipa Krug, nama treba novi objekat tipa Krug (koji je njegova
kopija), a ne novi objekat tipa ApstraktniLik (koji svakako i ne moemo kreirati). Neko bi mogao
doi na ideju da iskoristi operator typeid za odreivanje kojeg je zaista tipa objekat na koji
razmatrani pokaziva pokazuje i da u skladu sa tim konstruira odgovarajui objekat koji je njegova
kopija. Mada takvo rjeenje radi, ono je vrlo rogobatno. to je jo gore, svaka eventualna dopuna
hijerarhije likova nekim novim likovima (recimo, elipsama ili petouglovima) zahtijevala bi odgovarajue
dopune u konstruktoru kopije. Slijedi prikaz kako bi takvo rjeenje moglo izgledati:
KolekcijaLikova::KolekcijaLikova(const KolekcijaLikova &k) :
likovi(new ApstraktniLik*[k.kapacitet]), kapacitet(k.kapacitet),
broj likova(k.broj likova) {
for(int i = 0; i < broj likova; i++)
if(typeid(*k.likovi[i]) == typeid(Krug))
likovi[i] = new Krug(*(Krug*)k.likovi[i]);
else if(typeid(*k.likovi[i]) == typeid(Pravougaonik))
likovi[i] = new Pravougaonik(*(Pravougaonik*)k.likovi[i]);
else if(typeid(*k.likovi[i]) == typeid(Trougao))
...
}

Ovako bi trebalo nastaviti za sve mogue tipove likova. Primijetimo da smo ovdje morali koristiti i
pretvorbu nanie, s obzirom da ne moemo direktno kreirati objekat izvedene klase iz primjerka bazne
klase (za pretvorbu nanie umjesto operatora static cast koristili smo pretvorbu u stilu jezika C
iskljuivo radi kraeg pisanja). Meutim, rjeenja koja koriste typeid operator (i pretvorbe nanie)
gotovo nikada nisu dobra rjeenja (osim u rijetkim izuzecima) i tipino ukazuju da se problem moe
mnogo elegantnije rijeiti upotrebom virtualnih funkcija. Tako se najee koriteno rjeenje ovog

10

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

problema izvodi dodavanjem jedne nove isto virtuelne metode u apstraktnu baznu klasu koju moemo
nazvati DajKopiju (u engleskoj literaturi ova metoda se tipino naziva clone):
class ApstrakntniLik {
...
virtual ApstrakntniLik *DajKopiju() const = 0;
};

Uloga ove metode je da, kada se pozove nad nekim objektom, kreira identinu kopiju tog objekta i
vrati pokaziva na kreiranu kopiju. Naravno, ovu metodu treba dodati i implementirati u svakoj od klasa
koje su naslijeene iz apstraktne bazne klase. Sreom, te implementacije su veoma jednostavne, s
obzirom da svaki objekat vrlo lako moe kreirati svoju kopiju pozivom svog vlastitog konstruktora
kopije prilikom dinamike alokacije. Recimo, pokazaemo kako bi se ova metoda implementirala u
klasama Krug i Pravougaonik (za ostale klase vrijedila bi analogna implementacija):
class Krug {
...
virtual Lik *DajKopiju() const { return new Krug(*this); };
};
class Pravougaonik {
...
virtual Lik *DajKopiju() const { return new Pravougaonik(*this); };
};

Ovim implementacija konstruktora kopije postaje trivijalna i, to je mnogo vanije, potpuno neovisna od
toga kakvih uope objekata imamo u hijerarhiji objekata izvedenih iz bazne klase:
KolekcijaLikova::KolekcijaLikova(const KolekcijaLikova &k) :
likovi(new ApstraktniLik*[k.kapacitet]), kapacitet(k.kapacitet),
broj likova(k.broj likova) {
for(int i = 0; i < broj likova; i++)
likovi[i] = k.likovi[i]->DajKopiju();
}

Na ovaj nain smo realizirali polimorfno kopiranje. Jedina mana ovog rjeenja je to sve klase u
hijerarhiji moraju definirati metodu DajKopiju. Postoje dodue neki trikovi zasnovani na generikim
mehanizmima kojima je ovo mogue izbjei i automatizirati itav proces, ali radi se o prilino
naprednim tehnikama koje izlaze izvan okvira ovog kursa. Ovdje prikazano kolsko rjeenje uglavnom
zadovoljava sve praktine potrebe. to se tie kopirajueg operatora dodjele, on se moe realizirati na
analogan nain, samo to je prilikom dodjele prethodno potrebno prvo unititi sve objekte koji su bili u
vlasnitvu kolekcije kojoj se vri dodjela (na isti nain kao u destruktoru):
KolekcijaLikova &KolekcijaLikova::operator =(const KolekcijaLikova &k) {
if(&k != this) {
for(int i = 0; i < broj_likova; i++) delete likovi[i];
delete[] likovi;
likovi = new ApstraktniLik*[k.kapacitet];
kapacitet = k.kapacitet; broj_likova = k.broj_likova;
for(int i = 0; i < broj_likova; i++)
likovi[i] = k.likovi[i]->DajKopiju();
return *this;
}
}

Radi poboljanja efikasnosti, trebalo bi realizirati i pomjerajui konstruktor te pomjerajui operator


dodjele. To se moe izvesti na identian nain kao u ranije razvijenoj klasi Razred, tako da to moete
uraditi kao vjebu. Slijedi jednostavan primjer upotrebe razvijene klase:
KolekcijaLikova likovi(20);
likovi.DodajPravougaonik(5, 4); likovi.DodajKrug(3);
likovi.DodajLik(new Pravougaonik(3, 2)); likovi.DodajTrougao(5, 7, 8);
likovi.DodajLik(new Kvadrat(4));
likovi.IspisiKolekciju();

11

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Podsjetimo se jo jednom da zbog injenice da se funkcijom DodajLik likovi predaju u


vlasnitvo kolekciji, ova funkcija smije primati iskljuivo pokazivae na dinamiki alocirane objekte,
tako da ne smijemo pisati neto poput DodajLik(&k), gdje je k neki ve postojei objekat
(zapravo, ak i da ne postoji problem sa brisanjem, ovakve konstrukcije bi mogle biti nesigurne, jer
mogue je da promjenljiva k prestane postojati u trenutku dok kolekcija jo uvijek ivi, tako da bi
nakon toga kolekcija sadravala visei pokaziva). Jedna mogua alternativa je da funkcija DodajLik
uvijek pravi dinamiku kopiju objekta na koji pokazuje njen parametar (ovu kopiju moemo dobiti
pozivom funkcije DajKopiju), a da nakon toga u kolekciji uvamo kreiranu kopiju. Na taj nain e
proraditi konstrukcije poput DodajLik(&k). Meutim, ovim postaju problematine konstrukcije
poput DodajLik(new Krug(5)), ne samo zbog injenice to bismo bespotrebno imali dva identina
dinamiki alocirana objekta (jedan izvorni kreiran pozivom operatora new i njegova kopija kreirana
unutar same funkcije DodajLik), nego i zbog injenice da tada niko ne bi mogao obrisati izvorni
objekat kreiran sa new, jer nismo nigdje sauvali pokaziva na njega (destruktor kolekcije e izbrisati
samo kopiju, a ne i izvorni objekat). Drugim rijeima, curenje memorije bilo bi neizbjeno. Vidimo da je
ovdje dosta teko nai univerzalno rjeenje koje bi zadovoljilo svakoga.
Neto bolje rjeenje moe se postii upotrebom pametnih umjesto obinih pokazivaa unutar
kolekcije. Ako nita drugo, problemi vezano za oslobaanje memorije tada se automatski rjeavaju sami
po sebi. Nije teko izmijeniti definiciju prethodne kontejnerske klase da radi sa pametnim pokazivaima.
U tako izmijenenoj klasi, destruktor postaje nepotreban. to se tie kopirajueg konstruktora i operatora
dodjele, i oni postaju nepotrebni ukoliko se moemo zadovoljiti da kopiranje i dodjeljivanje objekata
tipa KolekcijaLikova bude zasnovano na plitkom kopiranju pohranjenih objekata. Meutim,
ukoliko nam je potrebno duboko kopiranje, mogli bismo ga izvesti slino kao u ranije napisanoj klasi
Razred, samo to radi polimorfnog kopiranja za kreiranje dinamike kopije objekta umjesto poziva
funkcije make_shared morati koristiti funkciju DajKopiju. Meutim, ova funkcija kao rezultat
daje obini a ne pametni pokaziva na kopiju objekta, tako da e biti potrebna i eksplicitna konverzija
njenog rezultata u pametni pokaziva. Slijedi da naredba unutar petlje u konstruktoru kopije koja obavlja
kreiranje dinamikih kopija treba izgledati ovako:
likovi[i] = std::shared_ptr<Likovi>(k.likovi[i]->DajKopiju());

Sline modifikacije potrebne su i u operatoru dodjele.


Veina opisanih problema posljedica su injenice da nam za primjenu polimorfizma trebaju
pokazivai. Naalost, pokazivai ak i kada su pametni, i dalje su pokazivai. Pokazivai samo pokazuju
na druge objekte, a ne sadre ih. Kada kopiramo jedan pokaziva u drugi, ne kopira se pripadni objekat,
nego samo dobijamo situaciju u kojoj dva pokazivaa pokazuju na isti objekat. Sada emo pokazati kako
je mogue napraviti polimorfne promjenljive koje zaista mogu sadravati objekte razliitih tipova (a ne
samo pokazivati na objekte razliitik tipova. Konkretno, kreiraemo tip PolimorfniLik takav da
promjenljive tog tipa mogu sadravati ma koji lik (tj. ma koji objekat tipa izvedenog iz apstraktnog tipa
ApstraktniLik. Slijedi primjer upotrebe promjenljivih takvog tipa:
PolimorfniLik lik1, lik2;
Krug k(5); Pravougaonik p(2, 6); Trougao t(3, 4, 5);
lik1 = k; lik1.Ispisi(); lik2 = p; lik2.Ispisi();
lik2 = lik1; lik1 = t. lik1.Ispisi();
std::cout << lik1.DajObim() << " " << lik2.DajPovrsinu() << std::endl;
PolimorfniLik lik3(Kvadrat(2)), lik4(Pravougaonik(7,4));
lik3.Ispisi(); lik4.Ispisi();
lik3 = Trougao(7, 10, 8); std::cout << lik3.DajObim() << std::endl;

Takoer, pomou ovog tipa moi emo kreirati prave heterogene nizove koji zaista sadre elemente
razliitih tipova, kao u sljedeem primjeru:
PolimorfniLik likovi[5]{Krug(4), Pravougaonik(2, 3), Kvadrat(5.32),
Trougao(8, 12, 9), Pravougaonik(10, 6)};
for(int i = 0; i < 5; i++) likovi[i].Ispisi();

12

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Tehnika kojom se ovo postie je relativno nepoznata, ali u osnovi jednostavna. Ideja je da se tip
PolimorfniLik realizira kao heterogeni kontejner, ali koji u sebi moe sadravati samo jedan
objekat. Takvi tipovi se nazivaju surogatski tipovi, zbog injenice da se oni koriste kao zamjena za
nekoliko drugih tipova, a njihovi primjerci se nazivaju surogati (inae, surogati su specijalni sluaj tzv.
proxy objekata, koji predstavljaju objekte koji se lano predstavljaju i oponaaju objekte nekog drugog
tipa). Surogatski tip PolimorfniLik e interno u sebi sadravati samo pokaziva na jedan objekat
nekog tipa izvedenog iz tipa ApstraktniLik. Po tome su surogati slini pametnim pokazivaima.
Meutim, za razliku od pametnih pokazivaa, kopiranjem surogata kopira se i objekat na koji taj interni
pokaziva pokazuje, odnosno surogat ga uvijek vue za sobom. Na taj nain se stie dojam da surogat
zaista sadri taj objekat. Takoer, surogatski tip mora imati konstruktor sa jednim parametrom koji
omoguava automatsku pretvorbu bilo kojeg tipa kojeg oponaa u taj surogatski tip. Takav konstruktor
mora dinamiki alocirati kopiju objekta koji se pretvara i dodijeliti adresu tako kreirane kopije internom
pokazivau. U skladu s tim, implementacija klase PolimorfniLik mogla bi izgledati recimo ovako:
class PolimorfniLik {
ApstraktniLik *p_lik;
void Test() const {
if(!p_lik) throw std::logic_error("Nespecificiran lik!");
}
public:
PolimorfniLik() : p_lik(nullptr) {};
~PolimorfniLik() { delete p_lik; }
PolimorfniLik(const ApstraktniLik &lik) : p_lik(lik->DajKopiju()) {}
PolimorfniLik(const PolimorfniLik &lik) {
if(!lik.p_lik) p_lik = nullptr;
else p_lik = p_lik->DajKopiju();
}
PolimorfniLik(PolimorfniLik &&lik) {
p_lik = lik.p_lik; lik.p_lik = nullptr;
}
PolimorfniLik &operator =(const PolimorfniLik &lik) {
if(&lik != this) {
delete p_lik;
if(!lik.p_lik) p_lik = nullptr;
else p_lik = p_lik->DajKopiju();
}
return *this;
}
PolimorfniLik &operator =(PolimorfniLik &&lik) {
if(&lik != this) {
delete p_lik;
p_lik = lik.p_lik; lik.p_lik = nullptr;
}
return *this;
}
double DajObim() const { Test(); return p_lik->DajObim(); }
double DajPovrsinu() const { Test(); return p_lik->DajPovrsinu(); }
void Ispisi() const { Test(); p_lik->Ispisi(); }
};

Interni pokaziva p_lik se u konstruktoru bez parametara postavlja na nul-pokaziva, tako da


surogat, ukoliko nije inicijaliziran odmah po kreiranju, ne predstavlja nikakav konkretan lik sve dok mu
se ne dodijeli neki konkretan lik. Surogat mora implementirati sve metode koje podravaju i tipovi koje
oponaa (konkretno DajObim, DajPovrsinu i Ispisi). Implementacije ovih metoda prvo
testiraju da li surogat predstavlja ikakav konkretan lik pozivom privatne metode Test (koja prosto
testira da li je interni pokaziva nul-pokaziva ili ne), te pozivaju odgovarajue metode nad objektom na
koji pokazuje interni pokaziva ukoliko je test uspjean. Konstruktor sa jednim parametrom prima
referencu na objekat tipa ApstraktniLik, to automatski znai da e moi primiti i ma kakav
konkretan lik iji je tip naslijeen iz tipa ApstraktniLik. Konstruktor kopije i kopirajui operator
dodjele obavljaju polimorfno kopiranje koristei poziv metode DajKopiju, ali samo ukoliko interni
pokaziva u objektu koji se kopira nije nul-pokaziva (u suprotnom se nema ta kopirati). Konano, tu su
i pomjerajui konstruktor i operator dodjele koji su dovoljno jednostavni da ne trae objanjenja.
13

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_b
Akademska godina 2013/14

Na prvi pogled, nije oigledno kako radi dodjela poput lik = k u kojoj je lik tipa
PolimorfniLik, a k recimo tipa Krug. Meutim, kako su lik i k razliitih tipova, a nema
operatora dodjele koji bi mogao da primi objekat tipa Krug, pri ovoj dodjeli se pokuava pretvorba
tipa, odnosno ona se interpretira kao lik = PolimorfniLik(k). Sada se poziva konstruktor sa
jednim parametrom koji kreira privremeni bezimeni objekat tipa PolimorfniLik na osnovu objekta
k tipa Krug, koji se zatim pomjerajuim operatorom dodjele prebacuje u objekat lik. Ovo bi se
moglo dodatno optimizirati tako to bi se dodao jo jedan operator dodjele koji bi primao referencu na
objekat tipa ApstraktniLik, tako da bi se ova dodjela mogla izvravati direktno bez pretvorbe. Ovo
je prije standarda C++11 zaista bilo gotovo neophodno, jer dok nisu postojali pomjerajui operatori
dodjele, za kopiranje privremenih objekata su se koristili kopirajui operatori dodjele, koji bi
bespotrebno pravili jo jednu kopiju. Ovako, uvoenjem pomjerajueg operatora dodjele, razlika u
efikasnosti koju bismo dobili uvoenjem jo jednog operatora dodjele je posve zanemarljiva.
Jedan od nedostataka surogatske klase PolimorfniLik je potreba za metodom DajKopiju u
svim klasama koju ona treba da zamjenjuje. Kao to je ve reeno, ovaj nedostatak je mogue rijeiti, ali
to izlazi izvan okvira ovog kursa. U osnovi, polimorfni funkcijski omotai interno koriste sline tehnike
kakve su izvedene u ovoj surogatskoj klasi. U nekim nestandardnim bibliotekama postoje klase koje su
ove tehnike doveli gotovo do savrenstva. Recimo, u boost kolekciji (nestandardnih) biblioteka za
C++ postoji biblioteka any koja definira istoimeni tip. Promjenljive tipa any su takve da je u njih
mogue stavljati doslovce sve to se moe kopirati (brojeve, stringove, vektore, razne korisniki
definirane tipove, itd.).
Za kraj ostaje jo da objasnimo kako zapravo radi mehanizam pozivanja virtualnih funkcija. Kao to
smo ve rekli, virtualne funkcije su zapravo prerueni pokazivai na funkcije, to i ne treba da udi, jer u
osnovi svake indirekcije (a kod virtualnih funkcija se oigledno radi o indirektnim pozivima) lee
pokazivai. Naime, svakoj virtualnoj funkciji pridruuje se jedan pokaziva na funkciju, koji se u
konstruktoru klase automatski inicijalizira da pokazuje upravo na tu funkciju. U svakoj od naslijeenih
klasa koje modificiraju tu funkciju, konstruktor inicijalizira taj pokaziva da pokazuje na modificiranu
verziju te funkcije. Virtualna funkcija se nikada ne poziva direktno, nego iskljuivo preko njoj
pridruenog pokazivaa, koji je prethodno inicijaliziran da pokazuje na pravu verziju funkcije, tj. onu
verziju koju zaista u tom trenutku treba pozvati. Stoga se ponaanje virtualnih funkcija u naelu moe
simulirati koritenjem pokazivaa na funkcije. Meutim, to u praksi ne treba raditi, jer je mnogo
jednostavnije koristiti virtualne funkcije, kad ve postoje. Njihovom upotrebom sve zavrzlame oko
upotrebe pokazivaa na funkcije i njihove pravilne inicijalizacije umjesto nas obavlja sam kompajler,
ime nas poteuje mnogih muka.
Strogo reeno, mehanizam virtualnih funkcija zapravo uvodi jo jedan nivo indirekcije. Naime,
ukoliko postoji mnogo virtualnih metoda u nekoj klasi, neracionalno je u svakom primjerku te klase
uvati pokaziva za svaku od tih virtualnih metoda, s obzirom da svi primjerci iste klase dijele iste
virtualne metode. Zbog toga se svi pokazivai na konkretne izvedbe virtualnih metoda neke klase uvaju
izvan klase u jednom nizu (tabeli) pokazivaa koji se zove tabela virtualnih metoda (skraeno TVM)
koju automatski kreira kompajler, dok sami primjerci klasa sadre samo pokaziva na tabelu virtualnih
metoda za tu klasu. Prilikom poziva neke virtualne metode, prvo se vri dereferenciranje tog pokazivaa
da se izvri pristup tabeli virtualnih metoda, nakon ega se u tabeli pronalazi odgovarajui pokaziva i
indirektno poziva traena metoda preko tog pokazivaa. Zbog ove dvostruke indirekcije, pozivanje
virtualnih metoda je neto sporije nego kod metoda koje nisu virtualne. Ovo usporenje se posebno
primijeti kod metoda sa kratkim tijelom ije je vrijeme izvravanja zanemarljivo, tim prije to se
virtualne funkcije ne mogu umetati u prevedeni kd (kao inline funkcije), nego se uvijek pozivaju (i
to uz dvije dopunske indirekcije). Ovo je, pored nekih drugih manje bitnih razloga, glavni razlog zbog
kojeg se sve metode ne tretiraju automatski kao virtualne, kao to je sluaj u isto objektno orjentiranim
jezicima, kao to je recimo Java. Drugi razlog je to je mogue konstruisati takve primjere nasljeivanja
u kojima se objekti izvedene klase u nekim kontekstima nee ispravno ponaati ukoliko su metode
bazne klase deklarirane kao virtualne, mada svi takvi primjeri koriste neregularno nasljeivanje koje
svakako treba izbjegavati po svaku cijenu.

14

You might also like