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

Dr.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Predavanje 10_b
Da bismo bolje shvatili sutinu inicijalizacije primjeraka klasa pomou konstruktora, vratimo se na
klasu Kompleksni koju smo ranije napisali. Neka je potrebno deklarirati promjenljivu a tipa
Kompleksni i inicijalizirati je na kompleksni broj (1,2). Najneposredniji nain da ovo uradimo je
sljedei nain, koji se naziva i direktna inicijalizacija (ukoliko Vam sintaksne konstrukcije koje ovdje
koristimo djeluju odnekud poznate, to je zbog toga to tip complex koji smo koristili ranije nije nita
drugo nego klasa definirana u istoimenoj biblioteci):
Kompleksni a(1, 2);

// U C++11 moe i Kompleksni a{1, 2}

Kod direktne inicijalizacije, konstruktoru se direktno prosljeuju informacije o tome kako treba
inicijalizirati objekat. Meutim, postoje i drugi naini da se u konanici postigne isti efekat. Da bismo to
pojasnili, podsjetimo se da se veina objekata takoer moe inicijalizirati pomou nekog objekta (ili
openitije, izraza) istog tipa, pri emu tipino dolazi do kopiranja objekta kojim vrimo inicijalizaciju u
objekat koji inicijaliziramo (posredstvom kopirajueg konstruktora, odnosno konstruktora kopije). Stoga
je inicijalizaciju promjenljive a na vrijednost (1,2) mogue izvesti i na sljedee naine (pri emu
je u C++11 mogue koristiti i vitiaste zagrade):
Kompleksni a(Kompleksni(1, 2));
Kompleksni a = Kompleksni(1, 2);

Posljednja inicijalizacija, u kojoj se koristi znak jednakosti i u kojoj se poziv konstruktora ne vidi
eksplicitno, naziva se kopirajua inicijalizacija (za razliku od pretposljednje, koja tehniki ipak spada u
direktnu inicijalizaciju, ali koja se vri objektom istog tipa). Tipino ove dvije inicijalizacije imaju isti
efekat, ali posljednja nee raditi ukoliko se u klasi Kompleksni definira kopirajui konstruktor koji se
jo proglasi za eksplicitan (u praksi se to gotovo nikada ne radi). U svakom sluaju, u oba gore navedena
primjera, prvo se konstrukcijom Kompleksni(1, 2), koja predstavlja poziv konstruktora kao
funkcije, kreira privremeni bezimeni objekat tipa Kompleksni inicijaliziran na vrijednost (1,2)
koji se potom koristi za inicijalizaciju promjenljive a (pomou kopirajueg konstruktora). Mada bi
ovakve inicijalizacije u principu trebale biti manje efikasne nego direktna inicijalizacija promjenljive a
(s obzirom da se prvo kreira privremeni objekat koji se inicijalizira na vrijednost (1,2) koji se zatim
kopira u objekat a), veina kompajlera e ove konstrukcije optimizirati i izbjei nepotrebno kopiranje,
tako da e se one efektivno svesti na konstrukciju u kojoj se promjenljiva a inicijalizira direktno (treba
istai da kompajler ima samo pravo ali ne i obavezu da takve optimizacije zaista sprovede). Ipak, treba
znati da su ovakve indirektne inicijalizacije dozvoljene samo kod klasa iji se primjerci mogu kopirati
(a takve su sve klase kod kojih dizajner nije eksplicitno rekao drugaije, to se moe izvesti tako to se
na neki nain kopirajui konstruktor uini nedostupnim, o emu emo kasnije govoriti). To vrijedi bez
obzira da li e do kopiranja zaista doi ili e ga kompajler izbjei. U suprotnom, kompajler e odmah
prijaviti greku da kopiranje primjeraka te klase nije dozvoljeno (odnosno da kopirajui konstruktor nije
dostupan), prije nego to uope pokua primijeniti ikakvu optimizaciju!
Kako se primjerci klasa mogu meusobno dodjeljivati (osim ukoliko projektant klase nije odredio
drugaije, to se takoer moe izvesti), postavljanje promjenljive a na vrijednost (1,2) naelno je
mogue izvesti i pomou sljedee konstrukcije, u kojoj se koristi naknadna dodjela:
Kompleksni a;
a = Kompleksni(1, 2);

// Automatska inicijalizacija na (0,0)!


// Ovo je dodjela, a ne inicijalizacija!

Meutim, bez obzira na inteligenciju upotrijebljenog kompajlera, ovakva konstrukcija je uvijek osjetno
manje efikasna od prethodnih. Naime, ovdje se prvo kreira objekat a koji se inicijalizira na kompleknu
vrijednost (0,0) posredstvom konstruktora bez parametara (stoga ovakva konstrukcija ne bi bila
mogua da konstruktor bez parametara ne postoji), nakon ega se kreira privremeni bezimeni objekat
tipa Kompleksni koji se inicijalizira na kompleksni broj (1,2) i koji se na kraju kopira u objekat
a (unitavajui pri tome njegov prethodni sadraj). Ovim se jasno uoava razlika izmeu inicijalizacije
i dodjele. Razlika u efikasnosti izmeu neposredne inicijalizacije i konstrukcija u kojima se koristi
naknadna dodjela osjetno se poveava sa porastom sloenosti objekata.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Radi boljeg razumijevanja izloenih koncepata, rezimirajmo ukratko u emu je razlika izmeu
sljedeih konstrukcija:
Kompleksni
Kompleksni
Kompleksni
Kompleksni

a(5);
a = 5;
a = Kompleksni(5);
a(Kompleksni(5));

Prva i etvrta konstrukcija spadaju u direktne inicijalizacije, dok su druga i trea kopirajue inicijalizacije.
Sve etiri konstrukcije jasno zahtijevaju da klasa Kompleksni posjeduje konstruktor sa jednim
parametrom. Prva konstrukcija direktno inicijalizira objekat a pozivom takvog konstruktora. Druga
konstrukcija se automatski interpretira kao trea, osim ukoliko je konstruktor klase Kompleksni sa
jednim parametrom deklarirali kao eksplicitni konstruktor (tada ova konstrukcija ne bi bila uope
dozvoljena). to se tie tree konstrukcije, ona se automatski interpretira kao etvrta, osim ukoliko je u
klasi Kompleksni kopirajui konstruktor deklariran kao eksplicitan (to se gotovo nikada ne radi,
osim u nekim patolokim situacijama). Konano, etvrta konstrukcija prvo kreira privremeni objekat koji
se inicijalizira pomou konstruktora sa jednim parametrom i koji se nakon toga pomou kopirajueg
konstruktora kopira u objekat a (mada e veina kompajlera optimizirati ovu konstrukciju da generira
posve isti izvrni kd kao i prva konstrukcija u kojoj se objekat a kreira neposredno). Situacija se
moe dodatno zakomplicirati ukoliko klasa ima vie konstruktora sa istim brojem parametara a koji se
razlikuju po tipu parametara (tj. kada se konstruktori preklapaju po tipu parametara), pogotovo ako su
neki eksplicitni a neki nisu. U detalje se ovdje neemo uputati, samo emo rei da je, uz pomo takvih
preklapanja, teoretski mogue postii da sve etiri gore napisane konstrukcije proizvode razliito dejstvo
(mada nema nikakvog razloga zato bi neko tako neto radio, osim istog iivljavanja).
Sasvim je mogue napraviti nizove iji su elementi primjerci neke klase, potpuno analogno nizovima
iji su elementi strukturnog tipa (razumije se da emo u takvim nizovima metode klase primjenjivati nad
individualnim elementima niza, a ne nad itavim nizom). Meutim, prisustvo konstruktora donekle
komplicira deklariranje nizova iji su elementi primjerci neke klase (u argonu se takvi nizovi esto
pogreno nazivaju nizovi klasa, iako je jasno da se od klasa koje su tipovi a ne objekti ne mogu praviti
nizovi, ve samo od primjeraka klasa). Nizove iji su elementi primjerci neke klase mogue je bez
problema deklarirati jedino ukoliko ta klasa ima podrazumijevani konstruktor, odnosno konstruktor bez
parametara (to ukljuuje i automatski generirani podrazumijevani konstruktor koji kompajler
automatski generira u sluaju da nismo sami napisali niti jedan konstruktor za klasu, to ni u kom sluaju
nije dobra praksa). U tom sluaju, konstruktor bez parametara e biti iskoriten da inicijalizira svaki
element niza. Na primjer, u prethodnu definiciju klase Kompleksni, deklaracija
Kompleksni niz[100];

// Ovo uzrokuje 100 poziva konstruktora!

deklarirae niz niz od 100 elemenata tipa Kompleksni, od koji e svaki (zahvaljujui konstruktoru
bez parametara) biti inicijaliziran na kompleksni broj (0,0) (to efektivno znai da e
podrazumijevani konstruktor klase Kompleksni biti pozvan ak 100 puta). Potpuno ista stvar bi se
desila i u sluaju kreiranja dinamikog niza iji su elementi tipa Kompleksni deklaracijom poput
Kompleksni *dinamicki_niz(new Kompleksni[100]);

Svi elementi tako stvorenog dinamikog niza takoer bi bili inicijalizirani konstruktorom bez parametara
(tako da e se i ovdje takav konstruktor pozvati 100 puta).
Situacija je znatno sloenija ukoliko klasa ima definirane konstruktore, ali meu njima nema
podrazumijevanog konstruktora (tj. konstruktora bez parametara). U tom sluaju, neposredne deklaracije
nizova (kako statikih, tako i dinamikih) iji su elementi primjerci te klase nisu mogue. Na primjer, uz
ranije definiranu klasu Datum, ni jedna od sljedee dvije deklaracije nee biti prihvaena:
Datum niz_datuma[100];
Datum *dinamicki_niz_datuma(new Datum[100]);

Razlog nije teko naslutiti: ni u jednoj od ove dvije deklaracije nije jasno kako bi se trebali inicijalizirati
elementi kreiranih nizova. U ovakvoj situaciji, sve do pojave C++11 standarda bilo je mogue jedino

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

deklarirati statiki inicijalizirani niz primjeraka neke klase, pri emu se inicijalizacija elemenata niza
vri kao i statika inicijalizacija ma kojeg drugog niza (navoenjem eljenih elemenata niza unutar
vitiastih zagrada). Pri tome se moraju navesti svi elementi niza. Meutim, kako su u ovom sluaju
elementi niza primjerci klase, to i elementi u vitiastim zagradama moraju biti takoer primjerci iste
klase (alternativno bi se mogli navesti i elementi koji se pomou neeksplicitnog konstruktora sa jednim
parametrom mogu automatski pretvoriti u primjerke te klase). Slijedi da oni moraju biti ili promjenljive
iste klase, ili rezultati primjene konstruktora pozvanog kao funkcija, ili neki izrazi koji su tipa te klase
(npr. pozivi neke funkcije koja vraa objekat te klase kao rezultat), ili neki izrazi koji su nekog tipa koji
se moe automatski pretvoriti u tip te klase. Na primjer, sljedea konstrukcija se moe upotrijebiti za
kreiranje inicijaliziranog niza koji sadri 5 datuma (znak jednakosti se u C++11 moe izostaviti):
Datum niz_datuma[5] = {Datum(31, 12, 2004), Datum(8, 4, 2003),
Datum(14, 7, 1998), Datum(4, 11, 2000), Datum(6, 2, 2005)};

U C++11 ovo se moe napisati i mnogo jednostavnije, zahvaljujui injenici da se u odgovarajuem


kontekstu inicijalizacione liste automatski konvertiraju u odgovarajue objekte (uz pomo pozivanja
konstruktora kao funkcije):
Datum niz_datuma[5]{{31, 12, 2004}, {8, 4, 2003}, {14, 7, 1998},
{4, 11, 2000}, {6, 2, 2005}};

U sluaju kada sve elemente elimo inicijalizirati na isti datum, situacija je donekle jednostavnija, s
obzirom da moemo uraditi neto kao u sljedeoj konstrukciji:
Datum d(1, 1, 2000);
Datum niz_datuma[10] = {d, d, d, d, d, d, d, d, d, d};

// C++11 bez "="

to se tie dinamiki alociranih nizova objekata koji ne posjeduju konstruktore bez parametara, sve
do pojave C++11 standarda tako neto nije bilo mogue, jer raniji standardi jezika C++ nisu omoguavali
inicijalizaciju dinamiki alociranih nizova kreiranih pomou operatora new (strogo uzevi, biblioteka
memory sadri tzv. alokatore pomou kojih se moe zaobii operator new i izvriti dinamiko
kreiranje nizova ak i u ovim sluajevima, ali u pitanju su napredne specijalistike tehnike o kojima
neemo govoriti). Meutim, nakon pojave C++11 standarda, situacija je mnogo jednostavnija, s obzirom
da C++11 dozvoljava i inicijalizaciju dinamiki alociranih nizova. Stoga je u C++11 mogua i
konstrukcija poput sljedee:
Datum *dinamicki_niz_datuma(new Datum[5]{Datum(31, 12, 2004),
Datum(8, 4, 2003), Datum(14, 7, 1998), Datum(4, 11, 2000),
Datum(6, 2, 2005)});

Skraeni zapis iste konstrukcije izgledao bi ovako:


Datum *dinamicki_niz_datuma(new Datum[5]{{31, 12, 2004}, {8, 4, 2003},
{14, 7, 1998}, {4, 11, 2000}, {6, 2, 2005}});

Meutim, u svim gore prikazanim primjerima, jasno je da je zbog potrebe da navedemo inicijalizaciju za
svaki element niza, na ovaj nain praktino nemogue definirati iole vei niz.
Postoje dva izlaza iz ove situacije. Jedno rjeenje je ubaciti u definiciju klase konstruktor bez
parametara koji inicijalizira atribute klase na neku podrazumijevanu vrijednost. Ovo rjeenje je
jednostavno, ali se ne smatra osobito dobrim, s obzirom da ne postoje uvijek smislene podrazumijevane
vrijednosti koje bi trebao da postavlja konstruktor bez parametara. Na primjer, za klasu Kompleksni
je prirodno izvriti inicijalizaciju na kompleksni broj (0,0) u sluaju da se drugaije ne navede. S
druge strane, nije posve jasno na koje vrijednosti bi trebalo inicijalizirati recimo elemente klase Datum
u sluaju da se eksplicitno ne navedu dan, mjesec i godina. Stoga je mnogo bolje rjeenje umjesto nizova
primjeraka neke klase koristiti nizove pokazivaa (obinih ili pametnih) na primjerke klase (preciznije,
nizove iji su elementi pokazivai na dinamiki kreirane primjerke klase). Na taj nain ne samo da
rjeavamo probleme vezane za inicijalizaciju, nego i olakavamo manipulaciju s objektima, na ta smo
ve ukazivali kada smo govorili o nizovima pokazivaa na primjerke struktura. Takvo rjeenje bie
demonstrirano malo kasnije.
3

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Neto je jednostavnija situacija prilikom kreiranja vektora iji su elementi primjerci neke klase, s
obzirom da se prilikom deklaracije vektora moe zadati parametar koji slui za inicijalizaciju svih
elemenata vektora. Tako, vektor od 1000 elemenata iji su elementi tipa Datum, moemo deklarirati
recimo ovako (prisustvo drugog parametra u deklaraciji je u ovom sluaju obavezno):
std::vector<Datum> vektor datuma(1000, Datum(1, 1, 2000));

Naravno, svi elementi ovakvog vektora bie inicijalizirani na 1/1/2000.


Kada ve govorimo o nizovima i generalno raznim kontejnerskim objektima (dekovima, listama,
skupovima, itd.) iji su elementi primjerci neke klase, neophodno je upoznati se i sa funkcijama
emplace_back, emplace_front i emplace uvedenim u C++11 standardu, koje je sa takvim
kontejnerskim objektima preporuljivo koristiti umjesto funkcija push_back, push_front i
insert respektivno. Ovdje emo opisati samo funkciju emplace_back, dok za preostale dvije
funkcije vrijede analogni komentari. Pretpostavimo da imamo recimo vektor vektor_datuma iji su
elementi tipa Datum i da elimo na njegov kraj dodati neki novi datum, recimo, 15/7/2012. Klasini
nain da to uradimo pomou funkcije push_back izgledao bi ovako:
vektor datuma.push_back(Datum(15, 7, 2012));

U C++11 ovo bismo mogli sintaksno pojednostaviti na sljedei oblik:


vektor datuma.push_back({15, 7, 2012});

// Samo u C++11

Meutim, bez obzira na sintaksu, ovdje dolazi do primjetnog gubitka u efikasnosti. U oba sluaja, prvo
se kreira bezimeni objekat tipa Datum, a koji se zatim kopira (kopirajuim konstruktorom) u
novokreirani element vektora (koji je prethodno bespotrebno inicijaliziran na vrijednost zadanu prilikom
kreiranja vektora). Stoga je umjesto prethodnih konstrukcija mnogo bolje koristiti sljedeu konstrukciju:
vektor datuma.emplace_back(15, 7, 2012);

// Samo u C++11

Ova konstrukcija logiki radi slino kao i prethodna, jedino to se prilikom kreiranja novog elementa
vektora (koji je tipa Datum) odmah konstruktoru prosljeuju parametri koji su zadani kao parametri
funkcije emplace_back. Na taj nain se u vektoru odmah kreira ispravno konstruisan objekat, bez
potrebe da se prvo kreira propisno inicijalizirani pomoni privremeni objekat, koji se potom kopira u
vektor. Funkcije emplace_front i emplace koriste se analogno kao alternativa za funkcije
push_front i insert. Inae, ove funkcije se mogu koristiti i kod vektora iji su elementi prosti
tipovi poput int, ali se one u tom sluaju prosto svode na funkcije poput push_back itd. Na
primjer, ukoliko je v vektor cijelih brojeva, konstrukcija poput v.emplace_back(5) praktino
ima isto dejstvo i istu efikasnost kao i konstrukcija v.push_back(5).
Statiki inicijalizirani niz iji su elementi primjerci neke klase mogue je napraviti i u sluaju kada
klasa posjeduje kako konstruktore sa parametrima, tako i konstruktore bez parametara (u C++11
standardu ovo je mogue i za dinamiki alocirane nizove, jer se i oni mogu inicijalizirati). U tom sluaju,
inicijalizaciona lista ne mora sadravati sve elemente, jer e preostali elementi biti automatski
inicijalizirani konstruktorom bez parametara. Na primjer, mogue je deklarirati sljedei niz:
Kompleksni niz[10] = {Kompleksni(4, 2), Kompleksni(3), 5};

U C++11 mogue je sintaksno pojednostavljenje poput sljedeeg:


Kompleksni niz[10]{{4, 2}, {3}, 5};

// Samo u C++11

U ovom primjeru, element niz[0] se inicijalizira na kompleksni broj (4,2), element niz[1] na
kompleksni broj (3,0), element niz[2] na kompleksni broj (5,0), a svi ostali elementi niza na
kompleksni broj (0,0). Primijetimo da je inicijalizacija elementa niz[2] na kompleksni broj
(5,0) navoenjem samo broja 5 mogua zahvaljujui pretvorbi tipova koja se ostvaruje pomou
konstruktora sa jednim parametrom. Naravno, ovakva inicijalizacija ne bi bila mogua da je konstruktor
klase Kompleksni definiran kao eksplicitni konstruktor pomou kljune rijei explicit.
4

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Opisane nelagodnosti vezane za ogranienja koja konstruktori nameu po pitanju mogunosti


kreiranja nizova iji su elementi primjerci neke klase u praksi ne predstavlja osobit problem, s obzirom
da se u objektno orijentiranom programiranju kao elementi nizova mnogo ee koriste pokazivai na
primjerke klasa nego sami primjerci klasa. Jedan od razloga za to je to esto ne znamo unaprijed na
koje vrijednosti treba inicijalizirati elemente klase, tako da pojedine primjerke klasa esto kreiramo
dinamiki, i to tek onda kada saznamo ime ih treba inicijalizirati. Pored toga, kasnije emo vidjeti da su
nizovi (ili vektori, ili neki drugi kontejnerski tipovi podataka) pokazivaa na klase takoer potrebni da se
podri jo jedan od bitnih koncepata objektno orijentiranog programiranja, tzv. polimorfizam, tako da je
bolje da se to ranije naviknemo na njihovu upotrebu.
Kao primjer, pretpostavimo da elimo unijeti podatke o deset datuma sa tastature i smjestiti ih u niz.
Oigledno, ne moemo znati na koje vrijednosti treba inicijalizirati neki datum prije nego to podatke o
njemu unesemo sa tastature. Ukoliko bismo deklarirali obian niz datuma, on bi zbog postojanja
konstruktora morao na poetku biti inicijaliziran neim, bilo runim navoenjem inicijalizacije za svaki
element, bilo dodavanjem konstruktora bez parametara u klasu Datum koja bi izvrila neku
podrazumijevanu inicijalizaciju. Meutim, u oba sluaja radimo bespotrebnu inicijalizaciju objekata
kojima emo svakako promijeniti sadraj im se podaci o konkretnom datumu unesu sa tastature (sline
primjedbe vrijede i u sluaju da koristimo vektor iji su elementi tipa Datum). Stoga je mnogo bolje
deklarirati niz (ili vektor) iji su elementi pokazivai na objekte tipa Datum, a same datume kreirati
dinamiki onog trenutka kada podaci o njima budu poznati. Nakon toga emo pokaziva na
novostvoreni objekat, smjestiti u niz (ili vektor, ili neki drugi kontejnerski tip podataka), a samom
objektu emo pristupati indirektno preko pokazivaa. Ilustrirajmo ovo na konkretnom primjeru koji trai
da se unese 10 datuma sa tastature, i koji nakon toga ispisuje sve unesene datume:
Datum *niz datuma[10] = {};
// Svi pokazivai se inicijaliziraju
std::cout << "Unesi 10 datuma:\n"; //
na "nullptr"...
try {
for(int i = 0; i < 10; i++) {
int dan, mjesec, godina;
std::cout << "Datum " << i + 1 << ":" << std::endl;
std::cout << " Dan: "; std::cin >> dan;
std::cout << " Mjesec: "; std::cin >> mjesec;
std::cout << " Godina: "; std::cin >> godina;
try {
niz datuma[i] = new Datum(dan, mjesec, godina); // Konstruktor
}
//
moe baciti
catch(std::domain_error izuzetak) {
//
izuzetak!
std::cout << izuzetak.what() << std::endl;
i--;
// Ponovi unos!
}
}
std::cout << "Unijeli ste datume:\n";
for(int i = 0; i < 10; i++) {
niz datuma[i]->Ispisi();
std::cout << std::endl;
}
}
catch(std::bad_alloc) {
std::cout << "Problemi sa memorijom!\n";
}
for(int i = 0; i < 10; i++) delete niz datuma[i]; // Obrii zauzeto...

Analizirajmo sada neka kljuna mjesta u ovom programskom isjeku. Na poetku je deklariran niz
od 10 pokazivaa na objekte tipa Datum, koji je, jednostavnosti radi, nazvan niz datuma (iako se,
striktno reeno, ne radi o nizu datuma, nego o nizu pokazivaa na datume). Pri tome su svi elementi niza
pokazivaa na poetku inicijalizirani na nul-pokazivae (navoenjem praznih vitiastih zagrada prilikom
deklaracije), sa ciljem da izbjegnemo eventualne probleme koje bi na kraju mogao izazvati operator
delete u sluaju da neka od alokacija sluajno nije uspjela. Bitno je istai da nizove pokazivaa na
primjerke neke klase uvijek moemo deklarirati bez problema, bez obzira kakve konstruktore klasa
sadri, s obzirom da konstruktori ne inicijaliziraju pokazivae, nego primjerke klase, tako da e

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

inicijalizacija biti odgoena za trenutak kada dinamiki kreiramo objekat pomou operatora new
(naravno, sve ovdje izloene primjedbe vrijede i za sluaj ukoliko se umjesto nizova koriste vektori).
Posebno je znaajno to sa ovakvim nizom pokazivaa moemo raditi gotovo identino kao da se radi o
obinom nizu primjeraka klasa, samo to za poziv metoda umjesto operatora . treba koristiti operator
->. Na primjer, za ispis elemenata niza umjesto konstrukcije niz datuma[i].Ispisi() treba
koristiti konstrukciju niz datuma[i]->Ispisi() (podsjetimo se da je ova konstrukcija zapravo
ekvivalentna konstrukciji (*niz datuma[i]).Ispisi()). Prema tome, injenica da radimo sa
pokazivaima na primjerke klase umjesto sa samim primjercima klase gotovo da ne unosi nikakvu
dodatnu potekou. Treba samo obratiti panju na dva sitna detalja. Prvo, operator new moe baciti
izuzetak u sluaju da ponestane memorije (mada je veoma mala ansa da e se ovo desiti, s obzirom da
jedan datum zauzima veoma malo memorije), tako da treba predvidjeti hvatanje izuzetka. Izuzetak
takoer moe baciti i konstruktor klase Datum (u sluaju da su podaci besmisleni), tako da i to treba
imati u vidu. Drugo, sve dinamiki stvorene objekte treba na prije zavretka programa i unititi, to je
izvedeno na kraju programa.
Prethodni programski isjeak moe se vrlo jednostavno prepraviti da radi sa pametnim pokazivaima,
to je vrlo preporuljivo uraditi. Na taj nain ne samo da ne moramo vie misliti o oslobaanju dinamiki
alociranih objekata tipa Datum kada nam vie nisu potrebni, nego smo zatieni i od eventualnih
curenja memorije koja bi mogla nastati ukoliko negdje bude baen neplanirani izuzetak prije nego to se
naie na dio kda u kojem se vri oslobaanje memorije (sa gore prikazanim isjekom programa takvi
nepredvieni izuzeci se nee pojaviti, ali bi se mogli pojaviti ukoliko bismo modificirali i proirivali taj
isjeak dodavanjem novih funkcionalnosti). Prepravke koje trebamo izvesti za prelazak na pametne
pokazivae su trivijalne. Sve to treba uraditi je promijeniti deklaraciju niza niz_datuma na
std::shared_ptr<Datum> niz datuma[10];

kao i naredbu u kojoj se vri dinamika alokacija objekata tipa Datum na


niz datuma[i] = std::make_shared<Datum>(dan, mjesec, godina);

Naravno, nestala bi i potreba za petljom na kraju isjeka u kojoj se vri brisanje dinamiki alociranih
objekata. Sve ostalo u ovom programskom isjeku moglo bi ostati potpuno isto.
Kao to elementi struktura mogu biti ponovo strukture, tako i elementi klasa mogu biti (i esto jesu)
takoer primjerci neke klase. Na primjer, neka hipotetika klasa Student vjerovatno bi trebala
sadravati atribut datum rodjenja koji je tipa Datum. Pogledajmo kako bi mogla izgledati
deklaracija neke minimalistike klase Student:
class Student {
char ime_i_prezime[50];
int indeks;
Datum datum_rodjenja;
public:
... // Ovdje bi trebalo definirati interfejs klase
};

Meutim, u ovakvim situacijama se ponovo javlja jedna potekoa uzrokovana konstruktorima


(nemojte pomisliti da konstruktori izazivaju samo probleme konstruktori su jedna od najkorisnijih
alatki objektno zasnovanog programiranja, a sve eventualne potekoe koje nastaju usljed njihovog
koritenja veoma se lako rjeavaju, samo treba objasniti kako). Naime, poto klasa Datum ima
konstruktor, niti jedan objekat tipa Datum ne moe ostati neinicijaliziran! Stoga svako stvaranje bilo
kojeg primjerka tipa Student mora neizostavno dovesti i do inicijalizacije njegovog atributa
datum_rodjenja. Jedino je pitanje kako. Da klasa Datum ima konstruktor bez parametara, on bi
bio automatski iskoriten za inicijalizaciju atributa datum_rodjenja (poziv tog konstruktora bio bi
jedna od podrazumijevanih akcija konstruktora klase Student). Meutim, kako klasa Datum ima
samo konstruktor sa tri parametra, objekti klase Datum moraju biti inicijalizirani sa tri parametra. Isto
vrijedi i za atribute klase: atribut datum_rodjenja mora takoer biti inicijaliziran sa tri parametra,
odmah pri stvaranju nekog objekta tipa Student. Kako je konstruktor jedino mjesto odakle je mogue
6

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

izvriti inicijalizaciju objekta odmah po njegovom stvaranju, slijedi da klasa Student svakako mora
imati definiran konstruktor (to i nije neki problem, jer bi svaka dobro osmiljena klasa svakako trebala
imati definirane konstruktore). Stoga bi konstruktor klase Student morao nekako da inicijalizira
atribut datum_rodjenja. Meutim, tu se javlja i jedan dodatni problem. Kada bi se inicijalizacija
ovog atributa izvela negdje unutar tijela konstruktora, on bi unutar tijela konstruktora bio privremeno
neinicijaliziran, sve do mjesta gdje je izvrena inicijalizacija, to se takoer ne smije dozvoliti. Slijedi da
atribut datum_rodjenja mora na neki nain biti inicijaliziran prije nego to se tijelo konstruktora
uope pone izvravati!
Da bi se rijeio ovaj problem, uvedena je sintaksa koja omoguava konstruktoru neke klase da
iskoristi konstruktore drugih klasa za inicijalizaciju svojih atributa, prije nego to se uope zaponu
izvravati akcije naznaene u tijelu konstruktora. Za tu svrhu, nakon zatvorene zagrade u deklaraciji
parametara konstruktora stavlja se dvotaka, iza koje slijedi takozvana konstruktorska inicijalizacijska
lista. Ona sadri popis svih inicijalizacija atributa koje se moraju izvriti putem konstruktora odmah pri
stvaranju objekta. Pri tome se u konstruktorskoj inicijalizacijskoj listi koristi ista sintaksa kao pri
inicijalizaciji obinih promjenljivih pomou konstruktora (parametri u okruglim zagradama, a poev od
C++11 standarda doputene su i vitiaste). Tek nakon konstruktorske inicijalizacijske liste slijedi tijelo
konstruktora. Demonstrirajmo ovo na konkretnom primjeru. Uvedimo u klasu Student konstruktor sa
pet parametara koji redom predstavljaju ime (sa prezimenom), broj indeksa, dan, mjesec i godinu
roenja studenta. Ovaj konstruktor e prvo u konstruktorskoj inicijalizacionoj listi inicijalizirati atribut
datum rodjenja u skladu sa parametrima proslijeenim u konstruktor klase Student, a zatim e
unutar tijela konstruktora postaviti vrijednosti preostala dva atributa ime i prezime i indeks
(atributu ime i prezime postaviemo vrijednost pozivom funkcije strcpy, s obzirom da se radi
o klasinom nizu znakova, a ne objektu tipa string, kojem bi se mogla postaviti vrijednost pomou
operatora dodjele =):
class Student {
char ime i prezime[50];
int indeks;
Datum datum rodjenja;
public:
Student(const char i_i_p[], int indeks, int d, int m, int g) :
datum rodjenja(d, m, g) {
std::strcpy(ime i prezime, i_i_p); Student::indeks = indeks;
}
... // Ovdje bi trebalo definirati ostatak interfejsa klase
};

Neko bi se mogao zapitati zato su se morale uvoditi konstruktorske inicijalizacione liste, odnosno
zato nije doputeno da se pri deklaraciji atributa datum rodjenja odmah napie neto poput
Datum datum rodjenja(14, 5, 1978);

kao kod deklaracija obinih promjenljivih tipa Datum. Tvrorci jezika C++ rukovodili su se sljedeim
rezonom: ukoliko se omogui takvo pojednostavljenje, svi objekti tipa Student bi nakon kreiranja
uvijek imali atribut datum_rodjenja inicijaliziran na istu vrijednost, to najee nema velikog
smisla. Ovako, konstruktorske inicijalizacione liste omoguavaju da konstruktor objekta Student
definira kako e biti inicijaliziran atribut datum_rodjenja. Mada je ovaj rezon posve opravdan, ve
smo vidjeli da su u C++11 standardu tvorci jezika malo popustili svoje tvrde stavove i dozvolili
recimo inicijalizaciju atributa strukture unutar deklaracije same strukture. Slino vrijedi i za klase, tako
da je poev od C++11 mogua i ovakva inicijalizacija atributa datum_rodjenja koja ne koristi
konstruktorsku inicijalizacionu listu:
class Student {
...
Datum datum rodjenja{14, 5, 1978};
...
}

// Samo u C++11...

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Primijetimo da su ovdje za inicijalizaciju atributa datum_rodjenja koritene vitiaste a ne


okrugle zagrade. Naime, slino kao kod struktura, za inicijalizaciju atributa klase na licu mjesta (tj. na
mjestu njihove deklaracije) nije dozvoljena upotreba konstruktorske sintakse (sa okruglim zagradama).
Razlog za ovo prividno nepotrebno ogranienje lei u injenici da kada bi takva sintaksa na tom mjestu
bila dozvoljena, neke komplikovanije konstrukcije u kojima se koristi takva vrsta inicijalizacije mogle bi
biti pogreno protumaene kao prototip neke funkcije lanice, a ne kao inicijalizacija atributa. Tvorci
jezika C++ eljeli su da na ovom mjestu izbjegnu mogunost pojave takvih dvosmislica. Uglavnom,
injenica je da se atribut datum_rodjenja mora nekako inicijalizirati, bilo putem konstruktorske
inicijalizacione liste, bilo na licu mjesta (to je podrano tek od C++11). Meutim, isto tako je jasno da
je inicijalizacija na licu mjesta mogua jedino ukoliko se atribut treba inicijalizirati nekim fiksnim
vrijednostima (koji ne ovise recimo od parametara konstruktora).
Konstruktorske inicijalizacione liste moraju se koristiti za inicijalizaciju svih onih atributa koji
moraju biti propisno inicijalizirani (osim ukoliko nisu inicijalizirani na licu mjesta, o emu smo maloas
govorili). Meutim, one se mogu koristiti i za inicijalizaciju ostalih atributa (koji ne moraju obavezno
biti inicijalizirani odmah po stvaranju), slino kao to se i inicijalizacija promjenljivih sa prostim
tipovima moe obavljati konstruktorskom ili jednoobraznom (samo u C++11) sintaksom. Tako smo i
inicijalizaciju atributa indeks mogli izvriti u konstruktorskoj inicijalizacionoj listi, bez obzira to je
on tipa int (dok sa atributom ime i prezime to nismo mogli uraditi, jer on zahtijeva striktno
kopiranje znak po znak, pomou funkcije strcpy ili upotrebom petlje):
class Student {
char ime i prezime[50];
const int indeks;
// Konstantni atribut!
Datum datum rodjenja;
public:
Student(const char i_i_p[], int indeks, int d, int m, int g) :
datum rodjenja(d, m, g), indeks(indeks) {
std::strcpy(ime_i_prezime, i_i_p);
}
... // Ovdje bi trebalo definirati ostatak interfejsa klase
};

U ovom primjeru moemo uoiti dvije neobinosti. Prvo, atribut indeks deklariran je sa
kvalifikatorom const. Ovim atribut indeks postaje tzv. konstantni (ili nepromjenljivi) atribut.
Takvim atributima se ne moe dodjeljivati vrijednost (odnosno, njihova se vrijednost moe samo itati),
tako da oni itavo vrijeme dok postoji objekat kojem pripadaju zadravaju vrijednost kakvu su dobili
prilikom inicijalizacije. Kao posljedica te injenice, konstantni atributi se mogu inicijalizirati iskljuivo
putem konstruktorskih inicijalizacionih listi (s obzirom da im je nemogue dodjeljivati vrijednost), ili
eventualno na licu mjesta (samo u C++11) to se rijetko radi (s obzirom da je u tom sluaju mogue
izvriti njihovu inicijalizaciju samo na neku fuksnu vrijednost). U navedenom primjeru, deklariranje
atributa indeks kao konstantnog atributa je sasvim opravdano, s obzirom da je broj indeksa
konstantno svojstvo nekog studenta, koje ne bi trebalo da se mijenja tokom itavog ivota objekta koji
opisuje nekog konkretnog studenta. Druga neobinost je upotreba konstrukcije indeks(indeks) u
konstruktorskoj inicijalizacionoj listi. Iako ova konstrukcija djeluje pomalo udno s obzirom da se isto
ime javlja na dva mjesta, ona prosto atribut indeks inicijalizira na vrijednost (istoimenog) formalnog
parametra indeks. Primijetimo da u ovom sluaju, zbog same prirode sintakse, ne nastaje nejasnoa
oko toga ta je atribut, a ta formalni parametar.
Treba primijetiti da ak i u sluaju kada je atribut konstantan, razliiti primjerci klase mogu imati
razliitu vrijednost tog atributa. Meutim, interesantna situacija nastaje kada je neki atribut u isto
vrijeme statiki i konstantan. Njegova vrijednost je tada nepromjenljiva, a pored toga, ta vrijednost je
zajednika za sve primjerke te klase. Stoga se takvi atributi se ne mogu inicijalizirati ak ni pomou
konstruktorskih inicijalizacijskih listi, jer bi tada razliiti primjerci klase mogli izvriti njegovu
inicijalizaciju na razliite vrijednosti. Oni se mogu inicijalizirati na ve opisani nain kako se
inicijaliziraju statiki atributi. Meutim, oni se isto tako mogu inicijalizirati i na licu mjesta (tj. prilikom
deklaracije samog atributa), na isti nain kao to se vri i deklaracija bilo koje konstante, kao na primjer
u sljedeoj deklaraciji:

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

class Student {
static const int broj studenata = 100;
... // Ostatak definicije klase
};

C++11 podrava i sintaksu u kojoj se umjesto znaka = koriste vitiaste zagrade. Meutim,
interesantno je da, mada je mogunost inicijalizacije atributa na licu mjesta uvedena tek u C++11, ova
vrsta atributa (konstantni statiki atributi) su se, pod izvjesnim uvjetima, mogli ovako inicijalizirati jo
od C++98, i to je tada predstavljao jedini izuzetak u kojem se inicijalizacija nekog atributa mogla vriti
direktno na mjestu njegove deklaracije. Ogranienje koje je tom prilikom postojalo je da je tako bilo
mogue inicijalizirati jedino konstantne statike atribute koji su na fundamentalnom nivou cjelobrojni
(recimo koji su tipa int ili char, ili nekog pobrojanog tipa). Tako se, recimo, statiki konstantni
atributi tipa poput double ili string nisu mogli inicijalizirati na ovaj nain, nego iskljuivo na
nain kao i svi drugi statiki atributi (razlog za ovo ogranienje bio je tehnike prirode, vezane za
mogunost da necjelobrojni atributi imaju razliitu internu reprezentaciju na razliitim raunarskim
arhitekturama). Sva ova ogranienja uklonjena su u C++11 standardu.
Postoji jo jedan sluaj kada se atribut mora inicijalizirati u konstruktorskoj inicijalizacionoj listi. To
je sluaj kada atribut predstavlja referencu na neki drugi objekat. Zamislimo, na primjer, da elimo
voditi evidenciju o knjigama u studentskoj biblioteci koje su zaduili pojedini studenti. Za tu svrhu
prirodno je definirati klasu Knjiga koja e sadravati osnovne informacije o svakoj knjizi, poput
naslova, imena pisca, godine izdanja, anra, itd. Meutim, pored osnovnih informacija o samoj knjizi,
potrebno je imati informaciju kod kojeg studenta se nalazi knjiga. Naravno, besmisleno je u klasi
Knjiga imati atribut Student u koji bismo smjetali cjelokupne podatke o studentu koji je zaduio
knjigu, s obzirom da ti podaci svakako ve postoje negdje drugdje (recimo, u objektu koji opisuje tog
studenta). Nas samo interesira ko je zaduio knjigu. Mogli bismo, na primjer, uvesti atribut koji bi uvao
recimo broj indeksa studenta koji je zaduio knjigu, tako da bismo, u sluaju potrebe, pretraivanjem
svih studenata (npr. u nekom nizu studenata) mogli pronai i ostale podatke o tom studentu (pri emu se
podrazumijeva da ne postoje dva studenta sa istim brojem indeksa). Ovaj pristup ima dva nedostatka.
Prvo, traenje studenata po indeksu u velikoj gomili studenata moe biti vremenski zahtjevno (mada se
pretraga moe drastino ubrzati ukoliko se umjesto nizova za uvanje podataka o studentima koriste
recimo mape). Drugo, postoji mogunost da upiemo broj indeksa nepostojeeg studenta (tj. da upiemo
da se knjiga nalazi kod nekog fiktivnog studenta koji uope ne postoji u spisku studenata). Mnogo bolje
rjeenje je u klasu Knjiga uvesti atribut koji predstavlja pokaziva na studenta koji je zaduio knjigu.
Preko takvog pokazivaa mogli bismo, u sluaju potrebe, veoma lako i efikasno dobaviti sve podatke o
odgovarajuem studentu. Stoga bi razumna deklaracija klase Knjiga mogla izgledati recimo ovako:
class Knjiga {
char naslov[100], ime pisca[50], zanr[30];
const int godina izdanja;
Student *kod koga je;
public:
... // Ovdje bi trebalo definirati ostatak interfejsa klase
};

Alternativno, umjesto pokazivaa na studenta, mogli bismo koristiti referencu na studenta (koja je, u
sutini, prerueni pokaziva), kao u sljedeoj deklaraciji:
class Knjiga {
char naslov[100], ime pisca[50], zanr[30];
const int godina izdanja;
Student &kod koga je;
public:

// Atribut referenca!

... // Ovdje bi trebalo definirati ostatak interfejsa klase


};

Koritenjem reference umjesto pokazivaa postiemo dvije prednosti. Prvo, ukoliko koristimo
referencu, moemo koristiti jednostavniju sintaksu (pristup referenci se automatski prevodi u pristup
9

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

objektu na koji ona ukazuje, bez potrebe da runo vrimo dereferenciranje, kao u sluaju pokazivaa).
Drugo, kako reference moraju biti inicijalizirane (tj. mora se zadati objekat za koji e biti vezane), ne bi
se moglo desiti da atribut kod koga je ostane grekom neinicijaliziran. Meutim, upravo zbog
injenice da reference ne smiju ostati neinicijalizirane, njihova inicijalizacija se mora izvriti u
konstruktorskoj inicijalizacionoj listi (poput inicijalizacije konstantnih atributa). U svakom sluaju, bez
obzira da li koristimo pokaziva ili referencu na studenta, konstruktor klase Knjiga kao jedan od
parametara svakako mora imati i studenta koji je zaduio knjigu. Drugim rijeima, jedan od formalnih
parametara konstruktora trebao bi biti referenca (najbolje konstantna) na neki objekat tipa Student i
taj parametar bi se iskoristio za inicijalizaciju atributa-reference kod koga je.
Pristup u kojem se umjesto pokazivaa na studenta koji je zaduio knjigu koristi referenca ipak
posjeduje i dva ozbiljna nedostatka. Prvi nedostatak vezan je za injenicu da referenca sve dok postoji
uvijek ostaje vezana za jedan te isti objekat, to bi znailo da sve dok neki objekat tipa Knjiga
postoji, njegov atribut kod koga je uvijek e ukazivati na jednog te istog studenta. Ukoliko nam
ovakvo ponaanje ne odgovara, reference nisu pogodan izbor za modeliranje ovog atributa. Drugi
problem vezan je za pitanje kako modelirati knjigu (tj. objekat tipa Knjiga) koju niko nije zaduio. U
sluaju da je atribut kod_koga_je pokaziva, prirodno rjeenje u tom sluaju je postaviti taj atribut
na nul-pokaziva. Meutim, ta mogunost otpada ukoliko se umjesto pokazivaa koriste reference. Jedan
od moguih naina da se rijei taj problem je da se uvede fiktivni student (tj. objekat tipa Student) za
kojeg se pretpostavlja da posjeduje one i samo one knjige koje niko nije zaduio (tog studenta moemo
nazvati recimo niko). Tako se knjiga koju nije zaduio niko modelira kao da ju je zaduio niko (tj.
knjiga koja se ne nalazi ni kod koga modelira se kao da se nalazi kod nikoga obratite panju na malu
jeziku zavrzlamu).
Na ovom mjestu nije na odmet raistiti jednu dilemu vezanu za prethodno prikazanu ideju za klasu
Knjiga. Pretpostavimo da je kod koga je pokaziva na objekat tipa Student koji modelira
studenta koji je zaduio knjigu, a ne referenca (mada u principu ista dilema nastaje i ukoliko je u pitanju
referenca). Pretpostavimo dalje da imamo neki primjerak klase Knjiga, recimo neka_knjiga, i
neka smo kreirali drugi primjerak iste klase, recimo druga_knjiga koji smo inicijalizirali objektom
neka_knjiga. Jasno je da je objekat druga_knjiga kopija objekta neka_knjiga. Meutim,
pitanje je da li se radi o plitkoj kopiji ? injenica je da nakon kopiranja oba ta objekta sadre pokaziva
na isti objekat tipa Student. Meutim, da bismo odgovorili na pitanje da li je to plitka kopija ili nije,
postavimo sebi pitanje da li ono na ta pokazuje pokaziva moemo logiki smatrati kao dio objekta u
kojem se taj pokaziva nalazi (tj. kao dio objekta tipa Knjiga). Odgovor je svakako odrean, jer
osobu koja je zaduila knjigu definitivno ne moemo posmatrati kao neto to je sastavni dio te knjige.
Stoga se ovdje ne radi o plitkoj kopiji. Sasvim je druga stvar bila sa ranije razvijenom strukturom
Matrica koja sadri pokaziva pomou kojeg se pristupa elementima matrice, za koje definitivno
moemo rei da logiki pripadaju objektu tipa Matrica. Moe se rei da o plitkoj kopiji moemo
govoriti samo u sluaju kada izvorni i kopirani objekat sadre pokazivae koji pokazuju na iste podatke,
ali za koje se smatra da logiki pripadaju toj klasi. I zaista, mada je za pravljenje potpune kopije neke
matrice potrebno napraviti i kopiju svih njenih elemenata, posve je nelogino smatrati da za potpuno
kopiranje objekta tipa Knjiga treba napraviti i klona osobe koja je zaduila knjigu i povjeriti kopiju te
knjige tom klonu. Vidjeemo kasnije da je za ispravno osmiljavanje neke klase od presudne vanosti
uoiti koji podaci logiki pripadaju toj klasi, a koji ne. Tu se naroito misli na podatke koji nisu
pohranjeni u samim atributima klase, nego negdje sa strane, a kojima se pristupa putem pokazivaa
koji su pohranjeni u atributima klase.
U vezi sa problematikom inicijalizacije atributa klase, prirodno je postaviti pitanje da li je bolje one
atribute koji se ne moraju inicijalizirati u inicijalizacionoj listi ipak inicijalizirati njoj, ili im je bolje
postaviti vrijednosti negdje unutar samog tijela konstruktora. Odgovor je da je neznatno efikasnije
izvriti inicijalizaciju u konstruktorskoj inicijalizacionoj listi. Naime, u tom sluaju se inicijalizacija tih
izvrava uporedo sa stvaranjem objekta, dok se u sluaju kada njihovu postavku izvodimo u tijelu
konstruktora prvo kreiraju neinicijalizirani atributi, koji se inicijaliziraju tek kad im se izvri eksplicitna
dodjela. Ovo podsjea na razliku izmeu deklaracije poput
int broj(5);

// Stvara se inicijalizirani objekat

10

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

i naknadnog dodjeljivanja poput


int broj;
broj = 5;

// Stvara se neinicijalizirani objekat


// Vri se dodjela ve stvorenom objektu

Naravno, atributi koji ne smiju ostati neinicijalizirani (objekti sa konstruktorima, konstantni atributi,
atributi reference) moraju se inicijalizirati u konstruktorskoj inicijalizacionoj listi (tu nemamo izbora), ili
eventualno na licu mjesta prilikom njihove deklaracije (samo u C++11). Inae, upravo zbog opisane
minorne razlike u efikasnosti, a dijelom i zbog jednoobraznosti, preporuuje se da se sve to je mogue
inicijalizirati u konstruktorskoj inicijalizacionoj listi inicijalizira unutar nje. Pri tome se esto deava da
samo tijelo konstruktora ostane potpuno prazno (pogotovo u sluajevima kada je sam postupak
inicijalizacije manje-vie trivijalan). Na primjer, konstruktor klase Kompleksni mogao se napisati i
ovako (sa praznim tijelom):
class Kompleksni {
double re, im;
public:
Kompleksni(double r = 0, double i = 0) : re(r), im(i) {}
... // Ovdje slijedi ostatak interfejsa klase
};

Bitno je naglasiti da kada koristimo konstruktorske inicijalizacione liste, svi atributi pomenuti u listi
se inicijaliziraju onim redoslijedom kako su deklarirani unutar klase, bez obzira na redoslijed navoenja
u listi (razlog za ovu prividnu neloginost vezan je za redoslijed izvravanja tzv. destruktora, o kojima
emo govoriti kasnije). Tako e se, u ranije navedenom primjeru konstruktora klase Student, atribut
indeks inicijalizirati prije atributa datum_rodjenja, bez obzira to je naveden kasnije u
inicijalizacionoj listi! U veini sluajeva redoslijed inicijalizacije nije bitan, meutim u rijetkim
situacijama kada je programeru bitan redoslijed kojim se inicijaliziraju pojedini atributi, treba voditi
rauna o ovoj injenici. Naroito treba izbjegavati inicijalizaciju nekog atributa izrazom koji sadri
vrijednosti drugih atributa (u takvom sluaju redoslijed inicijalizacije moe postati bitan).
Interesantno je napomenuti da je mogunost koja je uvedena u C++11 da se atributi inicijaliziraju
na licu mjesta prilikom njihove deklaracije unutar deklaracije strukture i klase samo sintaksni ukras za
inicijalizaciju putem konstruktorske inicijalizacione liste. Zaista, ukoliko recimo imamo neku strukturu
sa inicijaliziranim atributima kao to je recimo
struct NekaStruktura {
double a;
int b = 3;
std::string c{"Neki tekst!"};
}

kompajler to interno prevodi u neto poput sljedeeg:


struct NekaStruktura {
double a;
int b;
std::string c;
NekaStruktura() : b(3), c{"Neki tekst!"} {}
}

Ve smo rekli da C++11 dozvoljava da se u konstruktorskim inicijalizacionim listama koristi bilo


konstruktorska sintaksa (sa okruglim zagradama), bilo jednoobrazna sintaksa (sa vitiastim zagradama).
Najee je svejedno koja e se od te dvije sintakse koristiti. Meutim, ipak postoje dvije bitne razlike
izmeu te dvije sintakse. Prva razlika je u injenici da postoje tipovi podataka za koje konstruktorska i
jednoobrazna sintaksa u inicijalizaciji nemaju isto dejstvo. Primjer su razni kontejnerski tipovi poput
vektora (recimo, vector<int> v(10) i vector<int> v{10} nema isto dejstvo). Ako imamo
atribute takvih tipova, onda e se i njihova inicijalizacija putem konstruktorske inicijalizone liste odvija
u skladu sa sintaksom koju smo pri tome koristili. Na primjer, neka imamo klasu poput sljedee, u kojoj
je definiran konstruktor bez parametara koji inicijalizira svoje atribute putem konstruktorske
inicijalizacione liste:
11

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

class KlasaSa4Vektora {
std::vector<int> v1, v2, v3, v4;
public:
KlasaSa4Vektora() : v1(10), v2{10}, v3(5, 3), v4{5, 3} {}
... // Ostatak klase
};

U ovom primjeru, atribut v1 e biti inicijaliziran na vektor od 10 elemenata koji su inicijalno nule, dok
e atribut v2 biti inicijaliziran na vektor od samo jednog elementa koji inicijalno ima vrijednost 10.
Atribut v3 bie inicijaliziran na vektor od 5 elemenata koji inicijalno imaju vrijednost 3, dok e atribut
v4 biti inicijaliziran na vektor od dva elementa, koji inicijalno imaju vrijednosti 5 i 3 respektivno.
Interesantno je da je inicijalizacija putem konstruktorske inicijalizacione liste ujedno i jedini nain
da se postavi dimenzija atributa tipa vector odmah pri stvaranju objekta (tj. bez potrebe da mu u
tijelu konstruktora postavljamo veliinu pomou resize funkcije). Zaista, mada C++11 dozvoljava
inicijalizaciju atributa na licu mjesta unutar definicije klase, to se ne moe uraditi konstruktorskom
sintaksom, a jedino njom se moe zadati dimenzija vektora. Ovo je jasno vidljivo iz sljedeeg
(nekorektnog) primjera, koji predstavlja pokuaj da se inicijalizacijama na licu mjesta ostvari isti efekat
kao u prethodnom primjeru. Problem nastaje zbog toga to inicijalizacije atributa v1 i v3 na ovaj
nain nisu legalne:
class KlasaSa4Vektora {
std::vector<int> v1(10), v2{10}, v3(5, 3), v4{5, 3};
public:

// OVO NE RADI!

... // Ostatak klase


};

Druga razlika izmeu upotrebe konstruktorske i jednoobrazne sintakse je to jednoobrazna sintaksa


doputa i inicijalizaciju atributa koji su po tipu nizovi ili strukture (ili, openitije, klase bez konstruktora i
privatnih atributa koji se inicijaliziraju agregatskom inicijalizacijom, poput struktura), dok se takvi
atributi ne mogu inicijalizirati konstruktorskom sintaksom. Recimo, u sljedeem primjeru, inicijalizacija
atributa klase KlasaSaAgregatima ne bi se mogla izvriti konstruktorskom sintaksom:
struct NekaStruktura {
int x, y, z;
};
class KlasaSaAgregatima {
int niz[5];
NekaStruktura struktura;
public:
KlasaSaAgregatima() : niz{3, 5, 2, 3, 6}, struktura{2, 3, 1} {}
... // Ostatak klase
};

Treba naglastiti da ukoliko se konstruktor implementira izvan deklaracije klase (to svakako treba
raditi kad god je konstruktor iole sloeniji) tada se konstruktorska inicijalizaciona lista navodi tek
prilikom implementacije konstruktora, a ne prilikom navoenja njegovog prototipa. Na primjer, ukoliko
bismo eljeli da konstruktor klase Student implementiramo izvan klase, unutar deklaracije klase
Student bi trebalo navesti samo njegov prototip:
class Student {
char ime i prezime[50];
int indeks;
Datum datum rodjenja;
public:
Student(const char i_i_p[], int indeks, int d, int m, int g);
... // Ovdje bi trebalo definirati ostatak interfejsa klase
};

12

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 10_b
Akademska godina 2013/14

Sada bi implementacija konstruktora trebala izgledati ovako:


Student::Student(const char i_i_p[], int indeks, int d, int m, int g) :
datum rodjenja(d, m, g), indeks(indeks) {
std::strcpy(ime i prezime, i_i_p);
}

Nije na odmet napomenuti da se ovaj konstruktor nije mogao implementirati ovako:


Student::Student(const char i_i_p[], int indeks, int d, int m, int g) :
datum rodjenja(d, m, g), indeks(indeks), ime_i_prezime(i_i_p) {}

Prvi razlog je to nije podrano da se nizovni atributi mogu inicijalizirati drugim nizom (ve samo
popisom vrijednosti u vitiastim zagradama), a ak i da je to podrano, parametar i_i_p uope nije
pravi niz, ve pokaziva (s obzirom da ve znamo da formalni parametri nizovnog tipa zapravo nisu
pravi nizovi).
Na kraju, treba prodiskutirati jo jednu stvar. Moda je nekome palo na pamet solomonsko rjeenje
koje bi u mnogim sluajevima eliminiralo potrebu za konstruktorskim inicijalizacijskim listama uvijek
definirati konstruktore bez parametara! Meutim, kao to smo vidjeli, ovakvo rjeenje je veoma
nefleksibilno, jer ne moemo utjecati na postupak inicijalizacije. Treba shvatiti da konstruktorske
inicijalizacijske liste nisu uvedene da bi zakomplicirali ivot programerima, nego upravo suprotno da
olakaju razvoj programa. Stoga, prihvatite sljedei savjet: nemojte koristiti kvazi-rjeenja koja
predstavljaju samo liniju manjeg otpora. Konstruktore bez parametara definirajte samo u sluaju kada
neka klasa zaista treba da ima konstruktor bez parametara, a ne samo zato to ste lijeni da kasnije
poduzmete sve to treba poduzeti zbog injenice to klasa nema konstruktor bez parametara (jo gore
rjeenje je da uope ne definirate konstruktore). Moda Vam se ini da je tako lake. Meutim, ukoliko
tako postupite, na dui rok e se pokazati upravo suprotno: bie vam mnogo tee, pogotovo kad program
ne bude radio onako kao to oekujete...

13

You might also like