You are on page 1of 15

Dr.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

Predavanje 11_a
Kao to smo ve vidjeli, jedna od osnovnih uloga konstruktora je ostvarenje propisne inicijalizacije
objekata. Meutim, kod nekih objekata, konstruktori imaju i jednu dopunsku funkciju, neovisnu od
inicijalizacije zauzimanje dodatnih resursa. Naime, kad god su nekom objektu potrebni neki dodatni
resursi za njegov rad koji nisu sadrani u njegovim atributima, recimo dodatna koliina memorije (koja se
moe ostvariti putem dinamine alokacije memorije), kao to smo recimo imali u strukturi Matrica
kojoj je trebao dodatni memorijski prostor (izvan njenih atributa) za uvanje elemenata matrice,
zauzimanje tih resursa je najbolje izvriti unutar konstruktora. Ti dodatni resursi ne moraju uvijek biti
iskljuivo memorijski resursi, nego se moe raditi o datotekama, vezama sa udaljenim raunarima putem
raunarske mree, pristupnicima bazama podataka, itd. U svakom sluaju, zauzimanje resursa
neophodnih za funkcioniranje nekog objekta najbolje je izvriti odmah prilikom njegove inicijalizacije
(tj. u konstruktoru), jer e na taj nain resursi neophodni za rad objekta biti prisutni odmah po njegovom
stvaranju (za razliku od recimo ranije razvijane strukture Matrica, gdje su objekti tipa Matrica
bili praktino neupotrebljivi dok se ne izvri poziv neke kreirajue funkcije, kakva je recimo bila funkcija
KreirajMatricu. Vrijedi zapravo i obrnuto pravilo po kojem bi brigu o bilo kojem dodatnom
resursu najbolje bilo povjeriti nekom objektu koji bi se brinuo o njegovoj pravilnoj upotrebi, pri emu bi
se zauzimanje tog resursa vrilo prilikom inicijalizacije tog objekta (tj. u njegovom konstruktoru), dok bi
se oslobaanje tog resursa trebalo vriti uporedo sa prestankom postojanja tog objekta (vidjeemo
uskoro kako se to postie). Ta filozofija, karakteristina upravo za jezik C++, poznata je pod akronimom
RAII (Resource Aquisition Is Initialization), u velikoj mjeri je doprinijela popularizaciji jezika C++, i
smatra se da je jedna od njegovih najveih prednosti u odnosu na konkurentske jezike.
Poto je za sada dodatna memorija (ostvarena kroz dinamiku alokaciju) jedini dodatni resurs koji
znamo koristiti, ilustriraemo ovaj pristup upravo kroz zauzimanje dodatne memorike putem dinamike
alokacije. Neka na primjer elimo napraviti klasu nazvanu VektorNd koja e predstavljati vektor u
n-dimenzionalnom prostoru (tj. vektor koji se opisuje sa n koordinata), pri emu dimenzija n nije fiksna,
nego ju je mogue zadavati prilikom kreiranja objekata tipa VektorNd (slino kao kod standardnog
bibliotekog tipa vector). Ako za realizaciju ove klase elimo izbjei razne biblioteke tipove
podataka, jedino mjesto gdje moemo uvati koordinate vektora je u nekom dinamiki alociranom nizu
realnih brojeva, kojem emo pristupati preko odgovarajueg pokazivaa (s obzirom da atributi nizovnog
tipa moraju imati fiksnu veliinu). Slijedi kostur ove klase koji emo kasnije dopunjavati novim
funkcionalnostima (objanjenje uvedenih elemenata slijedi odmah ispod prikaza klase):
class VektorNd {
int dimenzija;
double *koordinate;
void TestIndeksa(int indeks) {
if(indeks < 1 || indeks > dimenzija)
throw std::range_error("Pogrean indeks!");
}
public:
explicit VektorNd(int dimenzija);
void PromijeniDimenziju(int nova_dimenzija);
void PostaviKoordinatu(int indeks, double vrijednost) {
TestIndeksa(indeks); koordinate[indeks 1] = vrijednost;
}
double DajKoordinatu(int indeks) const {
TestIndeksa(indeks); return koordinate[indeks - 1];
}
double &DajKoordinatu(int indeks) {
TestIndeksa(indeks); return koordinate[indeks - 1];
}
void Ispisi() const;
};

Implementacije konstruktora, kao i metoda PromijeniDimenziju i Ispisi su neto sloenije,


stoga su izvedene izvan definicije same klase:

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

VektorNd::VektorNd(int dimenzija) : dimenzija(dimenzija),


koordinate(new double[dimenzija]) {
std::fill(koordinate, koordinate + dimenzija, 0);
}
void VektorNd::PromijeniDimenziju(int nova_dimenzija) {
if(nova_dimenzija > dimenzija) {
novo_mjesto = new double[nova_dimenzija];
std::copy(koordinate, koordinate + dimenzija, novo_mjesto);
std::fill(novo_mjesto + dimeznija, novo_mjesto + nova_dimenzija, 0);
delete[] koordinate; koordinate = novo_mjesto;
}
dimenzija = nova_dimenzija;
}
void VektorNd::Ispisi() const {
std::cout << "{";
for(int i = 0; i < dimenzija; i++) {
std::cout << koordinate[i];
if(i != dimenzija 1) std::cout << ",";
}
std::cout << "}";
}

Analizirajmo sada izvedbu ove klase. Njeni atributi su dimenzija, koji predstavlja dimenziju
vektora, te koordinate, koji predstavlja pokaziva na prvi element dinamiki alociranog niza koji
uva vrijednosti koordinata vektora. Konstruktor ove klase ima jedan parametar koji predstavlja eljenu
dimenziju vektora (razlog zbog kojeg je on deklariran kao eksplicitni objasniemo neto kasnije). Taj
konstruktor odmah pri kreiranju objekta vri neophodnu dinamiku alokaciju memorije (primijetimo da
se ta alokacija vri unutar konstruktorske inicijaliziacione liste, to je sasvim legalno), i inicijalizira sve
koordinate vektora na nulu pozivom funkcije fill (to je efikasnije od upotrebe recimo for-petlje za
istu svrhu). Na taj nain emo biti sigurni da e objekti tipa VektorNd uvijek biti u ispravnom stanju
(ispravno dimenzioniran, sa ispravno zauzetim resursima i sa tano definiranim poetnim sadrajem)
odmah po njihovom stvaranju. Isto tako, zbog nepostojanja konstruktora bez parametara, nee biti
mogue zadavati objekte tipa VektorNd bez specificiranja dimenzije, to svakako ima smisla.
Metoda PromijeniDimenziju namijenjena je da omogui promjenu dimenzije ve postojeeg
vektora (uloga joj je analogna ulozi funkcije resize kod bibliotekog tipa vector). Mada je
upitno da li za n-dimenzionalne vektore kao matematike pojmove naknadna promjena dimenzije uope
ima ikakvog opravdanja, ovdje je ta metoda definirana isto da se stekne uvid u to kako rade funkcije
poput resize. U sluaju da je nova dimenzija vea od postojee, dinamiki se alocira novi prostor na
novom mjestu u memoriji uz kopiranje svih elemenata iz starog u novi prostor (ovaj postupak se naziva
realokacija). Takoer se vri i popunjavanje nulama onih elemenata koji ranije nisu postojali. Na kraju
se stari prostor oslobaa i pokaziva koordinate preusmjerava na novo mjesto. Naravno, pri tome se
aurira i vrijednost atributa dimenzija. Meutim, u sluaju da je nova dimenzija manja od stare,
realokacija je izbjegnuta tako to se samo smanji informacija o dimenziji (koja se uva u atributu
dimenzija dimenzija), tako da e korisnici objekta misliti da se vektoru dimenzija smanjila (bez
obzira to je koliina zauzete memorije ostala ista). Da li se ova varka isplati? U sluaju da se nova i
stara dimenzija malo razlikuju (npr. stara je bila 15 a nova 12), velika je uteda u efikasnosti izbjei
realokaciju, pri emu je cijena koju smo platili to je i dalje fiziki alociran prostor u koji moe stati 15
elementata. S druge strane, u sluaju velike razlike u dimenzijama (npr. smanjenje dimenzije sa 1000 na
10), bolje je izvriti realokaciju, da ne bismo bespotrebno uvali u memoriji i dalje prostor za 990
elemenata koji se vie ne koriste. U naem sluaju, radi jednostavnosti se nismo odluili da podrimo
ovu strategiju, koju moete sami uraditi kao korisnu vjebu.
Metode PostaviKoordinatu i DajKoordinatu dovoljno su jednostavne da uglavnom ne
zahtijevaju posebna objanjenja. Ipak, treba obratiti panju na nekoliko detalja. Prvo, ove metode
pozivaju pomonu privatnu metodu TestIndeksa koja testira da li se indeks (redni broj koordinate)
nalazi u legalnom opsegu od 1 do n i baca izuzetak u sluaju da test nije zadovoljen. Pri tome je
postignuto da se indeksi numeriraju od jedinice, kao to je uobiajeno u matematici. Drugo, primijetimo
da postoje dvije varijante metode DajKoordinatu sa istim tijelom i istim parametrima, samo to je
2

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

jedna oznaena kao inspektor (tj. sa const) a druga nije, pri emu ova druga vraa referencu kao
rezultat. Razlog za ovo dupliranje bie objanjeni uskoro. Konano, imamo i metodu Ispisi koja u
vitiastim zagradama ispisuje koordinate vektora meusobno razdvojene zarezom. Slijedi jednostavan
primjer koji demonstrira upotrebu napisane klase. Primijetimo da je sintaksa kojom se dimenzioniraju
objekti tipa VektorNd analogna sintaksi kojom se dimenzioniraju promjenljive tipa vector iz
istoimene biblioteke. Ovo nije sluajno, s obzirom da dimenzioniranje objekata tipa vector upravo
obavlja konstruktor klase vector:
try {
VektorNd v1(5), v2(3);
v1.PostaviKoordinatu(1, 3); v1.PostaviKoordinatu(2, 5);
v1.PostaviKoordinatu(3, -2); v1.PostaviKoordinatu(4, 0);
v1.PostaviKoordinatu(5, 1); v2.PostaviKoordinatu(1, 3);
v2.PostaviKoordinatu (2, 0); v2.PostaviKoordinatu(3, 2);
std::cout << v1.DajKoordinatu(2) << std::endl;
// 5
v1.Ispisi(); std::cout<< " "; v2.Ispisi();
// {3,5,-2,0,1} {3,0,2}
}
catch(std::bad_alloc) {
std::cout << "Problemi sa memorijom!\n";
}

Neko bi se s pravom mogao zapitati ta je sa oslobaanjem zauzete memorije. Taj problem emo
rijeiti uskoro, a zasad ga jo nismo rijeili. Neko bi mogao pomisliti da je dovoljno napisati neku
metodu, nazvanu recimo OslobodiMemoriju, koju bi mogli pozvati nad objektima tipa VektorNd
kad nam vie ne trebaju, a u svakom sluaju prije nego to objekti prestanu postojati (tada vie neemo
ni imati anse da oslobodimo memoriju). Implementacija ove metode mogla bi se sastojati prosto od
naredbe delete[] koordinate. Meutim, uskoro emo vidjeti zato takvo rjeenje nije dobro, kao
i da postoji mnogo jednostavnije i bolje rjeenje.
Razmotrimo sada ta smo htjeli postii sa dvije verzije metode DajKoordinatu. U sluaju kada
postoje dvije metode istog imena i istih parametara od kojih je jedna oznaena kao konstantna (tj. sa
oznakom const) a druga nije, tada se konstantna verzija poziva samo ukoliko se primijeni nad
konstantnim objektom. U svim ostalim sluajevima, poziva se nekonstantna verzija metode. Dakle, u
ovom primjeru, za sluaj nekonstantnih objekata pozivae se verzija koja vraa referencu na
odgovarajui element niza. Stoga je, u sluaju kada se metoda DajKoordinatu pozove nad
nekonstantnim objektom, njen rezultat l-vrijednost, pa se moe nai sa lijeve strane operatora dodjele
=. To nam omoguava da umjesto pozivom metode DajKoordinatu, neku od koordinata moemo
promijeniti i konstrukcijom poput sljedee, to daje dodatnu fleksibilnost:
v1.DajKoordinatu(1) = 3;

// Djeluje poput v1.PostaviKoordinatu(1, 3)

Pretpostavimo sada da smo napisali samo nekonstantnu verziju ove metode. Tada se ona ne bi
mogla pozivati nad konstantnim objektima, tako da konstantnim objektima tipa Vektor3d ne bismo
mogli ak ni itati koordinate. S druge strane, da smo definirali samo konstantnu metodu
DajKoordinatu koja vraa vrijednost odgovarajueg elementa niza (a ne referencu na njega),
pomou te funkcije bismo mogli samo itati ali ne i mijenjati elemente niza koristei gore prikazanu
sintaksu (dodue, jo uvijek bi nam ostala mogunost promjene koordinata putem metode
PostaviKoordinatu). Konano, da smo definirali samo konstantnu metodu DajKoordinatu
koja vraa referencu na odgovarajui element niza, mogli bismo promijeniti elemente vektora ak i
ukoliko je objekat tipa VektorNd prethodno deklariran kao konstantan! Na prvi pogled ovo izgleda
nemogue, s obzirom da smo pretpostavili da je metoda DajKoordinatu deklarirana kao konstantna,
to joj zabranjuje da mijenja atribute objekta. Meutim, ta metoda zaista ne mijenja niti jedan od
atributa klase VektorNd! Ona samo vraa referencu na element dinamiki kreiranog niza (koji nije
atribut klase), a sama izmjena obavlja se potpuno izvan same metode. Ovako, kada imamo dvie verzije
metode DajKoordinatu, u sluaju poziva nad konstantnim objektom poziva se konstantna verzija
metode koja vraa vrijednost odgovarajueg elementa niza koordinate, to onemoguava da se
poziv ove metode u tom sluaju nae sa lijeve strane operatora dodjele, dok je to mogue u sluaju
poziva ove metode nad nekonstantnim objektom (s obzirom da se vraa referenca kao rezultat). Slijedi
da je, ukoliko elimo da se poziv metode DajKoordinatu moe koristiti i sa lijeve strane operatora
3

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

dodjele, osim u sluaju kada je objekat konstantan, opisano rjeenje sa dvije verzije ove metode je zaista
jedino koje omoguava konzistentno ponaanje u svim situacijama! Istaknimo ipak da je rjeenje koje
koristi pristupne metode PostaviKoordinatu i DajKoordinatu samo privremeno rjeenje dok
ne nauimo kako da klasu VektorNd proirimo tako da moe direktno podravati indeksiranje, tj. da
moemo direktno pisati konstrukcije poput std::cout << v1[1] i v1[1] = 3 sa ciljem pristupa
elementima pridruenog dinamikog niza (uskoro emo vidjeti da je i ovo mogue).
Razmotrimo sada ta je loe u oslobaanju memorije putem neke namjenske metode za tu svrhu
poput OslobodiMemoriju. Prvi (i ujedno najmanji) problem je injenica da je veoma lako zaboraviti
eksplicitno pozvati takvu metodu, to bi svakako dovelo do curenja memorije. Drugi problem je
injenica da bi nakon poziva takve metode objekat bio u neispravnom stanju (pokaziva koordinate
bi postao visei pokaziva), tako da bi svaki pristup tom objektu bio ilegalan. Problem bi se dodue
mogao rijeiti ukoliko bismo pokaziva koordinate nakon brisanja postavili na nul-pokaziva i
zabranili bilo kakvu upotrebu objekta sve dok je taj pokaziva nul-pokaziva. Objekat bi eventualno
mogao ponovo postati ispravan nakon poziva neke metode koja bi ponovo alocirala prostor za smjetanje
koordinata. Meutim, takvo rjeenje uvodi nepotrebne komplikacije. Naredna dva problema su mnogo
ozbiljnija. Naime, razmotrimo ponovo ranije dati primjer upotrebe klase VektorNd sa dva objekta
v1 i v2. S obzirom da su ovi vektori definirani lokalno u try bloku (to smo morali uiniti da
bismo mogli uhvatiti izuzetak koji bi eventualno mogao biti baen iz konstruktora), unitavanje ovih
objekata bi se takoer moralo obaciti unutar try bloka, kao u sljedeem isjeku:
try {
VektorNd v1(5), v2(3);
...
v1.OslobodiMemoriju(); v2.OslobodiMemoriju();
}
catch(std::bad_alloc) {
std::cout << "Problemi sa memorijom!\n";
}

Ovdje bi nam svaki eventualno baeni izuzetak u try bloku nakon to su objekti v1 i v2 ve
stvoreni napravio problem (npr. ukoliko bismo sluajno pozvali metode PostaviKoordinatu ili
DajKoordinatu sa neispravnim indeksom), jer bismo tada izletili izvan try bloka i preskoili
poziv metoda za oslobaanje memorije. Stoga bismo morali hvatati sve takve izuzetke lokalnim try
blokovima smjetenim unutar ovog try bloka, da ih uspijemo obraditi prije nego to naiemo na
poziv metoda za oslobaanje memorije (to je prilino nepraktino). Vjerovatno najvei problem moe
nastati u sluaju da konstrukcija objekta v1 uspije, a konstrukcija objekta v2 ne uspije. Izuzetak koji
e pri tome biti baen bie uhvaen u catch bloku, ali nakon toga vie nemamo mogunost da
obriemo objekat v1 (koji vie nije u vidokrugu)! Jedino mogue rjeenje ovog problema (koristei
samo one jezike elemente koje smo dosad upoznali) bilo bi koritenje ugnijedenih try catch
konstrukcija. Meutim, takvo rjeenje je zaista veoma runo i postaje neprihvatljivo glomazno ukoliko
imamo vei broj objekata a ne samo dva (jer bi za svaki objekat trebao poseban try catch blok).
Da bi se rijeili gore opisani problemi, u jezik C++ su pored konstruktora uvedeni i destruktori, koji
rjeavaju sve ove probleme jednim udarcem. Za razliku od konstruktora, koji definiraju skupinu akcija
koje se automatski izvravaju prilikom stvaranja nekog objekta, destruktori predstavljaju skupinu akcija
koje se automatski izvravaju prilikom unitavanja objekta, tanije kada objekat prestaje postojati
(tipino na kraju bloka unutar kojeg je objekat deklariran, ili po izlasku iz funkcije u kojoj je objekat
deklariran pomou naredbi return ili throw). Zadatak destruktora je da oslobodi dodatne resurse
koje je objekat zauzeo tokom svog ivota, recimo prilikom poziva konstruktora ili neke metode koja vri
zauzimanje dodatnih resursa (ti resursi su obino dinamiki alocirana memorija, ali mogu biti i otvorene
datoteke na disku i razni drugi dopunski resursi koje smo spominjali). Kako se destruktori automatski
pozivaju, ne moe se dogoditi da iz zaboravimo pozvati, odnosno svako unitavanje objekta koji
posjeduje destruktore bie praeno pozivom destruktora. Slino konstruktorima, ni destruktori nemaju
povratni tip, ali za razliku od konstruktora oni ne mogu imati parametre, a ime im je isto kao ime klase u
kojoj se nalaze, samo sa prefiksom ~ (tilda) ispred imena. Slijedi primjer koji pokazuje kako moemo
u klasu VektorNd dodati destruktor, koji e preuzeti ulogu metode OslobodiMemoriju (naravno,
destruktor treba staviti u javni dio klase, inae se nee moi koristiti van funkcija lanica klase):
4

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

class VektorNd {
...
~VektorNd() { delete[] koordinate; }
...

Predavanje 11_a
Akademska godina 2013/14

// umjesto "OslobodiMemoriju"

};

Sada upotreba klase VektorNd postaje mnogo jednostavnija, jer se vie ne moramo eksplicitno
brinuti o oslobaanju dinamiki alocirane memorije koja je zauzeta prilikom stvaranja objekata tipa
VektorNd. Na primjer, u ranije navedenom primjeru upotrebe ove klase, nad objektima v1 i v2
e se automatski pozvati njihovi destruktori nakon kraja bloka unutar kojeg su definirani, tako da se ne
moe desiti da zaboravimo osloboditi memoriju. To e se desiti ne samo u sluaju prirodnog zavretka
bloka, nego i u sluaju da doe do naputanja bloka usljed bacanja nekog izuzetka, tako da nas ni
nepredvieni izuzeci ne trebaju ni najmanje brinuti. Takoer, lijepa je stvar to se destruktori pozivaju
samo nad objektima koji su zaista stvoreni (pod stvorenim objektom smataramo objekat iji se
konstruktor zavrio regularno, a ne bacanjem izuzetka). Tako, ukoliko na primjer svaranje objekta v1
uspije, a objekta v2 ne uspije, doi e do bacanja izuzetka i naputanja try bloka. Tada e
automatski biti pozvan destruktor nad objektom v1, koji e ga unititi. Destruktor nad objektom v2
koji nije stvoren nee se ni pozvati, a to nam upravo i treba.
U ve pomenutoj RAII filozofiji, podrazumijeva se da se resursi koji se zauzmu prilikom
inicijalizacije objekta (tj. unutar konstruktora) obavezno oslobaaju prilikom unitavanja objekta (tj.
unutar pripadnog destruktora). Zbog toga se ovaj pristup esto naziva i akronimom CADRe (Constructor
Acquires, Desctructor Releases). Na taj nain se vrijeme u toku kojeg je neki resurs zauzet izjednauje sa
vremenom u toku kojeg ivi objekat kojem je povjerena briga o tom resursu. Drugim rijeima, svako
unitavanje objekta kojem je povjerena briga o nekom resursu, bez obzira na koji nain je izazvano,
automatski dovodi i do oslobaanja odgovarajueg resursa. Na taj nain curenje resursa (npr. curenje
memorije) postaje praktino nemogue.
Ve je reeno da se destruktori uope ne pozivaju ukoliko objekat nije stvoren, tj. ukoliko se
konstruktor nije do kraja izvrio. To ima za posljedicu da konstruktor nikada ne smije ostaviti iza sebe
polovino stvoren objekat, jer tada niko nee obrisati niti e biti u stanju da obrie ono to je iza sebe
ostavio konstruktor. Drugim rijeima, konstruktor prije nego to baci izuzetak (ukoliko utvrdi da ga
mora baciti) mora iza sebe poistiti svo smee koje je iza sebe ostavio. To se tipino deava ukoliko se
u konstruktoru dinamiki alocira vie nizova: ukoliko se prvo izvri nekoliko uspjenih alokacija, a
zatim jedna neuspjena, konstruktor prije nego to baci izuzetak treba da pobrie sve uspjene alokacije
(jer ih u protivnom niko nee obrisati). To se moe uraditi tako to se unutar konstruktora ugradi try
blok koji e uhvatiti eventualno neuspjenu alokaciju, nakon ega se u catch bloku moe poistiti
zaostalo smee prije nego to se zaista baci izuzetak iz konstruktora.
Bitno je napomenuti da bacanje izuzetaka iz destruktora treba izbjegavati po svaku cijenu (sreom,
ovo je rijetko potrebno). Naime, bacanje izuzetaka iz destruktora moe u nekim situacijama izazvati
veoma udne efekte (koji obino zavravaju krahom programa), zbog ega je najbolje bacanje izuzetaka
iz destruktora potpuno izbjei. Takoer, destruktore nikada ne treba eksplicitno pozivati kao obine
funkcije lanice, mada sintaksa to dozvoljava (npr. konstrukcija poput v1.~VektorNd() je sintaksno
legalna). Ukoliko to uinite, samo traite sebi probleme. Destruktori se ekplicitno pozivaju jedino u
nekim vrlo specifinim situacijama koje spadaju u neke vrlo napredne programerske tehnike, koje znatno
prevazilaze opseg ovog kursa. Stoga destruktore treba pustiti da se automatski pozivaju tamo gdje je to
potrebno, a inae ih treba ostaviti na miru (osim ako o njima ne znate znatno vie nego to je ovdje
prezentirano). Na prvom mjestu, ukoliko bismo pozvali destruktor eksplicitno, mogli bismo imati
probleme sa brisanjem ve obrisanih resursa u sluaju kad se kasnije isti destruktor pozove automatski
(osim ako preduzmemo posebne mjere protiv viestrukog brisanja, koje inae ne bi bile potrebne). Drugi
problem je to destruktori, slino konstruktorima, nisu funkcije lanice, i oni uvijek izvravaju i neke
podrazumijevane akcije koje uope nisu specificirane u tijelu konstruktora (recimo, destruktori neke
klase automatski pozivaju destruktore nad svim njenim atributima iji tipovi takoer posjeduju
destruktore). Stoga, ukoliko bismo eksplicitno pozvali destruktor, izvrilo bi se ne samo ono to pie u
njegovom tijelu, nego i te podrazmijevane akcije, to vjerovatno nije ono to smo eljeli. Stoga, ukoliko
Vam je potrebno da iz neke druge funkcije izvrite posve iste naredbe koje su sadrane u tijelu
5

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

destruktora, nemojte eskplicitno pozivati destruktor kao funkciju lanicu, nego definirajte pomonu
funkciju (tipino u privatnoj sekciji klase) unutar koje ete smjestiti te naredbe, a zatim tu pomonu
funkciju pozovite iz destruktora, i sa bilo kojeg drugog mjesta gdje su Vam te naredbe potrebne.
Destruktori se takoer automatski pozivaju i pri upotrebi operatora delete i delete[] u
sluaju da je pomou operatora new stvoren objekat koji sadri konstruktore i destruktore. Ovo nije
nimalo iznenaujue, jer tom prilikom takoer dolazi do unitavanja pripadnih objekata (samo to oni
nemaju svoja imena). Stoga delete, slino kao i new, takoer pokree dvoetapni proces: prva etapa
je poziv odgovarajueg destruktora (ukoliko isti postoji), a tek tada slijedi oslobaanje zauzete
memorije. Na primjer, ukoliko smo dinamiki stvorili neki objekat tipa VektorNd naredbom poput
VektorNd *pok_na_vektor(new VektorNd(5));

tada e njegovo brisanje pomou naredbe


delete pok_na_vektor;

izazvati dva efekta: prvo e nad dinamiki stvorenim objektom *pok_na_vektor biti pozvan
destruktor (koji e osloboditi memoriju koja je zauzeta u konstruktoru objekta), pa e tek tada biti
osloboena memorija koji je sam objekat *pok_na_vektor zauzimao (tj. prostor za njegove atribute
dimenzija i koordinate). Slino, prilikom upotrebe operatora delete[] za brisanje nekog
dinamiki stvorenog niza, prije samog brisanja nad svakim elementom niza bie pozvan njegov
destruktor (ukoliko takav postoji). Ovim je pokazana jedna (od mnogih) razlika izmeu operatora
delete i delete[] (u sluaju da smo umjesto delete[] stavili delete, destruktor bi bio
izvren samo jednom, a ne nad svakim elementom niza), koja ukazuje da ova dva operatora ne treba
mijeati i da svaki treba koristiti iskljuivo za ono za ta je namijenjen!
Ostali smo duni da objasnimo zbog ega smo konstruktor klase VektorNd definirali kao
eksplicitni konstruktor. Da ovo nismo uradili, sasvim bi mogue bilo napisati neto poput
v1 = 7;

Na osnovu specijalne uloge koju imaju konstruktori sa jednim parametrom (automatska pretvorba
tipova), ovakva naredba bila bi interpretirana kao
v1 = VektorNd(7);

ime bi se postigao sasvim neoekivan efekat: stvorio bi se novi, sedmodimenzionalni vektor, koji bi bio
dodijeljen objektu v1 (a usput bi se pojavio i mnogo ozbiljniji problem uzrokovan plitkim kopiranjem,
o kojem emo govoriti u neto kasnije). Slina neoekivana situacija nastala bi ukoliko bi nekoj funkciji
koja oekuje objekat tipa VektorNd kao parametar bio proslijeen broj (taj broj bi bio proslijeen
konstruktoru sa jednim parametrom, nakon ega bi konstruisani objekat bio proslijeen funkciji, a to
sigurno nije eljeno ponaanje). Ovako, oznaavanjem konstruktora sa explicit, zabranjuje se
njegovo koritenje za automatsku pretvorbu tipova iz cjelobrojnog u tip VektorNd, tako da ovakve
besmislene konstrukcije nee biti ni dozvoljene (tj. dovee do prijave greke od strane kompajlera). U
sutini, treba se pridravati jednostavnog pravila: kad god parametar konstruktora sa jednim parametrom
ne slui za inicijalizaciju vrijednosti objekta nego za definiranje interne strukture objekta (kao u ovom
primjeru), takav konstruktor bi trebao biti eksplicitni konstruktor.
Za sada, za objekte tipa VektorNd nema nikakve razlike u inicijalizaciji pomou okruglih i
vitiastih zagrada (ovo vrijedi samo za C++11), odnosno konstrukcije poput VektorNd v(5) i
VektorNd v{5} za sada imaju isto dejstvo. Meutim, lijepo bi bilo da objekte tipa VektorNd
moemo inicijalizirati navoenjem elemenata u vitiastim zagradama, odnosno da moemo pisati recimo
VektorNd v{3, 5, 2, 6, 1};

// Ovo zasad ne radi...

Takoer, lijepo bi bilo da kroz koordinate objekata tipa VektorNd moemo prolaziti rangovskom
for-petljom. Sreom, sve ovo je posve lako postii (samo u C++11). Da bismo postigli mogunost
inicijalizacije navoenjem elemenata, potrebno je definirati jo jedan konstruktor nazvan sekvencijski
konstruktor ili konstruktor iz inicijalizacijske liste (engl. sequence constructor odnosno initializer-list
6

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

constructor). Takav konstruktor prima objekat tipa initializer_list kao parametar. Kada se za
inicijalizaciju koristi inicijalizaciona lista, ukoliko postoji odgovarajui sekvencijski konstruktor koji
odgovara tipu elemenata u inicijalizacionoj listi, bie pozvan on umjesto obinih konstruktora, to emo
detaljnije objasniti u nastavku. S druge strane, da bismo podrali prolazak rangovskom for-petljom kroz
elemente, sve to je potrebno izvesti je u klasu dodati funkcije lanice koje se moraju zvati begin ili
end. Te funkcije moraju vratiti kao rezultat nekakav objekat koji se ponaa makar kao iterator sa
kretanjem unaprijed i koji moe prolaziti kroz elemente kolekcije. Naravno, begin treba vratiti takav
objekat koji je na neki nain vezan za prvi element kolekcije, a end na element iza kraja kolekcije. U
naem primjeru, dovoljno je vratiti obine pokazivae, jer je odgovarajua kolekcija koja uva elemente
vrlo jednostavna (obini niz, iji elementi su kontinualni u memoriji). Stoga bi izmjene koje treba
napraviti u javnom dijelu klase VektorNd izgledale ovako:
class VektorNd {
...
VektorNd(std::initializer_list<double> lista);
...
double *begin() const { return koordinate; }
double *end() const { return koordinate + dimenzija; }
};

Implementacije metoda begin i end su vrlo jednostavne, pa smo ih izveli odmah unutar
definicije klase. Inae, implementacijom ovih metoda omoguili smo ne samo rangovske for-petlje sa
objektima tipa VektorNd, nego i koritenje takvih objekata sa funkcijama iz biblioteke algorithm.
to se tie sekvencijskog konstruktora koji smo ovdje stavili, on e biti pozvan kad god se za
inicijalizaciju objekta upotrijebi inicijalizacijska lista iji su svi elementi tipa double ili se mogu
automatski konvertirati u tip double. Tip initializer_list je jedan vrlo jednostavan tip (za
njegovo koritenje treba ukljuiti istoimenu biblioteku) koji slui za pristup elementima inicijalizacionih
listi i sadri jedino odgovarajui iterator sa direktnim pristupom za kretanje kroz inicijalizacionu listu
(elementi inicijalizacionih listi se mogu samo itati, a ne i mijenjati), koji je zapravo implementiran kao
obini pokaziva, te funkcije lanice begin, end i size (i nita drugo). Ovo e biti jasno iz
primjera mogue implementacije sekvencijskog konstruktora za ovu klasu:
VektorNd::ektorNd(std::initializer_list<double> lista) :
dimenzija(lista.size()), koordinate(new double[lista.size()]) {
std::copy(lista.begin(), lista.end(), koordinate);
}

Inae, tip initializer_list moe se koristiti neovisno od sekvencijskih konstruktora kad god
nam je potreban brz i efikasan pristup elementima inicijalizacijske liste. Recimo, neka elimo napraviti
funkciju NekaFunkcija koja moe primiti inicijalizacionu listu kao parametar, tako da moemo
pisati recimo NekaFunkcija({3, 5, 2, 4, 7}). Ranije smo vidjeli da to moemo postii tako to
emo staviti da parametar funkcije NekaFunkcija bude recimo tipa vector. Meutim, na taj
nain efektivno dolazi do konverzije inicijalizacione liste u vektor, to dovodi do nepotrebnih
komplikacija i gubitaka na efikasnosti. Ako e ova funkcija primati samo inicijalizacione liste kao
parametre, mnogo je pogodnije staviti da njen parametar bude upravo tipa initializer_list.
Dok bi konstruktore trebala da posjeduje praktino svaka klasa, destruktori su potrebni samo
ukoliko klasa koristi dodatne resurse u odnosu na resurse koji predstavljaju njeni atributi sami po sebi,
npr. ukoliko kreira dinamiki alocirani prostor u memoriji. Zadatak destruktora je tada da oslobodi sve
dodatne resurse koje je neki primjerak klase zauzeo, prije nego to taj primjerak prestane postojati. S
obzirom da su destruktori veoma korisni i oslobaaju nas mnogih problema, a njihova deklaracija (a
esto i implementacija) je sasvim jednostavna, moe smatrati pravilom da svaka klasa koja vri dodatno
zauzimanje raunarskih resursa (recimo dinamiku alokacija memorije) bilo iz konstruktora bilo iz neke
druge metode obavezno mora imati definiran destruktor, koji e osloboditi sve resurse koji su dodatno
zauzeti tokom ivota primjeraka te klase. Tehniki gledano, destruktore zapravo ima svaka klasa ak i
ukoliko nije posebno definiran, jer u sluaju kada se ne definira destruktor, kompajler automatski
generira podrazumijevani destruktor sa praznim tijelom (i koji stoga izvrava samo podrazumijevane
akcije koje inae svaki destruktor uvijek izvrava).

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

Naalost, mada destruktori rjeavaju brojne probleme, oni takoer i stvaraju neke nove (sreom
rjeive) probleme. Naime, injenica da se destruktori uvijek automatski pozivaju nad objektom
neposredno prije nego to objekat prestane postojati moe dovesti do nepredvidljivog i veoma opasnog
ponaanja u sluajevima kada postoji vie identinih primjeraka iste klase. Ovi problemi su u osnovi
uzrokovani plitkim kopiranjem i rjeavaju se definiranjem vlastitog kopirajueg konstruktora (umjesto
podrazumijevanog automatski generiranog) i vlastitog operatora dodjele (umjesto podrazumijevanog).
Oni su toliko vani za ispravno funkcioniranje klase da se kao pravilo moe uzeti da svaka klasa koja
posjeduje vlastiti destruktor (tj. onaj koji nije automatski generiran), obavezno mora posjedovati i vlastiti
konstruktor kopije i vlastiti operator dodjele. Ovo se esto naziva zakon velike trojke (engl. The Law of
Big Three). Stoga iznenauje da postoje brojne knjige o jeziku C++ koje konstruktore kopije spominju
vie uzgredno ili ak i nikako. Definiranju vlastitog operatora dodjele se takoer ne pridaje dovoljna
panja: on se obino opisuje u okviru openite prie o preklapanju operatora i to vie kao kuriozitet nego
kao neto to je vitalno za ispravno funkcioniranje klase.
Ve smo rekli da je konstruktor kopije (ili kopirajui konstruktor) specijalan sluaj konstruktora sa
jednim parametrom, iji je formalni parametar referenca na konstantni objekat klase kojoj pripada.
Njegova uloga je da omogui upravljanje postupkom koji se odvija prilikom kopiranja jednog objekta u
drugi. Da bismo uvidjeli potrebu za definiranjem vlastitog konstruktorom kopije, razmotrimo ta se
tipino deava kada se jedan objekat kopira u drugi (npr. objekat A u objekat B). Do ovakvog kopiranja
moe doi u etiri situacije: kada se neki objekat inicijalizira drugim objektom istog tipa, kada se neki
objekat prenosi po vrijednosti u neku funkciju (tada se stvarni parametar kopira u odgovarajui formalni
parametar), zatim kada se neki objekat vraa kao rezultat iz funkcije, te kada se vri dodjeljivanje nekog
objekta drugom objektu istog tipa. U svim ovim sluajevima osim posljednjeg (o emu emo posebno
govoriti) to kopiranje vri se posredstvom kopirajueg konstruktora. Pri tome podrazumijevani
kopirajui konstruktor prosto kopira sve atribute objekta A u odgovarajue atribute objekta B. Meutim,
kada god objekti sadre atribute koji su pokazivai koji pokazuju na dinamiki alocirane resurse koji
logiki pripadaju tom objektu (ovo ne ukljuuje pokazivae koji pokazuju na objekte koji nisu dinamiki
alocirani, ili objekte koji logiki ne pripadaju tom objektu, o emu smo ranije govorili u kontekstu
odnosa izmeu objekata tipa Knjiga i Student), ovo podrazumijevano kopiranje moe dovesti do
kreiranja tzv. plitke kopije o emu smo takoer govorili (kada smo opisivali strukturu Matrica), u
kojoj nakon kopiranja oba objekta sadre pokazivae koje pokazuju na iste resurse u memoriji, koji pri
tome logiki pripadaju objektu. U kombinaciji sa destruktorima, plitke kopije mogu biti fatalne. Na
primjer, pretpostavimo da objekti A i B sadre pokazivae na isti dinamiki niz u memoriji i da zbog
nekog razloga objekat A prestane postojati. Tom prilikom e se pozvati njegov destruktor, koji
najvjerovatnije unitava taj dinamiki niz. Meutim, objekat B (koji i dalje ivi) sada sadri pokaziva
na uniteni niz (tj. visei pokaziva) i dalje posljedice su nepredvidljive!
Razmotrimo jedan konkretan, naizgled bezazlen primjer koji ilustrira ovo o emu smo govorili.
Pretpostavimo da elimo ranije napisanu klasu VektorNd proiriti funkcijom ZbirVektora koja
vraa kao rezultat zbir dva n-dimenzionalna vektora koji su joj proslijeeni kao parametri.
Implementacija ove funkcije mogla bi izgledati recimo ovako (parametre v1 i v2 namjerno
prenosimo po vrijednosti, da bismo ukazali na problem o kojem elimo govoriti):
VektorNd ZbirVektora(VektorNd v1, VektorNd v2) {
if(v1.dimenzija != v2.dimenzija)
throw std::domain_error("Nesaglasne dimenzije!");
VektorNd v3(v1.dimenzija);
for(int i = 0; i < v1.dimenzija; i++)
v3.koordinate[i] = v1.koordinate[i] + v2.koordinate[i];
return v3;
}

Da bismo ovoj funkciji omoguili pristup privatnim lanovima klase, deklariraemo je klase kao
funkciju prijatelja klase (u suprotnom ona ne bi mogla nikako saznati dimenziju vektora):
class VektorNd {
...
friend VektorNd ZbirVektora(VektorNd v1, VektorNd v2);
};

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

Na prvi pogled je sve u redu, tako da je, uz pretpostavku da su a i b dva vektora iste dimenzije,
mogue napisati naredbu poput
VektorNd c(ZbirVektora(a, b));

// OVO MOE NAPRAVITI PROBLEM!!!

Na alost, nije sve ba tako lijepo kao to izgleda. Napisana klasa sadri ozbiljan propust, a gore
prikazana naredba moe dovesti do tekih posljedica koje se mogu oitovati tek nakon izvjesnog
vremena. Naime, nakon gornje naredbe sva tri vektora a, b i c sadravae visee pokazivae! Da
bismo vidjeli zato, razmotrimo ta se zaista deava pri gornjem pozivu. Prvo dolazi do kopiranja
stvarnih parametara a i b u formalne parametre v1 i v2. Pri tome nastaju plitke kopije, u kojima
objekti v1 i a odnosno v2 i b sadre pokazivae na iste dijelove memorije. Nakon toga se
formira lokalni vektor v3 koji se popunjava zbirom vektora v1 i v2. Ovaj vektor se vraa kao
rezultat funkcije pri emu dolazi do njegovog kopiranja u vektor c (stoga e pokazivai u vektorima
v3 i c pokazivati na isti dio memorije). Meutim, po zavretku funkcije, unitavaju se sva tri lokalna
vektora v1, v2 i v3 (formalni parametri su takoer lokalni objekti). Prilikom ovog unitavanja
dolazi do pozivanja destruktora klase VektorNd nad svakim od ova tri objekta, koji e osloboditi
memoriju koju su zauzimali dinamiki alocirani nizovi na koje pokazuju pokazivai unutar vektora
v1, v2 i v3 (to i jeste osnovni zadatak destruktora). Meutim, vektori a, b i c sadre
pokazivae koji pokazuju na iste dijelove memorije, tako da e nakon izvrenja ove naredbe sva tri
vektora sadravati pokazivae koji pokazuju na upravo osloboene dijelove memorije. Stoga, njihovo
dalje koritenje moe imati kobne posljedice!
ta da se radi? Kopiranje vektora a i b u v1 i v2 bismo mogli lako izbjei ukoliko bismo
parametre prenosili po referenci na konstantne objekte (tj. ako bismo deklarirali da su v1 i v2
reference na konstantne objekte tipa VektorNd). Kao to znamo, ovo je svakako i preporueni nain
prenosa primjeraka klasa u funkcije. Meutim, kopiranje rezultata koji se nalazi u vektoru v3 nije
mogue izbjei ak ni vraanjem reference na njega kao rezultata (ne smijemo vratiti referencu na
objekat koji prestaje postojati). Oigledno, svi problemi nastaju zbog plitkog kopiranja (stoga se esto
kae da se destruktori i plitke kopije meusobno ne vole. Pravo rjeenje je promijeniti mehanizam
kopiranja, tako da kopija objekta dobija ne samo kopiju pokazivaa nego i kopiju odgovarajueg
dinamikog niza pridruenog pokazivau. Na taj nain e destruktor kopiranog objekta unititi svoj a
ne i tui dinamiki niz. Sljedea slika ilustrira razliku izmeu plitke i potpune (duboke) kopije:
Plitka kopija:

Duboka kopija:

v1

v1

dimenzija koordinate

dimenzija koordinate

dimenzija koordinate

dimenzija koordinate

Neko se moe zapitati zbog ega podrazumijevani konstruktori kopije u jeziku C++ ne vre postupak
dubokog nego plitkog kopiranja. Za to postoje dva razloga. Prvo, kompajler ne moe znati na ta
pokazuju pokazivai koji se nalaze unutar objekta (recimo, da li pokazuju na dinamiki alocirane resurse
koji logiki pripadaju klasi ili na neto to uope nije sastavni dio klase), jer to moe zavisiti od toga ta
je programer radio sa tim pokazivaima u itavom ostatku programa (kompajler moe znati samo koju
adresu sadri pokaziva, ali ne moe znati ta logiki predstavlja objekat koji se tamo nalazi). Drugo,
nekada nam duboke kopije mogu praviti problem sa aspekta efikasnosti, pa bismo eljeli nekako ipak
koristiti plitke kopije, ali da ih pri tome nekako izmirimo sa destruktorima (o tome emo govoriti
kasnije). Zbog toga je omogueno da se definiraju vlastiti konstruktori kopije, koji omoguavaju
projektantu klase da sm definira postupak kako se objekti te klase trebaju kopirati, odnosno da po
potrebi implementira duboko kopiranje objekata na nain kako mu to odgovara.
Na osnovu prethodnog izlaganja, slijedi da je rjeenje opisanog problema sa klasom VektorNd
definiranje vlastitog konstruktora kopije koji e kreirati potpunu (duboku) kopiju objekta. Stoga emo u
javni dio klase dodati i deklaraciju vlastitog konstruktora kopije:
9

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

class VektorNd {
...
VektorNd(const VektorNd &v);
...

// Deklaracija konstruktora kopije

};

S obzirom da konstruktor kopije nije posve kratak, njegovu implementaciju emo izvesti izvan
deklaracije klase. Razmotrimo ta on zapravo treba da obavi. Atribut dimenzija svakako treba
kopirati, ali atribut-pokaziva koordinate ne treba prosto da se kopira. Umjesto toga, treba stvoriti
novi dinamiki niz i kopirati izvorni dinamiki niz u novostvoreni niz. Radi bolje efikasnosti, sve
atribute koji se mogu inicijalizirati u konstruktorskoj inicijalizacijskoj listi emo tako i inicijalizirati.
Takoer, za kopiranje elemenata, efikasnije je koristiti funkciju copy iz biblioteke algorithm
nego vriti kopiranje element po element uz pomo for-petlje. Stoga bi implementacija konstruktora
kopije za klasu VektorNd mogla izgledati recimo ovako:
VektorNd::VektorNd(const VektorNd &v) : dimenzija(v.dimenzija),
koordinate(new double[v.dimenzija]) {
std::copy(v.koordinate, v.koordinate + v.dimenzija, koordinate);
}

Treba napomenuti da su konstruktori kopije nuno zlo i da kompajleri imaju pravo (ali ne i
obavezu) da izbjegnu njihovo pozivanje kada je god to mogue. To izbjegavanje poznato je pod nazivom
elizija konstruktora kopije. Na primjer, ukoliko kompajler primijeti da se objekat A kopira u objekat B,
koji se zatim kopira u objekat C i koji se na kraju kopira u objekat D pri emu se nakon toga objekti B i
C ne koriste nizata (recimo, prestaju postojati), kompajler ima pravo direktno kopirati objekat A u
objekat D uz samo jedan poziv konstruktora kopije (umjesto tri). Dalje, ukoliko kompajler primijeti da
se konstruira objekat A koji se nakon toga kopira u objekat B a zatim u objekat C pri emu se nakon toga
objekti A i B ne koriste nizata, kompajler ima pravo da u potpunosti izbjegne kopiranja (i pozive
konstruktora kopije) i da direktno konstruira objekat C na isti nain kako bi konstruirao objekat A. Stoga
konstruktori kopije nikada ne bi trebali obavljati nita to izlazi iz okvira postupaka za kopiranje
objekata, jer nikada ne moemo biti sigurni koliko e se puta i kada konstruktori kopije zaista pozvati (s
obzirom da kompajler ima pravo izbjei njihov poziv kad god je to mogue). Posebno, logika programa
ne bi smjela ovisiti od toga koliko e se puta konstruktor kopije pozvati!
esto se javlja situacija da objekti koji se kopiraju imaju samo privremeni status i zna se da e biti
uniteni im se obavi njihovo kopiranje. Recimo, pretpostavimo da nekoj funkciji koja prima objekat
tipa VektorNd po vrijednosti poaljemo kao parametar izraz poput ZbirVektora(a, b). U ovom
sluaju, kreira se privremeni objekat koji predstavlja rezultat funkcije ZbirVektora, koji se potom
kopira u novu funkciju posredstvom kopirajueg konstruktora i odmah unitava. Prirodno se postavlja
pitanje ako ve znamo da e taj privremeni objekat biti uniten (i nee kasnije trebati nizata), zar nije
bolje izbjei kopiranje i prosto ukrasti tom objektu pokaziva na dinamiki alocirani niz (kao kod
plitkog kopiranja), ali da pri tom nekako sprijeimo brisanje tog dinamiki alociranog niza kada bude
nad tim privremenim objektom pozvan destruktor. To bi, u izvjesnom smislu bilo premjetanje
(odnosno pomjeranje) dinamiki alociranog niza iz objekta koji se kopira u objekat u koji se vri
kopiranje. Da bi se omoguilo tako neto, u verziji C++11 uveden je jo jedan tip konstruktora nazvan
pomjerajui konstruktor (engl. move constructor). Za razliku od kopirajueg konstruktora, njegov
parametar je r-vrijednosna referenca na objekat tipa klase u kojoj se definira, tako da se on koristi samo
za kopiranje r-vrijednosti, a poznato je da samo privremeni objekti imaju status r-vrijednosti (dok su
imenovani objekti l-vrijednosti). Pogledajmo kako bi mogao izgledati pomjerajui konstruktor za klasu
VektorNd (s obzirom da na kratkou, njegova implementacija je data odmah unutar definicije klase):
class VektorNd {
...
VektorNd(VektorNd &&v) : dimenzija(v.dimenzija),
koordinate(v.koordinate) { v.koordinate = nullptr; }
...
};

10

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

Razmotrimo kako ovaj konstruktor radi. Kako mu je parametar r-vrijednosna referenca, on sigurno
zna da je ono to mu je proslijeeno neki privremeni a ne imenovani objekat, koji e svakako biti
uniten. On zato prosto kopira pokaziva na dinamiki alocirani niz iz objekta koji se kopira u odredini
objekat umjesto kreiranja novog niza i kopiranja jednog niza u drugi (naravno, kopira se i informacija o
dimenziji), a zatim postavlja pokaziva u izvornom objektu na nul-pokaziva. Posljedica je da kada se
nad izvornim objektom pozove destruktor, on nee uraditi nita, jer e se izvriti naredba delete nad
nul-pokazivaem. Na taj nain su resursi prosto premjeteni iz izvorinog u odredini objekat.
Ukoliko klasa ima definiran i pomjerajui konstruktor, za tu klasu automatski postaje podrana i
move-semantika prilikom inicijalizacije objekata ili prenosa parametara u funkciju, odnosno prilikom
vraanja rezultata iz funkcije. Recimo, pretpostavimo da je a neki objekat tipa VektorNd i da
izvrimo inicijalizaciju poput
VektorNd b(std::move(a));

Sve to funkcija move radi je mijenjanje statusa svog argumenta iz l-vrijednosti u r-vrijednost. Stoga
e za inicijalizaciju objekta a koristiti pomjerajui a ne kopirajui konstruktor, koji e prosto
premjestiti resurse iz objekta b u a (a objekat a ostaviti u neupotrebljivom stanju sa nulpokazivaem u atributu koordinate, ali svakako smo pozivom funkcije move signalizirali da nam
taj objekat vie ne treba). Inae, ukoliko klasa nema definiran pomjerajui konstruktor, prilikom
kopiranja se uvijek koristi kopirajui konstruktor, bez obzira da li se kopira l-vrijednost ili r-vrijednost.
Vidjeli smo da vlastiti definirani konstruktori kopije mogu obezbjediti duboko kopiranje, ime
moemo biti sigurni da e svi razliiti objekti imati svoje neovisne primjerke alociranih resursa, odnosno
da se nee jedan drugom petljati u posao. Ipak, konstruktor kopije se eventualno poziva samo u tri od
etiri situacije u kojima bi mogle nastati plitke kopije. Naime, ve smo rekli da se konstruktor kopije
eventualno poziva pri inicijalizaciji novostvorenog objekta drugim objektom istog tipa, pri prenosu po
vrijednosti objekata u funkcije i pri vraanju objekata kao rezultata iz funkcije. Meutim, prilikom
dodjeljivanja nekog objekta drugom objektu koji od ranije postoji, konstruktor kopije se nikada ne
poziva! To znai da ukoliko bismo izvrili naredbe poput
b = a;
c = ZbirVektora(a, b);

pri emu sva tri vektora (tj. objekta tipa VektorNd) a, b i c postoje od ranije, u oba sluaja e
biti izvrena plitka kopija, bez obzira to smo definirali vlastiti konstruktor kopije! Razlog zato se
konstruktor kopije ne poziva i u ovom sluaju je to se ovaj sluaj razlikuje od prva tri po tome to
objekat kojem se vri dodjela ve od ranije postoji, pa konstruktor kopije ne moe da zna ta treba da
radi sa prethodnim sadrajem objekta (u sva tri preostala sluaja radi se o stvaranju novih objekata, pa
ovakvih dilema nema). Uzmimo, na primjer, da su vektori a i b prethodno deklarirani deklaracijom
VektorNd a(4), b(3);

i da nakon toga izvrimo dodjelu b = a. Prije izvrene dodjele, oba vektora a i b sadre
pokazivae koji pokazuju na dva razliita dinamiki alocirana niza razliite veliine. Nakon obavljenog
plitkog kopiranja, oba pokazivaa pokazivae na dinamiki niz dodijeljen prvom objektu, dok na
dinamiki niz koji je bio pridruen objektu b vie ne pokazuje niko. Dakle, u ovom sluaju dolazi i do
curenja memorije, s obzirom da taj dinamiki niz vie ne moe obrisati niko. Ova situacija prikazana je
na sljedeoj slici:
Prije kopiranja:

Poslije kopiranja:

dimenzija koordinate

dimenzija koordinate

dimenzija koordinate

dimenzija koordinate

11

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

Vano uoiti da ak ni duboko kopiranje kakvo je implementirano u konstruktoru kopije ne bi


rijeilo problem. Pri takvom kopiranju bila bi dodue izvrena alokacija novog dinamikog niza u koji bi
bio iskopiran niz koji pripada objektu a, ali dinamiki niz koji pripada objektu b ne bi bio uniten
(niti bi ga iko kasnije mogao unititi). Oito se prilikom dodjeljivanja nekog objekta nekom drugom
objektu koji je ve postojao od ranije, trebaju poduzeti drugaije akcije nego to su predviene
konstruktorom kopije. To je razlog zbog kojeg se konstruktor kopije i ne poziva u ovom sluaju. Na
ovom mjestu postaje posve jasno zbog ega je potrebno praviti otru razliku izmeu inicijalizacije i
dodjele (na ovu razliku emo mnogo lake misliti ukoliko se od samog poetka naviknemo da za
inicijalizaciju nikada ne koristimo sintaksu koja koristi znak dodjele =).
Opisani problem bi se mogao rijeiti kada bi se prilikom dodjele poput b = a prvo izvrio
destruktor nad objektom b, a zatim iskoristio konstruktor kopije za kopiranje objekta a u objekat
b. Meutim, tvorci jezika C++ namjerno nisu eljeli automatski podrati ovakvo ponaanje, jer ono
uglavnom nije i najbolji nain da se ostvari ispravna funkcionalnost. Na primjer, u sluaju da su vektori
a i b iste dimenzije, najprirodnije ponaanje prilikom dodjele b = a je prosto samo iskopirati sve
elemente dinamikog niza pridruenog objektu a u dinamiki niz dodijeljen objektu b (koji ve
postoji od ranije). Nikakve dodatne alokacije niti dealokacije memorije nisu potrebne. Stoga, jezik C++
doputa da sami definiramo kako e se interpretirati izraz oblika b = a u sluaju kada objekat b
odranije postoji. Kao to znamo, podrazumijevano ponaanje ovog izraza je isto kao prosto kopiranje
svih atributa objekta a u odgovarajue atribute objekta b, jedan po jedan (odnosno, isto kao i kod
podrazumijevanog konstruktora kopije). Meutim, treba znati da u sluaju kada je b primjerak neke
strukture ili klase, izraz oblika b = a zapravo se interpretira kao izraz b.operator =(a), bez
obzira kakvog je tipa a, odnosno kao poziv jedne specijalne funkcije lanice ije je ime operator =
nad objektom b (razmak izmeu kljune rijei operator i znaka = nije obavezan). Ukoliko
projektant klase ne definira sam takvu funkciju (ili vie njih sa razliitim tipovima parametara),
kompajler e sam generirati podrazumijevanu verziju te funkcije koja prima kao parametar referencu na
konstantni objekat istog tipa, koja obavlja podrazumijevani postupak dodjele (tj. prosto kopiranje
atributa). Tada govorimo o podrazumijevanom operatoru dodjele. Meutim, projektant klase moe
napisati vlastite verzije funkcije operator = i na taj nain uticati na postupak dodjele. To se naziva
preklapanje operatora dodjele (engl. overloading assignment operator). Mada je preklapanje operatora
dodjele specijalan sluaj opeg preklapanja operatora o kojem emo govoriti kasnije, oekivano
ponaanje operatora dodjele je veoma tijesno vezano za ponaanje konstruktora kopije, tako da je o
njegovom preklapanju prirodno govoriti odmah nakon opisa konstruktora kopije. Principijelno, funkcija
operator = moe da prima parametar bilo kojeg tipa i da vraa rezultat bilo kojeg tipa. Meutim, za
ostvarenje one funkcionalnosti koju ovdje elimo postii (meusobno dodjeljivanje objekata istog tipa),
njen formalni parametar treba biti referenca na konstantni objekat pripadne klase (dakle, isto kao i kod
konstruktora kopije), dok tip rezultata treba takoer biti referenca na objekat pripadne klase. U tom
sluaju govorimo o kopirajuem operatoru dodjele (engl. copy assignment operator). Ukoliko elimo da
optimiziramo dodjelu (izbjegavanjem kopiranja) u sluaju kada je izraz sa desne strane dodjele
r-vrijednost, moemo definirati i verziju koja kao parametar prima r-vrijednosnu referencu (ovo je
podrano samo u C++11) i tada govorimo o pomjerajuem operatoru dodjele (engl. move assignment
operator). U sluaju da pomjerajui operator dodjele nije definiran, kopirajui operator dodjele e se
koristiti bez obzira da li je izraz sa desne strane l-vrijednost ili ne.
Odluimo se da u klasi operator = podrimo vlastiti kako kopirajui tako i pomjerajui
operator dodjele (vlastiti kopirajui operator dodjele svakako moramo podrati da bi klasa ispravno
funkcionirala). Slijedi da u klasu VektorNd trebamo dodati sljedee deklaracije:
class VektorNd {
...
VektorNd &operator =(const VektorNd &v);
VektorNd &operator =(VektorNd &&v);
...
};

// Kopirajua verzija
// Pomjerajua verzija

Naravno, ove deklarirane funkcije lanice treba i implementirati. Poeemo od kopirajue verzije,
koja je sloenija. Na prvom mjestu, vano uoiti razliku izmeu neophodnog ponaanja konstruktora
kopije i kopirajueg operatora dodjele. Operator dodjele se nikad ne poziva pri inicijalizaciji objekata
12

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

(ak ni ukoliko se inicijalizacija vri pomou sintakse koja koristi znak dodjele =), nego samo kada se
dodjela vri nad objektom koji od ranije postoji. Za razliku od konstruktora kopije koji uvijek treba
izvriti dinamiku alokaciju memorije za potrebe objekta u koji se vri kopiranje (s obzirom da ona nije
prethodno alocirana), operator dodjele se primjenjuje nad objektom koji od ranije postoji, tako da je za
njegove potrebe ve alocirana memorija. Stoga operator dodjele treba da provjeri da li je alocirana
koliina memorije dovoljna da prihvati podatke koje treba kopirati. Ukoliko jeste, dovoljno je samo
izvriti kopiranje (uz prilagoavanje informacije o dimenziji). Meutim ukoliko nije, potrebno je
dealocirati memoriju koja je zauzeta i izvriti ponovnu alokaciju neophodne koliine memorije, pa tek
tada obavljati kopiranje. Uz ovakve preporuke, mogua implementacija funkcije lanice koja realizira
preklopljeni operator dodjele mogla bi izgledati recimo ovako:
VektorNd &VektorNd::operator =(const VektorNd &v) {
if(dimenzija < v.dimenzija) {
delete[] koordinate;
koordinate = new double[v.dimenzija];
}
dimenzija = v.dimenzija;
std::copy(v.koordinate, v.koordinate + v.dimenzija, koordinate);
return *this;
}

U prikazanoj implementaciji, realokacija (tj. dealokacija i ponovna alokacija) memorije izvodi se


samo u sluaju kada se vektoru manje dimenzionalnosti dodjeljuje vektor vee dimenzionalnosti. Na
primjer, ukoliko je od ranije a bio sedmodimenzionalni a b petodimenzionalni vektor, dodjela poput
b = a mora izvriti realokaciju da bi b bio spreman da prihvati sve komponente vektora a.
Meutim, u sluaju da je dimenzionalnost vektora b vea od dimenzionalnosti vektora a, dinamiki
niz vezan za vektor b ve sadri dovoljno prostora da prihvati sve komponente vektora a, tako da
realokacija nije neophodna. U svakom sluaju, ovo je mnogo efikasnije nego kad bi se realokacija vrila
uvijek. Naravno, u sluaju da je dimenzionalnost vektora a mnogo manja od dimenzionalnosti vektora
b, takoer je mudro izvriti realokaciju, da se bespotrebno ne zauzima mnogo vei blok memorije
nego to je zaista potrebno (ova ideja nije ugraena u gore prikazanu implementaciju). Upravo u tome i
jeste poenta: preklapanje operatora dodjele omoguava projektantu klase da specificira ta e se tano
deavati prilikom izvravanja dodjele i na koji nain. Recimo, mogue je dodjelu poput b = a podrati
samo ukoliko su a i b iste dimenzije, a u suprotnom recimo baciti izuzetak. Ili, mogue je realizirati
takvu dodjelu pri kojoj bi dimenzija vektora b nakon dodjele ostala ista kao prije dodjele, uz
odbacivanje suvinih elemenata u sluaju da vektor a ima veu dimenziju, ili dopunjavanje nulama u
sluaju da vektor a ima manju dimenziju od vektora b. Takva dodjela naziva se Procrustovska
dodjela (prema Procrustu, razbojniku iz grke mitologije, koji je prema legendi svoje rtve stavljao u
krevet i istezao ih ili im je sjekao udove sve dok im duina ne bude jednaka duini kreveta).
Primijetimo da preklopljeni operator dodjele kao rezultat vraa referencu na objekat nad kojim se
sama dodjela vri (to se postie dereferenciranjem pokazivaa this). Mada nema pravila koje nalae
da se tako mora raditi, to se gotovo uvijek radi. Razlog za to je to se na taj nain omoguava ulanavanje
operatora dodjele, odnosno konzistentno izvravanje izraza poput a = b = c. Zaista, takva konstrukcija
se zapravo interpretira kao
a.operator =(b.operator =(c));

Stoga, da bi izraz a = b = c zaista imao eljeno dejstvo (tj. b = c a zatim a = b), njegov podizraz
b.operator =(c) mora vratiti kao rezultat objekat b, a upravo to se postie na opisani nain.
Mnogi programeri prilikom realizacije kopirajueg operatora dodjele uvijek briu memoriju koju je
alocirao objekat kojem se vri dodjela, a nakon toga vre alokaciju potrebne koliine memorije, bez
ikakve prethodne provjere. Ovim se oponaa efekat kao da je nad objektom sa lijeve strane operatora
dodjele izvren destruktor, a zatim izvren konstruktor kopije. Meutim, na taj nain se bespotrebno vri
realokacija ak i u sluajevima kad je ona potpuno nepotrebna (recimo, ona je bez ikakve sumnje posve
neopravdana u sluaju da su izvorni i odredini objekat rezervirali potpuno istu koliinu memorije). Ipak,
radi ilustracije nekih specifinosti, pokaimo i kako bi izgledala takva implementacija (koja je neznatno
jednostavnija od prethodne implementacije):
13

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

VektorNd &VektorNd::operator =(const VektorNd &v) {


if(&v != this) {
delete[] koordinate;
dimenzija = v.dimenzija; koordinate = new double[dimenzija];
std::copy(v.koordinate, v.koordinate + v.dimenzija, koordinate);
}
return *this;
}

Ovdje treba posebnu panju obratiti na test koji glasi if(&v != this). Ovim testom se ispituje
da li su izvorni i odredini objekat (vektor) identini, i ukoliko jesu, ne radi se nita. Svrha ove naredbe
je da se izbjegne opasnost od tzv. destruktivne samododjele (engl. destructive self-assignment). Pod
samododjelom se podrazumijeva naredba koja je logiki ekvivalentna naredbi a = a. Naime, u sluaju
da doe do samododjele, u sluaju da nismo preduzeli posebne mjere opreza, objekat sa lijeve strane bio
bi uniten, ali bi samim tim bio uniten i objekat sa desne strane, poto se radi o istom objektu! Razumije
se da niko nee eksplicitno pisati naredbe poput a = a, ali se skrivene situacije koje su logiki
ekvivalentne ovakvoj naredbi mogu pojaviti. Na primjer, ukoliko su x i y reference koje su vezane
na isti objekat a, tada je dodjela x = y sutinski ekvivalentna dodjeli a = a, a situacije da su dvije
reference vezane na isti objekat (aliasing) uope nije rijetka, naroito u programima gdje se mnogo
koristi prenos parametara po referenci. Zbog toga, funkcija koja realizira preklopljeni operator dodjele
nikada ne smije brisati odredini objekat bez prethodne garancije da on nije ekvivalentan izvornom
objektu. Primijetimo da je u ranijoj implementaciji ova garancija implicitno ostvarena. Naime, mi smo
vrili destrukciju (i ponovnu konstrukciju) odredinog objekta samo ukoliko ustanovimo da je on manjeg
kapaciteta od kapaciteta izvornog objekta, to u sluaju samododjele sigurno nee biti sluaj.
Preostaje jo da implementiramo pomjerajui operator dodjele. On je znatno jednostavniji, s
obzirom da je dovoljno obrisati resurse objekta nad kojim se vri dodjela, a zatim ukrasti resurse
objektu koji se dodjeljuje, uz postavljanje pokazivaa na njegove resurse na nul-pokaziva (ime se
spreava njegov destruktor da oslobodi resurse koji su sada premjeteni u odredini objekat). To bi
konkretno moglo izgledati recimo ovako:
VektorNd &VektorNd::operator =(VektorNd &&v) {
delete[] koordinate;
dimenzija = v.dimenzija; koordinate = v.koordinate;
v.koordinate = nullptr;
return *this;
}

Pomjerajui operator dodjele omoguava nam da konstrukcije poput c = ZbirVektora(a, b)


postanu mnogo efikasnije, jer je objekat sa desne strane privremeni objekat koji e svakako biti uniten
im se iskoristi, pa je mnogo pametnije prosto ukrasti njegove resurse. Takoer, definiranjem
pomjerajueg operatora dodjele automatski je podrana i move-semantika prilikom dodjeljivanja. Zaista,
napiemo li neto poput b = std::move(a), dodjela nad objektom b izvrie se posredstvom
pomjerajueg a ne kopirajueg operatora dodjele. Zaista, poziv funkcije move doveo je do toga da se
izraz sa desne strane dodjele ne tretira vie kao l-vrijednost nego kao r-vrijednost.
Mogunost da se prilikom kopiranja objekata razdvoji tretman privremenih bezimenih i trajnih
imenovanih objekata i na taj nain drastino pobolja efikasnost i podri move-semantika, smatra se
jednom od najrevolucionarnijih inovacija uvedenih u C++11. Zbog toga se na zakon velike trojke u
C++11 dodaje amandman po kojem bi klasa koja ima definirane destruktore trebala imati ne samo
vlastiti kopirajui konstruktor i vlastiti (kopirajui) operator dodjele, nego vrlo vjerovatno i pomjerajui
konstruktor, te pomjerajui operator dodjele. Tako dopunjeni zakon velike trojke neki ve nazivaju
zakon velike petorke (engl. The Law of Big Five).
Ve smo rekli da se u sluaju da ne definiramo vlastiti kopirajui konstruktor, automatski generira
podrazumijevani kopirajui konstruktor, koji prosto kopira sve atribute jednog objekta u drugi. Pri tome,
ukoliko je neki atribut tipa klase koja posjeduje vlastiti kopirajui konstruktor, njen kopirajui
konstruktor e biti iskoriten za kopiranje odgovarajueg atributa. Interesantno je da se u tom sluaju
takoer generira i podrazumijevani pomjerajui konstruktor (u C++11), koji e za pomjeranje atributa
14

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_a
Akademska godina 2013/14

(pri kopiranju privremenih objekata ili prilikom upotrebe move-semantike) iskoristiti njihove vlastite
pomjerajue konstruktore (ukoliko postoje). Slino vrijedi i za operator dodjele, odnosno ukoliko nije
definiran vlastiti kopirajui operator dodjele, automatski se generiraju podrazumijevani kako kopirajui,
tako i pomjerajui operator dodjele. Slijedi da nije potrebno definirati vlastite kopirajue i pomjerajue
konstruktore niti operatore dodjele za bezbjedno kopiranje, pomjeranje i meusobno dodjeljivanje
objekata koji sadre npr. atribute tipa vector ili string (pa ni atribute tipa VektorNd i
Matrica, koje smo sami razvili), jer e se za ispravno obavljanje tih operacija pobrinuti kopirajui i
pomjerajui konstruktori odnosno operatori dodjele odgovarajuih atributa (naravno, vlastiti kopirajui
konstruktori i pratei rekviziti e sigurno biti potrebni u sluaju da pored takvih atributa, klasa posjeduje
i dodatne pokazivae koji pokazuju na dinamiki alocirane resurse). Ova injenica prua mogunost da
se u velikom broju praktinih sluajeva u potpunosti izbjegne potreba za definiranjem konstruktora
kopije i prateih rekvizita. Naime, umjesto koritenja dinamike alokacije memorije, moemo koristiti
tipove poput vector i string koji pruaju sve pogodnosti koje prua i dinamika alokacija
memorije (to nije nikakvo iznenaenje, s obzirom da je njihova implementacija zasnovana upravo na
dinamikoj alokaciji memorije). S obzirom da se ovi tipovi kopiraju i pomjeraju bez problema
(zahvaljujui njihovim vlastitim konstruktorima kopije i srodnim elementima), korisnik se ne mora
brinuti o ispravnom kopiranju i pomjreranju. Na primjer, pogledajmo kako bismo mogli realizirati klasu
VektorNd koristei tip vector umjesto dinamike alokacije memorije:
class VektorNd {
std::vector<double> koordinate;
double dimenzija;
void TestIndeksa(int indeks) { /* Isto kao i ranije */ }
public:
explicit VektorNd(int dimenzija) : dimenzija(dimenzija),
koordinate(dimenzija) {}
VektorNd(std::initializer_list<double> lista) :
dimenzija(lista.size()), koordinate(lista) {}
void PromijeniDimenziju(int nova_dimenzija) {
koordinate.resize(nova_dimenzija); dimenzija = nova_dimenzija;
}
... // Ostatak klase ostaje isti
};

Vlastito definirani destruktor, kopirajui i pomjerajui konstruktor, te operatori dodjele vie nisu
potrebni. Obratimo panju kako je konstruktor klase VektorNd iskoriten za inicijalizaciju broja
elemenata atributa koordinate koji je tipa vector<double>, te kako sada izgleda odgovarajui
sekvencijski konstruktor. Metoda PromijeniDimenziju je morala takoer biti promjenjena, dok
implementacije privatne metode TestIndeksa, metoda PostaviKoordinatu, DajKoordinatu
i Ispisi, te prijateljske funkcije ZbirVektora mogu ostati iste kao i do sada. U sutini, sa
ovakvim izmjenama, ak i atribut dimenzija postaje suvian, jer je njegova vrijednost uvijek jednaka
veliini atributa koordinate, koja se moe dobiti pozivom funkcije size. Naravno, uklonimo li
atribut dimenzija, moramo nainiti i odgovarajue izmjene u svim funkcijama koje su ga koristile
(tako to emo umjesto njega koristiti konstrukciju koordinate.size()). Da smo imali i pristupnu
metodu DajDimenziju koju smo koristili u ostalim funkcijama umjesto direktnog pristupa atributu
dimenzija, dovoljno bi bilo promijeniti samo tu metodu, dok bi sve ostalo moglo ostati isto.
Ukoliko se sada pitate zbog ega smo se uope patili sa dinamikom alokacijom memorije,
destruktorima, kopirajuim i pomjerajuim konstruktorima te preklapanjem operatora dodjele kada
problem moemo jednostavnije rijeiti prostom upotrebom tipova poput vector ili string,
odgovor je jednostavan: u suprotnom ne bismo mogli shvatiti kako ovi tipovi podataka zapravo rade, i ne
bismo bili u stanju kreirati vlastite tipove podataka koji se ponaaju poput njih. Pored toga, koritenje
tipa vector samo sa ciljem izbjegavanja dinamike alokacije memorije i prateih rekvizita
(konstruktora kopije, itd.) jeste najlaki, ali ne i najefikasniji nain za rjeavanje problema. Naime,
deklariranjem nekog atributa tipa vector, u klasu koju razvijamo ugraujemo sva svojstva klase
vector, ukljuujui i svojstva koja vjerovatno neemo uope koristiti (isto vrijedi i za upotrebu
atributa tipa string). Na taj nain, klasa koju razvijamo postaje optereena suvinim detaljima, to
dovodi do gubitka efikasnosti i bespotrebnog troenja raunarskih resursa.

15

You might also like