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_a
Akademska godina 2013/14

Predavanje 13_a
Kada smo govorili o filozofiji objektno orijentiranog programiranja, rekli smo da su njena etiri
osnovna naela sakrivanje informacija, enkapsulacija, nasljeivanje i polimorfizam. Sa sakrivanjem
informacija i enkapsulacijom smo se ve detaljno upoznali. Sada je vrijeme da se upoznamo i sa
preostala dva naela, nasljeivanjem i polimorfizmom. Da bi program bio objektno orjentiran (a ne
samo objektno zasnovan), u njemu se moraju koristiti i ova dva naela. Tek kada programer ovlada i
ovim naelima i pone ih upotrebljavati u svojim programima, moe rei da je usvojio koncepte objektno
orijentiranog programiranja.
Nasljeivanje (engl. inheritance) je metodologija koja omoguava definiranje klasa koje preuzimaju
veinu svojstava nekih ve postojeih klasa i koje se mogu koristiti u svim kontekstima u kojma se mogu
koristiti te postojee klase. Na primjer, ukoliko definiramo da je klasa B nasljeena iz klase A, tada
klasa B automatski preuzima sve atribute i metode koje je posjedovala i klasa A, pri emu je mogue
u klasi B dodati nove atribute i metode koje klasa A nije posjedovala, kao i promijeniti definiciju
neke od nasljeenih metoda. Pored toga, primjerci klase B moi e se koristiti na bilo kojem mjestu
gdje bi se mogli koristiti i primjerci klase A (to je neto to ne bismo mogli postii da smo prosto
prepisali definiciju klase A u klasu B i izvrili navedene dopune i modifikacije). Klasa A tada se
naziva bazna, osnovna ili roditeljska klasa (engl. base class, parent class) za klasu B, a za klasu B
kaemo da je izvedena ili nasljeena iz klase A (engl. derived class, inherited class).
Veoma je vano ispravno shvatiti u kojim sluajevima treba koristiti nasljeivanje. Naime, cijeli
mehanizam nasljeivanja zamiljen je tako da se neka klasa (recimo klasa B) treba nasljediti iz neke
druge klase (recimo klasa A) jedino u sluaju kada se svaki primjerak izvedene klase moe shvatiti kao
specijalan sluaj primjeraka bazne klase, pri emu eventualni dodatni atributi i metode izvedene klase
opisuju specifinosti primjeraka izvedene klase u odnosu na primjerke bazne klase. Na primjer, neka
imamo klase Student i DiplomiraniStudent koje redom predstavljaju nekog studenta (bilo
kakvog) i nekog diplomiranog studenta (tj. studenta II ili III ciklusa studija). injenica je da e klasa
DiplomiraniStudent sigurno sadravati neke atribute i metode koje klasa Student ne sadri
(npr. godinu diplomiranja, temu diplomskog rada, ocjenu sa odbrane diplomskog rada, itd.). Meutim,
neosporna je injenica da svaki diplomirani student jeste ujedno i student, iz ega slijedi da sve to ima
smisla da se radi sa primjercima klase Student ima smisla da se radi i sa primjercima klase
DiplomiraniStudent. Stoga ima smisla klasu DiplomiraniStudent definirati kao nasljeenu
klasu iz klase Student. Dakle, klasa Student e modelirati ma kakvog studenta (bez specifikacije
da li se radi o studentu koji je diplomirao ili ne), dok e klasa DiplomiraniStudent modelirati
samo one studente koji su diplomirali.
Nasljeivanjem klase B iz klase A mi zapravo govorimo svi primjerci klase B jesu ujedno i
primjerci klase A (meutim, svi primjerci klase B nisu nuno i primjerci klase A). Na primjer,
nasljeivanjem klase Kardiolog iz klase Ljekar mi govorimo svi kardiolozi jesu ljekari, ali svi
ljekari nisu nuno kardiolozi. Generalno, prije nego to se odluimo da neku klasu B definiramo kao
izvedenu klasu iz klase A, trebamo sebi postaviti pitanje da li se svaki primjerak klase B moe
ujedno shvatiti kao primjerak klase A, kao i da li se primjerci klase B uvijek mogu koristiti u istom
kontekstu u kojem i primjerci klase A. Ukoliko su odgovori na oba pitanja potvrdni, svakako treba
koristiti nasljeivanje. Ukoliko je odgovor na prvo pitanje odrean, nasljeivanje ne treba koristiti, s
obzirom da emo tada od nasljeivanja imati daleko vie tete nego koristi. U sluaju da je odgovor na
prvo pitanje potvrdan, a na drugo odrean, nasljeivanje ponekad moe biti, ali najee nije dobro
rjeenje, to moe zavisiti od situacije. O tome emo detaljnije govoriti neto kasnije. Inae, ukoliko se
ipak izvede nasljeivanje, a uvjeti za njegovu ispravnu primjenu nisu zadovoljeni, govorimo o
neregularnom ili nepravilnom nasljeivanju (engl. improper inheritance).
Nasljeivanje nipoto ne treba koristiti samo zbog toga to neka klasa posjeduje sve atribute i
metode koje posjeduje i neka druga klasa. Na primjer, pretpostavimo da elimo da napravimo klase
nazvane Vektor2d i Vektor3d koje predstavljaju respektivno dvodimenzionalni odnosno
trodimenzionalni vektor. Oigledno e klasa Vektor3d sadravati sve atribute i metode kao i klasa
Vektor2d (i jednu koordinatu vie), tako da na prvi pogled djeluje prirodno koristiti nasljeivanje i

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

definirati klasu Vektor3d kao izvedenu klasu iz klase Vektor2d. Meutim, injenica je da svaki
trodimenzionalni vektor nije ujedno i dvodimenzionalni vektor, tako da u ovom primjeru nije nimalo
uputno koristiti nasljeivanje. Naravno, sam jezik nam ne brani da to uradimo (kompajler ne ulazi u
filozofske diskusije tipa da li je neto to ste vi uradili logino ili nije, nego samo da li je sintaksno
doputeno ili nije). Meutim, ukoliko bismo uradili tako neto (tj. izveli neregularno nasljeivanje),
mogli bismo se uvaliti u nevolje. Naime, jezik C++ doputa da se primjerci naslijeene klase koriste u
svim kontekstima u kojem se mogu koristiti i primjerci bazne klase. To znai da bi se primjerci klase
Vektor3d mogli koristiti svugdje gdje i primjerci klase Vektor2d. Jasno je da to ne mora biti
opravdano, s obzirom da trodimenzionalni vektori nisu dvodimenzionalni vektori, tako da je sasvim
mogue zamisliti operacije koje su definirane za dvodimenzionalne vektore a nisu za trodimenzionalne
vektore. Stoga je najbolje definirati klase Vektor2d i Vektor3d posve neovisno jednu od druge,
bez koritenja ikakvog nasljeivanja. Moda djeluje pomalo apsurdno, ali ukoliko ba elimo da
koristimo nasljeivanje, tada je bolje klasu Vektor2d naslijediti iz klase Vektor3d! Zaista,
dvodimenzionalni vektori jesu specijalni sluaj trodimenzionalnih vektora i mogu se koristiti u svim
kontekstima gdje i trodimenzionalni vektori. Na prvi pogled djeluje dosta neobino da je ovdje
nasljeivanje opravdano, s obzirom da dvodimenzionalni vektori imaju jednu koordinatu manje.
Meutim, istu stvar moemo posmatrati i tako da smatramo da dvodimenzionalni vektori takoer sadre
tri koordinate, ali da im je trea koordinata uvijek jednaka nuli! Ipak, neovisna realizacija klasa
Vektor2d i Vektor3d je vjerovatno najbolje rjeenje.
U nekim iznimnim sluajevima, neregularno nasljeivanje moe imati svoje primjene (uglavnom za
realizaciju nekih prljavih trikova), ali u veini sluajevima neregularno nasljeivanje ukazuje na
izrazito lo dizajn. Naalost, primjeri neregularnog nasljeivanja nerijetko se mogu vidjeti i u literaturi.
Recimo, u jednom udbeniku nalazi se primjer nasljeivanja u kojem je klasa Krug naslijeena iz
klase Tacka. Ovakvo (neregularno) nasljeivanje bilo je motivirano injenicom da je klasa Krug
imala sve iste atribute i metode kao i klasa Tacka (sa identinom implementacijom), jedino je klasa
Krug sadravala i neke nove atribute (poluprenik kruga) i metode. Meutim, takvim nasljeivanjem,
mi govorimo da su krugovi specijalan sluaj taaka (to oito nije tano) i omoguavamo da se objekti
tipa Krug koriste u svim kontekstima u kojima se mogu koristiti objekti tipa Tacka (to svakako
nema smisla), pri emu emo kasnije vidjeti da bi se u takvim kontekstima krug reducirao samo na svoj
centar. tavie, prije bi se taka mogla posmatrati kao specijalan sluak kruga (sa poluprenikom
jednakim nuli) nego obrnuto.
Tipina greka u razmiljanju nastaje kada programer pokuava da relaciju sadri izvede preko
nasljeivanja, odnosno da nasljeivanje izvede samo zbog toga to neka klasa posjeduje sve to
posjeduje i neka ve postojea klasa. Na primjer, ukoliko utvrdimo da neka klasa B konceptualno treba
da sadri klasu A, velika greka u pristupu je takav odnos izraziti tako to e klasa B naslijediti klasu
A. ak vrijedi obnuto, odnosno to je praktino siguran znak da klasa B ne treba da bude naslijeena
iz klase A. Umjesto toga, klasa B treba da sadri atribut koji je tipa A. Na primjer, klasa Datum
sigurno e imati atribute koji uvaju pripadni dan, mjesec i godinu. Klasa Student mogla bi imati te
iste atribute (koji bi mogli uvati dan, mjesec i godinu roenja studenta), i iste metode koje omoguavaju
pristup tim atributima, ali to definitivno ne znai da klasu Student treba naslijediti iz klase Datum
(s obzirom da student nije datum). Pravo rjeenje je u klasi Student umjesto posebnih atributa za
dan, mjesec i godinu roenja koristiti jedan atribut tipa Datum koji opisuje datum roenja. Na taj nain
postiemo da klasa Student sadri klasu Datum. Ovakav tip odnosa izmeu dvije klase, u kojem
jedna klasa sadri drugu, a koji smo ve ranije koristili, naziva se agregacija i sutinski se razlikuje od
nasljeivanja. Ili, u maloas pomenutom primjeru neregularnog nasljeivanja klase Krug iz klase
Tacka, prirodno rjeenje bi bilo da se umjesto nasljeivanja koristi agregacija, pri emu bi klasa
Krug trebala kao jedan od svojih atributa imati atribut tipa Tacka (koji bi predstavljao centar
kruga), dok bi se detalji koje ima krug a nema taka izveli kao dodatni atributi. Dakle, treba zapamtiti da
nasljeivanje nije bilo kakvo proirivanje postojee klase, ve iskljuivo proirivanje koje predstavlja
specijalizaciju postojee klase.
Nakon to smo objasnili kad treba, a kad ne treba koristiti nasljeivanje, moemo rei kako se ono
ostvaruje, i ta se njim postie. Pretpostavimo da imamo klasu Student, u kojoj emo, radi
jednostavnosti, definirati samo atribute koji definiraju ime studenta (sa prezimenom) i broj indeksa,
konstruktor sa dva parametra (ime i broj indeksa) kao i neke posve elementarne metode:

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

class Student {
std::string ime;
int indeks;
public:
Student(std::string ime, int ind) : ime(ime), indeks(ind) {}
std::string DajIme() const { return ime; }
int DajIndeks() const { return indeks; }
void Ispisi() const {
std::cout << "Student " << ime << " ima indeks " << indeks;
}
};

Ukoliko elimo da definiramo klasu DiplomiraniStudent koja je nasljeena iz klase


Student a posjeduje novi atribut koji predstavlja godinu diplomiranja i odgovarajuu metodu za
pristup tom atributu, to moemo uraditi na sljedei nain:
class DiplomiraniStudent : public Student {
int godina diplomiranja;
public:
DiplomiraniStudent(string ime, int ind, int god dipl) :
Student(ime, ind), godina diplomiranja(god dipl) {}
int DajGodinuDiplomiranja() const { return godina diplomiranja; }
};

Nasljeivanje se u jeziku C++ realizira putem mehanizma koji se naziva javno izvoenje (engl.
public derivation), koji se postie tako to u deklaraciji klase iza naziva klase stavimo dvotaku iza koje
slijedi kljuna rije public i ime bazne klase. U izvedenoj klasi treba deklarirati samo atribute ili
metode koje dodajemo (ili metode koje mijenjamo, to emo uskoro demonstrirati). Svi ostali atributi i
metode prosto se preuzimaju (nasljeuju) iz bazne klase. Meutim, ovdje postoji jedan vaan izuzetak:
konstruktori se nikada ne nasljeuju. Ukoliko je bazna klasa posjedovala konstruktore, izvedena klasa ih
mora ponovo definirati. Razlog za ovo je injenica da su konstruktori namijenjeni da inicijaliziraju
elemente objekta, a objekti nasljeene klase gotovo uvijek imaju dodatne atribute koje konstruktori
bazne klase ne mogu da inicijaliziraju (s obzirom da ne postoje u baznoj klasi). Ukoliko zaboravimo
definirati konstruktor u izvedenoj klasi, tada e u sluaju da bazna klasa posjeduje konstruktor bez
parametara, taj konstruktor biti iskoriten da inicijalizira one atribute koji postoje u baznoj klasi (to je
jedna od podrazumijevanih akcija koju e obaviti automatski generirani konstruktor u izvedenoj klasi),
dok e novododani atributi ostati neinicijalizirani. U svim ostalim sluajevima kompajler e prijaviti
greku. U navedenom primjeru, u klasi DiplomiraniStudent definiran je konstruktor sa tri
parametra, koji pored imena i broja indeksa zahtijeva da zadamo i godinu diplomiranja studenta.
Konstruktor izvedene klase gotovo uvijek treba da odradi sve to je radio i konstruktor bazne klase,
a nakon toga da odradi akcije specifine za izvedenu klasu. Stoga konstruktor izvedene klase gotovo po
pravilu mora pozvati konstruktor bazne klase (ovo pozivanje se moe izostaviti jedino ukoliko bazna
klasa posjeduje konstruktor bez parametara, koji e tada automatski biti pozvan iz konstruktora izvedene
klase). Poziv konstruktora bazne klase izvodi se iskljuivo u konstruktorskoj inicijalizacijskoj listi, tako
to se navede ime bazne klase i u zagradama parametri koje treba proslijediti konstruktoru bazne klase.
Tako, u navedenom primjeru, konstruktor klase DiplomiraniStudent poziva konstruktor klase
Student da inicijalizira atribute ime i indeks, a pored toga (takoer u konstruktorskoj
inicijalizacijskoj listi) inicijalizira i svoj specifini atribut godina_diplomiranja. Napomenimo da
ovo pravilo vrijedi i za konstruktor kopije. Ukoliko bazna klasa ima definiran konstruktor kopije, mora
ga imati i izvedena klasa, pri emu e konstruktor kopije izvedene klase obavezno pozvati konstruktor
kopije bazne klase i jo eventualno odraditi ono to je potrebno da bi se dobila korektna kopija objekta
izvedene klase.
Vano je naglasiti da atributi i metode koji su privatni u baznoj klasi nisu dostupni ak ni metodama
klase koja je iz nje nasljeena. Drugim rijeima, metode klase DiplomiraniStudent nemaju
direktan pristup atributima ime i indeks, iako ih ova klasa sadri. Da nije tako, zlonamjerni
programer bi veoma jednostavno mogao dobiti pristup privatnim elementima neke klase tako to bi
prosto definirao novu klasu koja nasljeuje tu klasu, nakon ega bi koristei metode nasljeene klase
mogao pristupati privatnim elementima bazne klase!
3

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

injenica da privatni elementi bazne klase nisu dostupni metodama izvedene klase esto moe da
predstavlja ogranienje. Zbog toga je pored prava pristupa koji se definiraju pomou kljunih rijei
private (koja oznaava elemente koji su dostupni samo funkcijama lanicama klase u kojima su
definirani, i funkcijama i klasama koje su deklarirane kao prijatelji te klase) i public (koja oznaava
elemente koji su dostupni u itavom programu) uveden i trei tip prava pristupa, koji se po pravima
nalazi negdje izmeu ova dva tipa. Ovaj tip prava pristupa naziva se zatieni pristup, a definira se
pomou kljune rijei protected. Metode svih klasa naslijeenih iz neke klase mogu pristupiti onim
atributima i metodama objekta nad kojim su pozvani koji imaju zatieno pravo pristupa (tj. ija su prava
pristupa oznaena sa protected), to ne bi bilo mogue kada bi oni imali privatno pravo pristupa. U
svim ostalim aspektima, atributi i metode sa zatienim pravom pristupa ponaaju se kao da imaju
privatno pravo pristupa. Stoga, ukoliko bismo atributima ime i indeks dali zatieno pravo
pristupa, sve metode klase DiplomiraniStudent (ukljuujui i konstruktor) mogle bi im u sluaju
potrebe pristupiti, dok bi pristup i dalje bio onemoguen u ostalim dijelovima programa. Slijedi
modificirana verzija klase Student, u kojoj atributi ime i indeks imaju zatieno pravo pristupa:
class Student {
protected:
std::string ime;
int indeks;
public:
... // Javni dio klase ostaje isti
};

esto se moe uti prilino neprecizno tumaenje prema kojem su atributi i metode sa zatienim
pravom pristupa dostupni metodama svih klasa nasljeenih iz klase u kojoj su definirani. Meutim,
ukoliko paljivo razmotrimo ta zapravo znai zatieno pravo pristupa, vidjeemo da je ovo tumaenje
samo djelimino tano. Na primjer, sve metode ma koje klase naslijeene iz klase Student (recimo
klase DiplomiraniStudent) zaista e moi pristupiti atributima ime i indeks koji se odnose
na onaj objekat nad kojima su pozvane (tj. na onaj objekat na koji pokazuje pokaziva this), ali nee
moi pristupiti atributima ime i indeks koji se odnose na neki drugi objekat tipa Student (ili
tipa neke klase naslijeene iz klase Student) koji nije onaj objekat nad kojim je metoda pozvana. Na
primjer, niti jedna metoda klase DiplomiraniStudent ne moe pristupiti atributima ime i
indeks neke promjenljive s tipa Student definirane negdje drugdje, bez obzira to oni imaju
zatiena prava pristupa a ta metoda pripada klasi naslijeenoj iz klase Student. To vrijedi ak i
ukoliko je s lokalna promjenljiva tipa Student deklarirana unutar te metode. Mogli bismo rei da
metode klasa naslijeenih iz neke klase A imaju pravo pristupa samo svojim naslijeenim atributima i
metodama sa zatienim pravima pristupa, ali ne i atributima i metodama sa zatienim pravima pristupa
drugih objekata tipa A. Mada ovo pravilo djeluje pomalo neobino i isuvie restriktivno, postoje jaki
razlozi zato je tako. Zaista, pretpostavimo da nije tako, i da je a neki objekat tipa A koji posjeduje
zatieni atribut x. Tada bi zlonamjerni programer mogao bez problema pristupiti atributu x objekta
a tako to bi kreirao neku klasu naslijeenu iz klase A i u njoj definirao neku metodu koja bi
direktno pristupila atributu x objekta a (bez prethodno opisanih restrikcija, to bi bilo mogue).
Drugim rijeima, bez ovakvih restrikcija, mehanizam zatite bi se mogao veoma lako zaobii, tako da
zatita u sutini ne bi bila nikakva zatita!
U nastavku emo podrazumijevati da je atributima ime i indeks dato zatieno pravo pristupa
(mada mnoge preporuke govore da zatieno pravo pristupa treba koristiti sa velikom rezervom i da je
bolje drati to je god mogue vie stvari u privatnom dijelu klase ukoliko je ikako mogue). Ipak, bez
obzira na ova neto slobodnija prava pristupa, bitno je naglasiti da se u konstruktorskoj inicijalizacionoj
listi mogu inicijalizirati samo atributi koji su neposredno deklarirani u toj klasi, bez obzira na prava
pristupa atributa u baznoj klasi.
Nad objektima nasljeene klase mogu se koristiti sve metode kao i nad objektima bazne klase. Tako
su sljedee konstrukcije sasvim korektne:
Student s1("Paja Patak", 1234);
DiplomiraniStudent s2("Miki Maus", 3412, 2004);
s1.Ispisi(); std::cout << std::endl; s2.Ispisi();

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

Ovaj primjer dovee do sljedeeg ispisa:


Student Paja Patak ima indeks 1234
Student Miki Maus ima indeks 3412
S druge strane, esto se javlja potreba da se u nasljeenoj klasi promijene definicije nekih metoda koje
su definirane u baznoj klasi. Na primjer, u klasi DiplomiraniStudent ima smisla promijeniti
definiciju metode Ispisi tako da ona uzme u obzir i godinu diplomiranja studenta. Izmijenjena klasa
mogla bi izgledati ovako:
class DiplomiraniStudent : public Student {
... // Sve ostalo ostaje isto
void Ispisi() const {
std::cout << "Student " << ime << ", diplomirao "
<< godina diplomiranja << ". godine, ima indeks " << indeks;
}
};

Primijetimo da ovakva izmjena ne bi bila legalna da atributima ime i indeks nije dato
zatieno pravo pristupa (u suprotnom bismo morali koristiti metode DajIme i DajIndeks da
pristupimo ovim atributima). Uz prikazanu izmjenu, prethodni primjer doveo bi do sljedeeg ispisa:
Student Paja Patak ima indeks 1234
Student Miki Maus, diplomirao 2004. godine, ima indeks 3412
Drugim rijeima, nad objektom s2, koji je tipa DiplomiraniStudent, poziva se njegova vlastita
metoda Ispisi, a ne metoda nasljeena iz klase Student. Nasljeena verzija metode Ispisi i
dalje je dostupna u klasi DiplomiraniStudent, ali ukoliko iz bilo kojeg razloga elimo pozvati
upravo nju, tu elju moramo eksplicitno naznaiti pomou operatora ::. Tako, ukoliko bismo nad
objektom s2 eljeli da pozovemo metodu Ispisi nasljeenu iz klase Student a ne istoimenu
metodu definiranu u klasi DiplomiraniStudent, trebali bismo pisati
s2.Student::Ispisi();

Na isti nain je mogue u nekoj od metoda koje modificiramo u nasljeenoj klasi pozvati istoimenu
metodu nasljeenu iz bazne klase. Na primjer, u sljedeoj definiciji klase DiplomiraniStudent
metoda Ispisi poziva istoimenu metodu nasljeenu iz njene bazne klase:
class DiplomiraniStudent : public Student {
... // Sve ostalo ostaje isto
void Ispisi() const {
Student::Ispisi();
std::cout << ", a diplomirao je " << godina diplomiranja
<< ". godine";
}
};

Sada bi ranije navedeni primjer upotrebe ovih klasa doveo do sljedeeg ispisa:
Student Paja Patak ima indeks 1234
Student Miki Maus ima indeks 3412, a diplomirao je 2004. godine
Treba napomenuti da je odrednica Student:: ispred poziva metode Ispisi veoma bitna, jer bi
bez nje kompajler shvatio da metoda Ispisi poziva samu sebe, to bi bilo protumaeno kao
beskonana rekurzija (tj. rekurzija bez izlaza).
Vano je znati da ukoliko u izvedenoj klasi definiramo neku funkciju istog imena kao u baznoj klasi,
ta funkcija e zasjeniti funkcije istog imena u baznoj klasi ak i ukoliko one imaju drugaiji broj i tip
parametara. Zbog toga se ovdje ne govori o preklapanju (engl. overloading) nego o preglasavanju
(engl. overriding). U pitanju nije doupnjavanje funkcionalnosti postojee funkcije u baznoj klasi nego o
5

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

njenoj zamjeni posve novom funkcionalnou u izvedenoj klasi. Na primjer, pogledajmo sljedei primjer
nasljeivanja, u kojoj izvedena klasa definira funkciju istog imena koja postoji u baznoj klasi, ali koja
prima drugaiji tip parametara (ovdje smo koristili kljunu rije struct umjesto class da ne
bismo morali pisati public:, jer u ovim klasama svakako nema nita privatno):
struct
void
void
}
struct
void
}

BaznaKlasa {
Metoda(int x) { std::cout << "Parametar tipa int"; }
Metoda(std::string s) { std::cout << "Parametar tipa string"; }
IzvedenaKlasa : public BaznaKlasa {
Metoda(double x) { std::cout << "Parametar tipa double"; }

Ukoliko je sad recimo izvedeni neki primjerak klase IzvedenaKlasa i izvrimo poziv
poput izvedeni.Metoda(5), mnogi pogreno oekuju da e se pozvati nasljeena metoda iz klase
BaznaKlasa koja prima parametar tipa int. Umjesto toga, bie pozvana metoda iz klase
IzvedenaKlasa koja prima parametar tipa double, osim ukoliko eksplicitno ne specificiramo da
mislimo na metodu iz bazne klase konstrukcijom poput izvedeni.BaznaKlasa::Metoda(5). Iz
istog razloga, poziv poput izvedeni.Metoda("Test") uope nee biti prepoznat, bez ekplicitne
specifikacije da se misli na metodu Metoda iz bazne klase. Ukoliko elimo da zadrimo funkcionalnost
neke postojee metode iz bazne klase i u izvedenoj klasi, ali da je pri tome proirimo novim znaenjima
za neke druge tipove podataka (tj. ukoliko elimo izvesti klasino preklapanje), u izvedenoj klasi
moramo navesti deklaraciju poput using BaznaKlasa::Metoda. Ovim govorimo da preuzimamo
definicije metoda nazvanih Metoda iz bazne klase, ali dajemo sebi pravo i da ih eventualno preklopimo
sa istoimenim funkcijama sa razliitim brojem ili tipom parametara.
Ve smo rekli da se konstruktori ne nasljeuju, nego da izvedena klasa uvijek mora definirati svoje
konstruktore (ukljuujui i konstruktor kopije, ukoliko ga je bazna klasa definirala). Zbog toga se ne
nasljeuju ni automatske pretvorbe tipova koje se ostvaruju eventualnim konstruktorima sa jednim
parametrom (koji nisu oznaeni sa explicit), nego ih nasljeena klasa mora ponovo definirati
ukoliko eli da zadri mogunost automatske pretvorbe u objekte izvedene klase. Pored konstruktora,
jedina svojstva bazne klase koja se ne nasljeuju su operatorske funkcije za operator dodjele = i
deklaracije prijateljstva. Operatori dodjele se ne nasljeuju zbog toga to je njegovo ponaanje obino
tijesno vezano uz konstruktor kopije, koji se ne nasljeuje. Dalje, deklaracije prijateljstva se ne
nasljeuju iz prostog razloga to bi njihovo automatsko nasljeivanje omoguilo razne zloupotrebe.
Stoga, ukoliko je neka funkcija f deklarirana kao prijatelj klase A, ona nije ujedno i prijatelj klase
B nasljeene iz klase A. Ovdje ne treba krivo pomisliti da se prijateljske funkcije ne nasljeuju. One
se nasljeuju, u smislu da su prijateljske funkcije bazne klase sasvim dostupne i svim objektima ma koje
klase naslijeene iz nje, ali te funkcije nisu prijatelji naslijeene klase (tj. nemaju pravo pristupa njenim
privatnim atributima). Ukoliko elimo da zadrimo prijateljstvo i u nasljeenoj klasi, funkciju f
ponovo treba proglasiti prijateljem klase unutar deklaracije klase B. Osim konstruktora, operatora
dodjele i relacije prijateljstva, svi ostali elementi bazne klase nasljeuju se u izvedenoj klasi (ukljuujui
i sve druge operatorske funkcije osim za operator dodjele).
Sasvim je mogue da vie klasa bude naslijeeno iz iste bazne klase. Pored toga, moemo imati i
itav lanac nasljeivanja. Na primjer, klasa C moe biti naslijeena iz klase B, koja je opet
naslijeena iz klase A. Konano, mogue je da neka klasa naslijedi vie klasa, tj. da bude naslijeena iz
vie od jedne bazne klase. Na primjer, sljedea konstrukcija
class C : public A, public B {
...
};

deklarira klasu C koja je naslijeena iz baznih klasa A i B. U ovom sluaju se radi o tzv.
viestrukom nasljeivanju. Mada viestruko nasljeivanje u nekim specifinim primjenama moe biti
korisno, ono sa sobom vue mnoge nedoumice koje se moraju posebno razmotriti (npr. ta se deava
ukoliko vie od jedne bazne klase imaju neke atribute ili metode istih imena, zatim ta se deava ukoliko
su bazne klase takoer izvedene klase, ali koje su izvedene iz jedne te iste bazne klase, itd.). U doba

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

kada jezik C++ nije posjedovao generike klase, viestruko nasljeivanje se intenzivno koristilo za
postizanje nekih funkcionalnosti, koje se danas mnogo prirodnije rjeavaju uz pomo generikih klasa.
Stoga se viestruko nasljeivanje danas znatno manje koristi (iako jo uvijek ima situacija u kojima je
ono neophodno), tako da se njegovom opisivanju neemo zadravati, tim prije to nisu rijetka miljenja
da viestruko nasljeivanje esto unosi vie zbrke nego to donosi koristi.
Ve je reeno da se svaki primjerak naslijeene klase moe koristiti u kontekstima u kojima se moe
koristiti primjerak bazne klase. Tako se objekat bazne klase moe inicijalizirati objektom naslijeene
klase, zatim moe se izvriti dodjela objekta naslijeene klase objektu bazne klase i, konano, funkcija
koja prihvata kao parametar ili vraa kao rezultat objekat bazne klase moe prihvatiti kao parametar ili
vratiti kao rezultat objekat naslijeene klase. Ovo je dozvoljeno zbog toga to svaki objekat naslijeene
klase sadri sve elemente koje sadre i objekti bazne klase. Meutim, da bi ovakva mogunost imala
smisla, veoma je vano da se svaki primjerak naslijeene klase moe ujedno shvatiti i kao primjerak
bazne klase, to smo ve ranije naglasili. U suprotnom bismo mogli doi u potpuno neprirodne situacije.
Na primjer, ukoliko bismo klasu Student naslijedili iz klase Datum, tada bi svaka funkcija koja
prima objekat klase Datum prihvatala i objekat tipa Student kao parametar, to teko da moe
imati smisla. Jo je bitno naglasiti da se u svim navedenim situacijama sve specifinosti izvedene klase
gube. Tako, ukoliko promjenljivu s2 koja je tipa DiplomiraniStudent poaljemo kao parametar
nekoj funkciji koja kao parametar prima objekat tipa Student, podaci o godini diplomiranja iz
promjenljive s2 bie ignorirani. Takoer, ukoliko izvrimo dodjelu poput s1 = s2 gdje je s1
promjenljiva tipa Student, u promjenljivu s1 se kopiraju samo ime i broj indeksa iz promjenljive
s2, dok se informacija o godini diplomiranja ignorira (s obzirom da promjenljiva s1, koja je tipa
Student, nije u stanju da ovu informaciju prihvati). Ovakvo ponaanje naziva se odsjecanje (engl.
slicing) i nije uvijek poeljno. Uskoro emo vidjeti kako ovo ponaanje moemo izbjei.
Veoma je vano shvatiti da je odnos izmeu bazne i naslijeene klase strogo jednosmjeran, tako da
se u opem sluaju objekti bazne klase ne mogu koristiti u kontekstima u kojima se mogu koristiti
objekti naslijeene klase, s obzirom da se, openito posmatrano, objekti bazne klase ne mogu posmatrati
kao objekti izvedene klase. Na primjer, svaki diplomirani student jeste student, ali svaki student nije
diplomirani student. Stoga dodjela poput s2 = s1 gdje su s1 i s2 promjenljive iz prethodnog
primjera nije legalna, iako dodjela s1 = s2 jeste. Ovo je sasvim razumljivo, jer promjenljiva s2 ima
dodatni atribut koji promjenljiva s1 nema (godinu diplomiranja), pa je nejasno ta bi trebalo dodijeliti
atributu godina diplomiranja objekta s2. Zbog istog razloga, bilo koja funkcija koja kao
parametar prima objekat tipa DiplomiraniStudent ne moe primiti kao parametar objekat tipa
Student, s obzirom da takva funkcija moe definirati specifine radnje koje nisu mogue sa obinim
studentima. Objekti bazne klase se mogu koristiti u kontekstima u kojima se koriste objekti izvedene
klase jedino u sluaju da izvedena klasa posjeduje neeksplicitni konstruktor sa jednim parametrom koji
je tipa bazne klase, koji omoguava pretvorbu tipa bazne klase u objekat izvedene klase (ili ukoliko
bazna klasa posjeduje operator konverzije u tip izvedene klase). U tom sluaju, takav konstruktor (ili
operator konverzije) e nedvosmisleno odrediti kako treba tretirati atribute izvedene klase koji ne postoje
u baznoj klasi.
Ranije smo rekli da se nasljeivanje u jeziku C++ ostvaruje pomou mehanizma javnog izvoenja.
Pored javnog izvoenja, jezik C++ poznaje jo i privatno izvoenje (engl. private derivation) kao i
zatieno izvoenje (engl. protected derivation), koji se realiziraju na slian nain kao i javno izvoenje,
samo to se ispred imena bazne klase a iza dvotake umjesto kljune rijei public navode kljune
rijei private odnosno protected. I dok kod javnog izvoenja elementi osnovne klase koji su
imali javno pravo pristupa zadravaju to pravo pristupa i u izvedenoj klasi, kod privatnog odnosno
zatienog izvoenja elementi osnovne klase koji su imali javno pravo pristupa u izvedenoj klasi e
imati privatno odnosno zatieno pravo pristupa. To zapravo znai da izvedena klasa nee imati isti
interfejs kao i bazna klasa, odnosno interfejs izvedene klase e initi samo one metode koje su
eksplicitno definirane kao metode sa javnim pristupom unutar izvedene klase (koje, naravno, mogu
pozivati neke od metoda bazne klase). Alternativno je mogue pomou kljune rijei using
specificirati da neka od metoda koja je inila interfejs bazne klase treba (neizmijenjena) da bude
sadrana i u interfejsu privatno izvedene klase. Na primjer, ukoliko klasa A sadri metodu
NekaMetoda u svom interfejsu, i ukoliko je klasa B privatno izvedena iz klase A, tada
deklaracijom using A::NekaMetoda unutar interfejsa klase B naglaavamo da metoda
NekaMetoda treba da ini i interfejs klase B.
7

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

Iz izloenog neposredno slijedi da se privatno izvedene klase ne mogu smatrati kao specijalni
sluajevi bazne klase, niti se primjerci tako izvedenih klasa mogu koristiti u istim kontekstima kao i
primjerci bazne klase. Stoga je jasno da se privatnim ili zatienim izvoenjem ne realizira nasljeivanje
(neki govore o privatnom odnosno zatienom nasljeivanju, ali bolje je rije nasljeivanje uope ne
koristiti u ovom kontekstu). Primjerci klase koja je privatno odnosno zatieno izvedena iz bazne klase
tretiraju se posve neovisno od primjeraka bazne klase, i nikakvo njihovo meusobno mijeanje nije
dozvoljeno. U sutini, privatno ili zatieno izvoenje najee se koristi kada neka klasa (recimo, klasa
B) dijeli neke izvedbene detalje sa nekom drugom klasom (recimo, klasom A), ali pri emu se klasa
B ne moe tretirati kao specijalan sluaj klase A. U tom sluaju, moe se pritediti na dupliranju
kda ukoliko se klasa B privatno izvede iz klase A. Na primjer, klasa Vektor3d bi se mogla
realizirati kao privatno izvedena iz klase Vektor2d. Meutim, kako privatno nasljeivanje nosi i
svoje komplikacije, vjerovatno je najbolje klase Vektor2d i Vektor3d realizirati kao posve
neovisne klase. U sutini, privatnim i zatienim izvoenjem umjesto nasljeivanja se zapravo realizira
neka vrsta agregacije bazne klase u izvedenu. Preciznije, sve to se moe postii privatnim ili zatienim
nasljeivanjem moe se postii i klasinom agregacijom, samo to se kod privatnog odnosno zatienog
nasljeivanja koristi drugaija sintaksa, analogna sintaksi koja se koristi kod nasljeivanja, to u nekim
sluajevima moe biti dosta praktino. Kako se privatnim i zatienim izvoenjem ne realizira koncept
nasljeivanja, o tim vrstama izvoenja neemo dalje govoriti.
Nasljeivanje dobija svoju punu snagu i primjenu tek u kombinaciji sa pokazivaima i referencama.
Naime, slino kao to se objektu bazne klase moe dodijeliti objekat nasljeene klase (uz neminovan
gubitak specifinosti koje nosi objekat izvedene klase), tako se i pokazivau na objekte bazne klase
moe dodijeliti adresa nekog objekta nasljeene klase ili neki drugi pokaziva na objekat nasljeene
klase (ovo vrijedi samo za nasljeivanje odnosno javno izvoenje prilikom privatnog odnosno
zatienog izvoenja ova konvencija ne vrijedi). Takoer, svaka funkcija koja prima kao parametar ili
vraa kao rezultat pokaziva na objekte bazne klase moe primiti kao parametar ili vratiti kao rezultat
pokaziva na neki objekat izvedene klase (recimo adresu nekog objekta izvedene klase). Drugim
rijeima, podrana je automatska pretvorba pokazivaa na objekte izvedene klase u pokazivae na
obine klase. Takva pretvorba obino se naziva pretvorba navie (engl. upcasting). Na primjer, neka su
s1 i s2 deklarirani kao u prethodnim primjerima, i neka imamo deklaraciju
Student *pok1, *pok2;

Tada su sljedee dodjele sasvim legalne, bez obzira to su oba pokazivaa pok1 i pok2 deklarirani
kao pokazivai na tip Student, a objekat s2 je tipa DiplomiraniStudent:
pok1 = &s1;
pok2 = &s2;

Razumije se da smo isto tako mogli odmah izvriti inicijalizaciju prilikom deklariranja ovih pokazivaa,
tako da bismo isti efekat postigli deklaracijom
Student *pok1(&s1), *pok2(&s2);

Ovdje ipak nije sve tako jednostavno. S obzirom da deklaracija pokazivaa pok2 nije u skladu sa
tipom onoga na ta on pokazuje, javlja se dilema kojeg je tipa izraz *pok2. S obzirom na deklaraciju,
slijedi da bi njegov tip trebao biti Student. Meutim, s obzirom na injenino stanje (tj. na ta on
zaista pokazuje), njegov tip bi trebao biti DiplomiraniStudent. Ova dilema se rjeava tako to
kada se nasljeivanje ukljui u igru, dereferencirani pokazivai mogu imati dva razliita tipa, koji se
nazivaju statiki i dinamiki tip. Statiki tip je odreen na osnovu deklaracije, tako da izraz *pok2
ima statiki tip Student, dok je dinamiki tip odreen injeninim stanjem, tako da isti izraz ima
dinamiki tip DiplomiraniStudent. Statiki tip dereferenciranog pokazivaa ne moe se mijenjati
tokom ivota pokazivaa, za razliku od dinamikog tipa, koji zavisi od onoga na ta pokaziva u tom
trenutku pokazuje, tako da se on moe mijenjati tokom ivota pokazivaa. Za razliku od izraza *pok2,
i statiki i dinamiki tip izraza *pok1 je Student, dakle oni su identini (barem nakon gore
napisanih naredbi). Isto tako se govori i o statikom i dinamikom tipu samih pokazivaa. Tako, u gore
navedenom primjeru, statiki tip pokazivaa pok2 je Student * (tj. pokaziva na objekat tipa
Student), dok je njegov dinamiki tip DiplomiraniStudent * (tj. pokaziva na objekat tipa
DiplomiraniStudent).
8

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

Razmotrimo sada kako e se ponaati ovi pokazivai ukoliko nad objektima na koje oni ukazuju
primijenimo neku metodu pomou operatora ->, na primjer, sljedeom konstrukcijom:
pok1->Ispisi();
std::cout << std::endl;
pok2->Ispisi();

// Isto to i (*pok1).Ispisi();
// Isto to i (*pok2).Ispisi();

Dilema se javlja kod drugog ispisa, jer je nejasno da li e se izraz *pok2 tumaiti kao objekat tipa
Student ili objekat tipa DiplomiraniStudent, tj. da li e se u obzir uzeti statiki ili dinamiki
tip. Mada djeluje prirodnije da se uzme u obzir injenino stanje, vidimo da e se u oba sluaja pozvati
metoda Ispisi iz klase Student, odnosno ovdje se kao relevantan uzima statiki tip:
Ovaj primjer e proizvesti sljedei ispis:
Student Paja Patak ima indeks 1234
Student Miki Maus ima indeks 3412
Ovakvo donekle kontraintuitivno ponaanje posljedica je injenice da je kompajler odluku o tome koju
metodu treba pozvati donio na osnovu toga kako su deklarirani pokazivai pok1 i pok2, a ne na
osnovu toga na ta oni zaista pokazuju (tj. na osnovu njihovog statikog tipa). Takva strategija naziva se
rano povezivanje (engl. early binding), s obzirom da kompajler povezuje odgovarajui pokaziva sa
odgovarajuom metodom samo na osnovu njegove deklaracije, to se moe izvriti jo u fazi prevoenja
programa, prije nego to se program pone izvravati. Sreom, uskoro emo vidjeti da ovo nije i jedina
mogua strategija.
Posljedica ranog povezivanja je da ponovo imamo situaciju u kojoj su sve informacije o specifinosti
objekata tipa DiplomiraniStudent izgubljene kada je pokazivau na tip Student dodijeljena
adresa objekta tipa DiplomiraniStudent. Kada se ne koriste pokazivai, jasno je da do gubljenja
specifinosti mora doi, jer objekat bazne klase ne moe da prihvati sve specifinosti objekta izvedene
klase (koji sadri vie informacija). S druge strane, takoer je jasno da u sluaju kada se koriste
pokazivai, do ovakvog gubljenja specifinosti ne bi moralo doi. Zaista, bez obzira to je pok2
pokaziva na tip Student, on je usmjeren da pokazuje na objekat tipa DiplomiraniStudent, koji
postoji i ve se nalazi u memoriji, tako da ne postoji nikakav gubitak informacija. Meutim, da ne bi
dolo do gubljenja specifinosti izvedene klase (odnosno da bi poziv metode Ispisi nad objektom na
koji pokazuje pokaziva pok2 ispisao informacije kao da se zaista radi o diplomiranom studentu, a ne
samo ope informacije koje su dostupne za svakog studenta), odluku o tome koja se metoda Ispisi
poziva treba donijeti na osnovu toga na ta pokaziva zaista pokazuje, a ne na osnovu toga kako je on
deklariran (odnosno, na osnovu njegovog dinamikog tipa). Drugim rijeima, odluku o tome koja se
metoda poziva ne treba se donositi unaprijed, u fazi prevoenja programa, nego je treba odgoditi do
samog trenutka poziva metode (u vrijeme izvravanja programa), jer u opem sluaju tek tada moe biti
poznato na ta pokaziva zaista pokazuje (to emo vidjeti u jednom od narednih primjera). Ova
strategija naziva se kasno povezivanje (engl. late binding).
Da bismo ostvarili kasno povezivanje, ispred deklaracije metode na koju elimo da se primjenjuje
kasno povezivanje treba dodati kljunu rije virtual. Metode koje se pozivaju strategijom kasnog
povezivanja nazivaju se virtualne metode, odnosno virtualne funkcije lanice. Drugim rijeima, ukoliko
je funkcija lanica virtualna, odluka o tome koja e se verzija funkcije lanice pozvati (u sluaju kada ih
ima vie) donosi se na osnovu dinamikog, a ne statikog tipa kao to je to sluaj kod obinih funkcija
lanica. Stoga emo metodu Ispisi u klasi Student deklarirati tako da postane virtualna metoda:
class Student {
... // Sve ostalo ostaje isto
virtual void Ispisi() const {
std::cout << "Student " << ime << " ima indeks " << indeks;
}
};

Sa ovakvom izmjenom, indirektni poziv metode Ispisi pomou operatora -> nad pokazivaima
pok1 i pok2 (ili direktni nad objektima *pok1 i *pok2) dovee do eljenog ispisa

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

Student Paja Patak ima indeks 1234


Student Miki Maus ima indeks 3412, a diplomirao je 2004. godine
Bitno je naglasiti da se kljuna rije virtual pie samo unutar deklaracije klase, tako da u sluaju da
virtualnu metodu implementiramo izvan deklaracije klase, kljunu rije virtual ne trebamo (i ne
smijemo) ponavljati.
U naelu, kad god imamo potrebu da u izvedenoj klasi mijenjamo neku funkciju iz bazne klase, to
je gotovo siguran znak da bi ta funkcija u baznoj klasi trebala biti virtualna, jer u suprotnom neemo
imati ponaanje koje je u skladu sa faktikim stanjem. Postoje dodue neki rijetki izuzeci u kojima se
eljeno ponaanje dobija upravo ukoliko funkcija nije virtualna, ali tu se uglavnom radi o sluajevima
zasnovanim na neregularnom nasljeivanju. Razlog zbog koje sve funkcije lanice nisu automatski
virtualne lei u injenici da se pozivanje virtualnih funkcija lanica obavlja sporije nego ukoliko one
nisu virtualne, jer se prilikom svakog poziva troi vrijeme na utvrivanje koja se zaista verzija funkcije
treba pozvati (vidjeemo uskoro zbog ega se to utvrivanje mora vriti prilikom svakog poziva). Stoga,
kada bi svaka funkcija bila virtualna, to bi nepotrebno usporavalo pozivanje ak i onih funkcija za koje
ne planiramo da e ikada biti promijenjene u nekoj od nasljeenih klasa. Radi svega ovoga, da bi se
mehanizam mijenjanja neke funkcije lanice u izvedenoj klasi uinio sigurnijim, u C++11 je uvedena
kljuna rije override. Kada god elimo neku funkciju u izvedenoj klasi izmijeniti, preporuka je da
se izmijenjena funkcija oznai sa ovom kljunom rijei, kao u navedenom primjeru:
class DiplomiraniStudent : public Student {
... // Sve ostalo ostaje isto
void Ispisi() const override {
... // Definicija funkcije ostaje ista
}
};

Dodavanjem kljune rijei postiemo dva zatitna efekta. Prvo, kompajler e nam prijaviti greku
ukoliko odgovarajua funkcija u baznoj klasi ne postoji ili ukoliko nije deklarirana kao virtualna. Drugo,
greku emo dobiti i ukoliko u baznoj klasi postoji takva funkcija, ali nema isti prototip u odnosu na
funkciju koju definiramo u izvedenoj klasi (npr. razlikuje se broj ili tip parametara). Naime, vidjeli smo
ve da u takvim sluajevima dobijamo neeljene situacije koji se teko otkrivaju u kojima novonapisana
funkcija sakriva sve istoimene funkcije iz bazne klase. Ovdje nam kompajler prijavljuje greku na
takve pokuaje, jer to gotovo sigurno nije ono to smo eljeli.
Za reference pri nasljeivanju vrijedi slina logika kao i za pokazivae. Referenca na objekat bazne
klase moe se vezati za objekat naslijeene klase (ovdje se isto radi o pretvorbi navie), to predstavlja
izuzetak od pravila da se reference mogu vezati samo za objekte istog tipa. Naravno, ovo vrijedi i za
formalne parametre funkcija koji su reference (ukljuujui i reference na konstantne objekte). Pri tome,
takoer ne mora doi ni do kakvog gubitka informacija, jer su reference u sutini sintaksno prerueni
pokazivai (tako da i kod referenci moemo razlikovati njihov statiki i dinamiki tip). Stoga,
mehanizam poziva virtualnih funkcija radi i sa referencama (virtualne funkcije se pozivaju na osnovu
dinamikog a ne statikog tipa reference). Pretpostavimo, na primjer, da imamo deklariranu sljedeu
funkciju, iji je formalni parametar referenca na konstantni objekat tipa Student:
void NekaFunkcija(const Student &s) {
s.Ispisi();
}

Pretpostavimo dalje da je s2 objekat tipa DiplomiraniStudent, kao u ranijim primjerima. Tada


je sljedei poziv posve legalan, bez obzira na nepodudarnost tipova formalnog i stvarnog argumenta:
NekaFunkcija(s2);

Ukoliko je metoda Ispisi unutar klase Student deklarirana kao virtualna metoda, kasno
povezivanje e pri tome obezbijediti da iz funkcije NekaFunkcija bude pozvana metoda Ispisi
iz klase DiplomiraniStudent bez obzira to je formalni parametar s deklariran kao referenca na
10

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

objekat tipa Student (njegov dinamiki tip e biti referenca na objekat DiplomiraniStudent).
Do ovoga nee doi ukoliko metoda Ispisi nije virtualna, nego e se uvijek pozvati metoda
Ispisi iz klase Student, bez obzira na tip stvarnog parametra. Takoer, do ovoga nee doi
ukoliko formalni parametar s nije referenca (nego prosto objekat tipa Student), bez obzira da li je
metoda Ispisi virtuelna ili ne. Drugim rijeima, mehanizam virtualnih funkcija i kasnog povezivanja
nikada ne djeluje kada se neki objekat naslijeene klase kopira u drugi objekat bazne klase. Naime, u
tom sluaju se pri pozivu funkcije objekat s2 kopira u parametar s pri emu se, kako smo ve
vidjeli, gube sve specifinosti objekta s2! Stoga se mehanizam virtualnih funkcija tada ne smije
primijeniti (s obzirom da se odgovarajua virtualna metoda iz naslijeene klase skoro sigurno oslanja na
specifinosti naslijeene klase). Moemo rei i ovako: o statikom i dinamikom tipu ima smisla
govoriti samo ukoliko su u igri pokazivai ili reference (mada je mogue simulirati postojanje
dinamikog tipa i kod nekih drugih vrsta objekata, ali ono ne dolazi samo po sebi kao u sluaju
pokazivaa i referenci, ve ga je potrebno implementirati, o emu e biti govora kasnije).
Kasno povezivanje i virtualne metode omoguavaju mnogobrojne interesantne primjene, koje
dobijaju svoj puni smisao tek u kombinaciji sa dinamikom alokacijom memorije. Meutim, prije nego
to nastavimo dalje, moramo nainiti jo jednu izmjenu u klasi Student, koja se sastoji u dodavanju
virtualnog destruktora (u javni dio klase) sa praznim tijelom. Njegova e uloga biti uskoro razjanjena:
class Student {
...
virtual ~Student() {}
...
};

Razmotrimo sada sljedei programski isjeak, koji dodue koristi pametne umjesto obinih pokazivaa,
ali za njih vrijedi posve ista logika vezana za nasljeivanje kao u sluaju obinih pokazivaa. Razlog
zbog kojeg koristimo pametne pokazivae je olakano upravljanje memorijom. Naime, ukoliko bismo
koristili obine pokazivae, dolo bi do curenja memorije u trenutku kada pokaziva s preusmjerimo
da pokazuje na novi dinamki alocirani objekat, osim ukoliko ne bismo prethodno obrisali objekat na
koji je s ranije pokazivao (naredbom poput delete s), ili ukoliko ne bismo imali jo jedan
pokaziva koji pokazuje na isti objekat, i koji bi nakon preusmjeravanja pokazivaa s nastavio da
pokazuje na isti objekat:
std::shared_ptr<Student> s;
s = std::make_shared<Student>("Paja Patak", 1234));
s->Ispisi();
std::cout << std::endl;
s = std::make_shared<DiplomiraniStudent>("Miki Maus", 3412, 2004);
s->Ispisi();

Ovaj primjer dovodi do istog ispisa kao i ranije prikazani primjer sa pokazivakim promjenljivim
pok1 i pok2. Meutim, u ovom primjeru se u oba sluaja metoda Ispisi poziva (indirektno) nad
istom promjenljivom s, pri emu se prvi put ova promjenljiva ponaa poput pokazivaa na objekat tipa
Student, a drugi put poput pokazivaa na objekat tipa DiplomiraniStudent. Promjenljiva s
je zapravo promijenila svoj dinamiki tip. Slino, moemo rei da se izraz *s prvi put ponaa kao
objekat tipa Student, a drugi put kao objekat tipa DiplomiraniStudent, odnosno izraz *s je
takoer promijenio svoj dinamiki tip. Metodologija koja omoguava da se ista promjenljiva u razliitim
trenucima ponaa kao da ima razliite tipove, tako da poziv iste metode (tanije, metode istog imena)
nad istom promjenljivom u razliitim trenucima izaziva razliite akcije naziva se polimorfizam.
Preciznije, ovdje govorimo o tzv. jakom polimorfizmu, s obzirom da se definira i tzv. slabi polimorfizam,
koji prosto podrazumijeva da promjenljive razliitih tipova mogu imati metode istog imena koje rade
razliite stvari, tako da primjena iste metode nad promjenljivim razliitog tipa izaziva razliite akcije. U
nastavku kada budemo govorili o polimorfizmu, misliemo uglavnom na jaki polimorfizam, ukoliko
eksplicitno ne naglasimo drugaije.
U prethodnom sluaju, s je tipian primjer polimorfne promjenljive. U jeziku C++, jedino se
pokazivake promjenljive i reference mogu direktno ponaati kao polimorfne promjenljive. Pri tome,
reference mogu djelovati jo ubjedljivije kao polimorfne promjenljive nego pokazivai, s obzirom da se
11

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

za poziv metoda nad referencom koristi isti operator . kao za poziv metoda nad obinim objektima.
Pored pokazivaa i referenci, polimorfno se mogu ponaati jo jedino promjenljive tipa neke klase koja
unutar sebe sadri neki atribut koji je polimorfni pokaziva ili polimorfna referenca. Na taj nain je
mogue kreirati polimorfne tipove i polimorfne, ija je pokazivaka priroda sakrivena od korisnika u
implementaciji klase. Recimo, polimorfni funkcijski omotai predstavljaju takvu vrstu polimorfnih
objekata. Primjer realizacije ovakvih polimorfnih tipova razmotriemo neto kasnije.
Polimorfizam je kljuna metodologija objektno orijentiranog programiranja, koja ak ne mora
nuno biti vezana za nasljeivanje. Meutim, u jeziku C++ nasljeivanje i virtualne metode predstavljaju
osnovno sredstvo pomou kojeg se realizira polimorfizam, mada teoretski postoje i drugi naini. Na
primjer, polimorfizam se moe realizirati i putem pokazivaa na funkcije. Zaista, ukoliko imamo neki
atribut koji je po tipu pokaziva na funkciju, takav pokaziva se moe sintaksno pozivati kao da se radi o
obinoj funkciji. Kako tokom ivota objekta taj pokaziva moe pokazivati na razliite funkcije, poziv
koji sintaksno izgleda kao poziv jedne te iste metode moe tokom ivota objekta efektivno pozivati
razliite funkcije, ime se postie polimorfno ponaanje. Vidjeemo kasnije da virtualne funkcije
zapravo nisu nita drugo nego neka vrsta preruenih pokazivaa na funkcije.
Ve smo rekli da se polimorfno ponaanje neke promjenljive tipino realizira tako da se ona
deklarira kao pokaziva na neku baznu klasu koja sadri makar jednu virtualnu metodu, ija je definicija
promijenjena u nekoj od klasa nasljeenih iz te bazne klase (zbog toga se u jeziku C++ uvodi definicija
po kojoj je polimorfna klasa ona koja sadri barem jedu virtuanlu metodu). U tom sluaju se odluka o
tome koju zaista metodu treba pozvati (tj. iz koje klase) odgaa sve do samog trenutka poziva. Da je
odgoda do samog trenutka poziva zaista neophodna demonstrira sljedei primjer, u kojem se odluka o
tome koja se od dvije metode Ispisi (iz klase Student ili iz klase DiplomiraniStudent) ne
moe donijeti prije samog trenutka poziva, jer odluka zavisi od podataka koje korisnik unosi sa tastature,
koji se ne mogu unaprijed predvidjeti:
std::string ime;
int indeks, god dipl;
std::cout << "Unesi ime i prezime studenta:";
std::getline(std::cin, ime);
std::cout << "Unesi broj indeksa studenta:";
std::cin >> indeks;
std::cout << "Unesi godinu diplomiranja ili 0 ukoliko student "
<< "jo nije diplomirao:";
std::cin >> god dipl;
std::shared_ptr<Student> s;
if(god dipl == 0) s = std::make_shared<Student>(ime, indeks);
else s = std::make_shared<DiplomiraniStudent>(ime, indeks, god dipl);
s->Ispisi();

Ostaje jo da razmotrimo zbog ega smo u baznoj klasi Student definirali virtualni destruktor.
Ne smijemo zaboraviti da kada god pomou operatora delete uklanjamo neki dinamiki alocirani
objekat, nad tim objektom se izvrava njegov destruktor. Meutim, ukoliko destruktor nije virtualan,
odluka o tome koji se destruktor poziva donosi se na osnovu statikog tipa pokazivaa na koji je
operator delete primijenjen. To znai da ukoliko je pok deklariran kao pokaziva (obini) na tip
Student, nakon to izvrimo delete pok bie pozvan destruktor iz klase Student, neovisno
od toga na ta zaista pokazuje pokaziva pok. Ukoliko u nekoj klasi nije uope definiran destruktor,
kompajler e sam generirati podrazumijevani destruktor (sa praznim tijelom), ali koji nije virtualan
(razlog za to je to virtualnost ima i svoju cijenu, a filozofija jezika C++ je da nikada ne treba plaati za
ono to se nee koristiti). Deklariranjem virtualnog destruktora u baznoj klasi garantiramo da e prilikom
poziva operatora delete biti uvijek pozvan ispravan destruktor. Dodue, u prethodnim primjerima mi
nigdje eksplicitno ne koristimo operator delete, ali se njegov poziv odvija skriveno zbog injenice da
se koriste pametni pokazivai. Naime, svaki put kada pametni pokaziva prestaje postojati, poziva se
njegov destruktor (tj. destruktor pametnog pokazivaa), koji unitava i objekat na koji on pokazuje, osim
ukoliko ustanovi da postoje i drugi pametni pokazivai koji jo uvijek pokazuju na taj objekat. Meutim,
to unitavanje se izvodi upravo pozivom operatora delete, tako da i u prethodnim primjerima imamo
izvoenje operatora delete nad (obinim) pokazivaima koji su deklarirani kao pokazivai na tip
Student, samo to se to izvoenje obavlja skriveno, iz destruktora pametnih pokazivaa.
12

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

Neko e se vjerovatno zapitati zato je definiranje virtualnog destruktora potrebno ak i u sluaju


kada i bazna i sve izvedene klase uope nemaju definiran vlastiti destruktor, odnosno kada imaju samo
podrazumijevani destruktor koji autmatski generira kompajler. Moe izgledati da je svejedno koji e se
od njih pozvati, jer svakako niti jedan od njih ne radi nita. Meutim, ne smijemo zaboraviti da
destruktori ipak nisu klasine funkcije lanice (iako lie na njih), tako da injenica da je destruktor sa
praznim tijelom ipak ne znai da on ne radi nita. Rekli smo da postoje neke podrazumijevane akcije
koje destruktor izvrava ak i ako mu je tijelo prazno, a unutar tijela destruktora samo se dopisuju
dodatne akcije koje ne spadaju u okvir podrazumijevanih akcija. Stoga, ak i ukoliko su automatski
generirani, nema garancije da e konstruktor bazne i izvedenih klasa raditi istu stvar, tako da pozivanje
krivog destruktora moe imati nepredvidljive posljedice (curenje memorije je najea posljedica, koja
obino nastaje zbog nepotpunog brisanja atributa izvedene klase, ali posljedice mogu biti i mnogo
ozbiljnije i mogu dovesti i do kraha programa). Da rezimiramo, kada god elimo koristiti polimorfizam
bazna klasa mora imati virtualni destruktor, makar sa praznim tijelom. U suprotnom, posljedice su posve
nepredvidljive (ukoliko Vam izgleda da u nekim situacijama program radi ispravno i bez virtualnog
destruktora, ne mora znaiti da e tako biti i sa nekim drugim kompajlerom u odnosu na onaj koji
trenutno koristite). Stoga je virtualni destruktor u baznoj klasi neophodan kad god imamo potrebu da
uklanjamo neki objekat izvedene klase preko pokazivaa na baznu klasu (a to je gotovo uvijek kada
elimo koristiti polimorfizam).
Vano je naglasiti da, bez obzira na polimorfizam, ne moemo preko pokazivaa na baznu klasu
pozvati neku od metoda koja postoji samo u naslijeenoj klasi a ne i u baznoj klasi, ak i ukoliko taj
pokaziva trenutno pokazuje na objekat naslijeene klase (analogna primjedba vrijedi i za reference). Na
primjer, sljedea konstrukcija nije dozvoljena, zbog toga to je s deklariran kao pokaziva na klasu
Student koja ne posjeduje metodu DajGodinuDiplomiranja, bez obzira to je on inicijaliziran
tako da pokazuje na primjerak klase DiplomiraniStudent:
Student *s(new DiplomiraniStudent("Miki Maus", 3412, 2004));
std::cout << s->DajGodinuDiplomiranja();

Ovo nije dozvoljeno zbog toga to u trenutku prevoenja programa prevodilac nema garancije da
promjenljiva zaista pokazuje na objekat koji posjeduje ovu metodu. Zaista, kada bi bio dozvoljen poziv
metode DajGodinuDiplomiranja nad promjenljivom s, moglo bi doi do velikih problema, s
obzirom na injenicu da je s pokaziva na tip Student, tako da moe pokazivati na objekat koji
uope ne sadri podatak o godini diplomiranja. Stoga su autori jezika C++ usvojili da ovakvi pozivi ne
budu dozvoljeni. Naravno, provjera na koji tip s zaista pokazuje mogla bi se izvriti prilikom samog
poziva metode, ali to bi bilo dodatno troenje vremena koje bi se moralo vriti prilikom svakog poziva
svake metode, to autori jezika C++ nisu eljeli. Ukoliko nam je ba neophodno da nad pokazivaem
koji je deklariran da pokazuje na baznu klasu primijenimo neku metodu koja je definirana samo u
naslijeenoj klasi, a sigurni smo da u posmatranom trenutku taj pokaziva zaista pokazuje na objekat
izvedene klase (odnosno, ukoliko smo sigurni da je u posmatranom trenutku njegov dinamiki tip takav
da poziv eljene metode ima smisla) moemo na pokaziva primijeniti eksplicitnu konverziju tipa u tip
pokazivaa na izvedenu klasu pomou operatora za pretvorbu tipa (type-casting operatora), a zatim na
rezultat pretvorbe primijeniti metodu koju elimo. Ovakva pretvorba (koja se za razliku od pretvorbe
navie ne vri automatski) naziva se pretvorba nanie (engl. downcasting). Recimo, nad promjenljivom
s iz prethodnog primjera mogli bismo uraditi sljedee:
std::cout << ((DiplomiraniStudent *)s)->DajGodinuDiplomiranja();

Alternativno, umjesto prethodne konstrukcije koja koristi sintaksu za pretvorbu tipa u C stilu, moemo
koristiti i sintaksu preporuenu u jeziku C++, koja za tu svrhu koristi kljunu rije static cast:
std::cout << static cast<DiplomiraniStudent *>(s)
->DajGodinuDiplomiranja();

Meutim, bez obzira na koritenu sintaksu, ovakve konstrukcije preduzimamo na vlastitu odgovornost, s
obzirom da posljedice mogu biti posve nepredvidljive ukoliko s ne pokazuje na objekat koji je tipa
DiplomiraniStudent (tj. ukoliko mu dinamiki tip nije odgovarajui). Slinu konverziju morali
bismo izvriti ukoliko je s polimorfna referenca (samo bismo u oznaci tipa prilikom konverzije

13

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 13_a
Akademska godina 2013/14

umjesto znaka * pisali znak & ime naznaavamo da vrimo konverziju ponovo u referencu).
Ukoliko nam je potrebno da u toku izvravanja programa ispitamo na ta pokazuje neki polimorfni
pokaziva (ili referenca), moemo koristiti operator typeid, koji se koristi na nain koji je sasvim
jasan iz sljedeeg primjera:
if(typeid(*s) == typeid(DiplomiraniStudent))
std::cout << static cast<DiplomiraniStudent *>(s)
->DajGodinuDiplomiranja();
else
std::cout << "alim, s ne pokazuje na diplomiranog studenta!";

Operator typeid moe se primijeniti na neki izraz ili ime tipa, a kao rezultat daje izvjesnu
kolekciju informacija o tipu onoga na ta je primijenjen (tana priroda tih informacija uglavnom nije
bitna, niti je posve precizno specificirana). Alternativno, moe se vriti i tzv. dinamika pretvorba
pokazivaa uz pomo kljune rijei dynamic_cast. Ona se sintaksno vri identino kao i klasina
pretvorba pomou kljune rijei static_cast (tzv. statika pretvorba), samo je razlika u tome to se
kao rezultat dinamike pretvorbe dobija nul-pokaziva u sluaju da se pretvorba ne moe obaviti zbog
toga to izvorni pokaziva ne pokazuje na objekat koji odgovara eljenom tipu pretvorbe (tj. ukoliko
dinamiki tip pokazivaa nije u skladu sa tipom u koji elimo izvriti pretvorbu). Sljedei primjer
ilustrira dinamiku pretvorbu pokazivaa. Primjer je interesantnan i zbog toga jer se u njemu koristi
malo poznata osobina da se unutar uvjeta za if naredbu moe izvriti i deklaracija promjenljive, koja
tada vrijedi samo lokalno unutar uvjeta (neki kau da je ova mogunost uvedena upravo za potrebe
ovakvih konstrukcija). Inae, deklaraciju smo mogli i pojednostaviti koritenjem kljune rijei auto:
if(DiplomiraniStudent *s1 = dynamic cast<DiplomiraniStudent *>(s))
s1->DajGodinuDiplomiranja();
else cout << "alim, s ne pokazuje na diplomiranog studenta!";

U principu, koritenje dinamike pretvorbe je mnogo monije, ali manje efikasno od upotrebe
operatora typeid. Zaista, neka imamo jo neku klasu naslijeenu iz klase DiplomiraniStudent
(recimo klasu StudentDoktorant) i neka pokaziva s koji je deklariran kao pokaziva na tip
Student u nekom trenutku pokazuje na objekat tipa StudentDoktorant. Pretpostavimo dalje da
je potrebno pokaziva s pretvoriti u pokaziva na tip DiplomiraniStudent. Ovakva pretvorba je
posve sigurna, s obzirom da objekat tipa StudentDoktorant (na koji s pokazuje) sadri sve to
sadre i objekti tipa DiplomiraniStudent. Stoga e dynamic cast uspjeno obaviti takvu
pretvorbu. S druge strane, tu injenicu neemo moi saznati primjenom typeid operatora, s obzirom
da e izraz typeid(*s) utvrditi injenicu da s pokazuje na objekat tipa StudentDoktorant a
ne na objekat tipa DiplomiraniStudent (tako da neemo znati da li je pretvorba sigurna). Bez
obzira na sve, mada je injenica da pretvorba pokazivakih tipova nanie i typeid operator mogu
ponekad biti korisni, pa ak i neophodni (inae ne bi bili ni uvedeni), njihova intenzivna upotreba gotovo
sigurno ukazuje na pogrean pristup problemu koji se rjeava.
Zbog nekih tehnikih razloga, operatori static_cast i dynamic_cast ne rade sa pametnim
pokazivaima. Umjesto njih, za pretvorbu nanie pametnih pokazivaa treba koristiti funkcije slinih
imena static_pointer_cast i dynamic_pointer_cast. Preciznije, umjesto konstrukcija poput
static_cast<Tip *> i dynamic_cast<Tip *>, pri radu sa pametnim pokazivaima koriste se
konstrukcije std::static_pointer_cast<Tip> i std::dynamic_pointer_cast<Tip>.
Mada je pokazivau na baznu klasu mogue dodijeliti pokaziva na objekat naslijeene klase
odnosno adresu nekog objekta naslijeene klase, obrnuta dodjela nije mogua. Tako, pokazivau na
objekat naslijeene klase nije mogue dodijeliti pokaziva na objekat bazne klase, odnosno adresu nekog
objekta bazne klase. Takoer, funkcija koja prima kao parametar ili vraa kao rezultat pokaziva na
objekat naslijeene klase ne moe prihvatiti kao parametar ili vratiti kao rezultat pokaziva na objekat
bazne klase. Analogno vrijedi i za reference. Razlog za ovu zabranu je veoma jednostavan: preko
pokazivaa (ili reference) na naslijeenu klasu moe se pristupiti atributima i metodama koje u baznoj
klasi uope ne moraju postojati, pa bi mogunost dodjele adrese nekog objekta bazne klase pokazivau
na naslijeenu klasu moglo dovesti do kobnih posljedica. Ukoliko smo apsolutno sigurni ta radimo,
ovakve dodjele ipak moemo izvesti koritenjem operatora za pretvorbu tipa (tj. pretvorbom nanie), ali
ako imamo i djeli sumnje u ono to radimo, ovakve vratolomije treba izbjei po svaku cijenu.
14

You might also like