You are on page 1of 16

Dr.

Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

Predavanje 11_a
Kao što smo već vidjeli, jedna od osnovnih uloga konstruktora je ostvarenje propisne inicijalizacije
objekata. Međutim, kod nekih tipova 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 sadržani u njegovim atributima, recimo dodatna količina memorije (koja se
može ostvariti putem dinamičke 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 izvršiti unutar konstruktora. Ti dodatni resursi ne moraju uvijek biti
isključivo memorijski resursi, nego se može raditi o datotekama, vezama sa udaljenim računarima
putem računarske mreže, pristupnicima bazama podataka, itd. U svakom slučaju, zauzimanje resursa
neophodnih za funkcioniranje nekog objekta najbolje je izvršiti odmah prilikom njegove inicijalizacije
(tj. u konstruktoru), jer će na taj način 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
praktično neupotrebljivi dok se ne izvrši poziv neke kreirajuće funkcije, kakva je recimo bila funkcija
“StvoriMatricu”). 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 vršilo prilikom inicijalizacije tog objekta (tj. u njegovom konstruktoru), dok bi
se oslobađanje tog resursa trebalo vršiti uporedo sa prestankom postojanja tog objekta (vidjećemo
uskoro kako se to postiže). Ta filozofija, karakteristična upravo za jezik C++, poznata je pod akronimom
RAII (Resource Acquisition Is Initialization), u velikoj mjeri je doprinijela popularizaciji jezika C++, i
smatra se da je jedna od njegovih najvećih prednosti u odnosu na konkurentske jezike. Prema ovoj
filozofiji, zauzimanje resursa neophodnih za rad objekta je zapravo sastavni dio njegove inicijalizacije.

Pošto je za sada dodatna memorija (ostvarena kroz dinamičku alokaciju) jedini dodatni resurs koji
znamo koristiti, ilustriraćemo ovaj pristup upravo kroz zauzimanje dodatne memorije putem dinamičke
alokacije. Neka na primjer želimo napraviti klasu nazvanu “VektorNd” koja će predstavljati vektor u
𝑛-dimenzionalnom prostoru (tj. vektor koji se opisuje sa 𝑛 koordinata), pri čemu dimenzija 𝑛 nije fiksna,
nego ju je moguće zadavati prilikom kreiranja objekata tipa “VektorNd” (slično kao kod standardnog
bibliotečkog tipa “vector”). Ako za realizaciju ove klase želimo izbjeći razne bibliotečke tipove podataka,
jedino mjesto gdje možemo čuvati koordinate vektora je u nekom dinamički alociranom nizu realnih
brojeva, kojem ćemo pristupati preko odgovarajućeg pokazivača (s obzirom da atributi nizovnog tipa
moraju imati fiksnu veličinu). Prikazaćemo kostur ove klase koji ćemo kasnije dopunjavati novim
funkcionalnostima (objašnjenje uvedenih elemenata slijedi odmah ispod prikaza klase):
class VektorNd {
int dimenzija;
double *koordinate;
void TestIndeksa(int indeks) const {
if(indeks < 1 || indeks > dimenzija) throw std::range_error("Pogrešan 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”, nešto su složenije, stoga


su izvedene izvan definicije same klase:
VektorNd::VektorNd(int dimenzija) : dimenzija(dimenzija),
koordinate(new double[dimenzija]) {
std::fill(koordinate, koordinate + dimenzija, 0);
}

1
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

void VektorNd::PromijeniDimenziju(int nova_dimenzija) {


if(nova_dimenzija > dimenzija) {
double *novo_mjesto = new double[nova_dimenzija];
std::copy(koordinate, koordinate + dimenzija, novo_mjesto);
std::fill(novo_mjesto + dimenzija, 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 dinamički 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 objasnićemo nešto kasnije). Taj
konstruktor odmah pri kreiranju objekta vrši neophodnu dinamičku alokaciju memorije (primijetimo
da se ta alokacija vrši unutar konstruktorske inicijaliziacijske liste, što je sasvim legalno) i inicijalizira
sve koordinate vektora na nulu pozivom funkcije “fill”, što je efikasnije od upotrebe recimo for-petlje
(od C++11 nadalje smo alternativno mogli odmah kreirati niz inicijaliziran nulama pomoću konstrukcije
“new double[dimenzija]{}”). Time garantiramo da će objekti tipa “VektorNd” uvijek biti u ispravnom
stanju (ispravno dimenzionirani, sa ispravno zauzetim resursima i sa tačno definiranim početnim
sadržajem) odmah po njihovom stvaranju. Isto tako, zbog nepostojanja konstruktora bez parametara,
neće biti moguće zadavati objekte tipa “VektorNd” bez specificiranja dimenzije, što svakako ima smisla.
Metoda “PromijeniDimenziju” namijenjena je da omogući promjenu dimenzije već postojećeg
vektora (uloga joj je analogna ulozi funkcije “resize” kod bibliotečkog tipa “vector”). Mada je upitno da li
za 𝑛-dimenzionalne vektore kao matematičke pojmove naknadna promjena dimenzije uopće ima ikakvog
opravdanja, ovdje je ta metoda definirana čisto da se stekne uvid u to kako rade funkcije poput “ resize”.
U slučaju da je nova dimenzija veća od postojeće, dinamički se alocira novi prostor na novom mjestu u
memoriji uz kopiranje svih elemenata iz starog u novi prostor (ovaj postupak se naziva realokacija).
Također se vrši i popunjavanje nulama onih elemenata koji ranije nisu postojali. Na kraju se stari
prostor oslobađa i pokazivač “koordinate” preusmjerava na novo mjesto. Naravno, pri tome se ažurira i
vrijednost atributa “dimenzija”. Međutim, u slučaju da je nova dimenzija manja od stare, realokacija je
izbjegnuta tako što se samo smanji informacija o dimenziji (koja se čuva u atributu “dimenzija”), tako
da će korisnici objekta misliti da se vektoru dimenzija smanjila (bez obzira što je količina zauzete
memorije ostala ista). Da li se ova varka isplati? U slučaju da se nova i stara dimenzija malo razlikuju
(npr. stara je bila 15 a nova 12), velika je ušteda u efikasnosti izbjeći realokaciju, pri čemu je cijena koju
smo platili to što je i dalje fizički alociran prostor u koji može stati 15 elementata. S druge strane, u
slučaju velike razlike u dimenzijama (npr. smanjenje dimenzije sa 1000 na 10), bolje je izvršiti
realokaciju, da ne bismo bespotrebno čuvali u memoriji i dalje prostor za 990 elemenata koji se više ne
koriste. U našem slučaju, radi jednostavnosti se nismo odlučili da podržimo ovu strategiju, koju možete
sami uraditi kao korisnu vježbu.
Metode “PostaviKoordinatu” i “DajKoordinatu” dovoljno su proste da uglavnom ne zahtijevaju
posebna objašnjenja. Ipak, treba obratiti pažnju na nekoliko detalja. Prvo, ove metode pozivaju
pomoćnu privatnu metodu “TestIndeksa” koja testira da li se indeks (redni broj koordinate) nalazi u
legalnom opsegu od 1 do 𝑛 i baca izuzetak u slučaju da test nije zadovoljen. Pri tome je postignuto da se
indeksi numeriraju od jedinice, kao što je uobičajeno u matematici. Drugo, primijetimo da je metoda
“DajKoordinatu” izvedena u dvije varijante sa istim tijelom i istim parametrima, samo što je jedna
označena kao inspektor (tj. sa “const”) a druga nije, pri čemu ova druga vraća referencu kao rezultat.
Razlog za ovo dupliranje biće objašnjeni uskoro. Konačno, imamo i metodu “Ispisi” koja u vitičastim
zagradama ispisuje koordinate vektora međusobno 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 slučajno, s obzirom da dimenzioniranje objekata tipa “vector” upravo obavlja
konstruktor klase “vector”:

2
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

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 oslobađanjem zauzete memorije. Taj problem ćemo
riješiti uskoro, a zasad ga još nismo nikako riješili. Neko bi mogao pomisliti da je dovoljno napisati neku
metodu, nazvanu recimo “OslobodiMemoriju”, koju bi mogli pozvati nad objektima tipa “VektorNd” kad
nam više ne trebaju, a u svakom slučaju prije nego što objekti prestanu postojati (s obzirom da nakon
što prestanu postojati više nemamo šanse da oslobodimo memoriju). Implementacija ove metode
mogla bi se sastojati prosto od naredbe “delete[] koordinate”. Međutim, uskoro ćemo vidjeti zbog čega
takvo rješenje nije dobro, kao i da postoji mnogo jednostavnije i bolje rješenje.
Razmotrimo sada šta smo htjeli postići sa dvije verzije metode “DajKoordinatu”. U slučaju kada
postoje dvije metode istog imena i istih parametara od kojih je jedna označena kao konstantna (tj. sa
oznakom “const”) a druga nije, tada se konstantna verzija poziva samo ukoliko se primijeni nad
konstantnim objektom. U svim ostalim slučajevima, poziva se nekonstantna verzija metode. Dakle, u
ovom primjeru, za slučaj nekonstantnih objekata pozivaće se verzija koja vraća referencu na odgovarajući
element niza. Stoga je, u slučaju kada se metoda “DajKoordinatu” pozove nad nekonstantnim objektom,
njen rezultat l-vrijednost. To nam omogućava konstrukcije poput “v1.DajKoordinatu(1)++”, koje ne bi
bile inače moguće. Također, omogućeno je da umjesto pozivom metode “PostaviKoordinatu”, promjenu
koordinata možemo vršiti i konstrukcijom poput sljedeće, što nam 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 “VektorNd” ne bismo mogli
čak ni čitati koordinate. S druge strane, da smo definirali samo konstantnu metodu “DajKoordinatu”
koja vraća vrijednost odgovarajućeg elementa niza (a ne referencu na njega), pomoću te funkcije bismo
mogli samo čitati ali ne i mijenjati koordinate koristeći gore prikazane konstrukcije (doduše, još uvijek
bi nam ostala mogućnost promjene koordinata putem metode “PostaviKoordinatu”). Konačno, da smo
definirali samo konstantnu metodu “DajKoordinatu” koja vraća referencu na odgovarajući element niza,
mogli bismo promijeniti elemente vektora čak i ukoliko je objekat tipa “VektorNd” prethodno deklariran
kao konstantan! Na prvi pogled ovo izgleda nemoguće, s obzirom da smo pretpostavili da je metoda
“DajKoordinatu” deklarirana kao konstantna, što joj zabranjuje da mijenja atribute objekta. Međutim, ta
metoda zaista ne mijenja niti jedan od atributa klase “VektorNd”! Ona samo vraća referencu na element
dinamički kreiranog niza (koji nije atribut klase), a sama izmjena obavlja se potpuno izvan same metode.
Ovako, kada imamo dvije verzije metode “DajKoordinatu”, u slučaju poziva nad konstantnim objektom
poziva se konstantna verzija metode koja vraća vrijednost odgovarajućeg elementa niza “koordinate”, što
onemogućava da se poziv ove metode u tom slučaju nađe sa lijeve strane operatora dodjele, dok je to
moguće u slučaju poziva ove metode nad nekonstantnim objektom (s obzirom da se vraća referenca
kao rezultat). Slijedi da je, ukoliko želimo da se poziv metode “DajKoordinatu” može koristiti i kao
l-vrijednost, osim u slučaju kada je objekat konstantan, opisano rješenje sa dvije verzije ove metode je
zaista jedino koje omogućava konzistentno ponašanje u svim situacijama! Istaknimo ipak da je rješenje
koje koristi pristupne metode “PostaviKoordinatu” i “DajKoordinatu” samo privremeno rješenje dok ne
naučimo kako da klasu “VektorNd” proširimo tako direktno podržava indeksiranje, tj. da možemo
direktno pisati konstrukcije poput “std::cout << v1[1]” i “v1[1] = 3” za pristup elementima pridruženog
dinamičkog niza (uskoro ćemo vidjeti da je i ovo moguće).
Razmotrimo sada šta je loše u oslobađanju 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 viseći pokazivač), tako da bi svaki pristup tom objektu bio ilegalan. Problem bi se doduše mogao
riješiti ukoliko bismo pokazivač “koordinate” nakon brisanja postavili na nul-pokazivač i zabranili bilo
kakvu upotrebu objekta (npr. bacanjem izuzetka) sve dok je taj pokazivač nul-pokazivač. Objekat bi

3
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

eventualno mogao ponovo postati ispravan nakon poziva neke metode koja bi ponovo alocirala prostor
za smještanje koordinata. Međutim, takvo rješenje 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
učiniti da bismo mogli uhvatiti izuzetak koji bi eventualno mogao biti bačen iz konstruktora),
uništavanje ovih objekata bi se također moralo ubaciti unutar try-bloka, kao u sljedećem isječku:
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 bačeni izuzetak u try-bloku nakon što su objekti “v1” i “v2” već
stvoreni napravio problem (npr. ukoliko bismo slučajno pozvali metode “PostaviKoordinatu” ili
“DajKoordinatu” sa neispravnim indeksom), jer bismo tada izletili izvan try-bloka i preskočili poziv
metoda za oslobađanje memorije. Stoga bismo morali hvatati sve takve izuzetke lokalnim try- blokovima
smještenim unutar ovog try-bloka, da ih uspijemo obraditi prije nego što naiđemo na poziv metoda za
oslobađanje memorije (što je prilično nepraktično). Vjerovatno najveći problem može nastati u slučaju
da konstrukcija objekta “v1” uspije, a konstrukcija objekta “v2” ne uspije. Izuzetak koji će pri tome biti
bačen biće uhvaćen u catch-bloku, ali nakon toga više nemamo mogućnost da obrišemo objekat “v1”
(koji više nije u vidokrugu)! Jedino moguće rješenje ovog problema (koristeći samo one jezičke elemente
koje smo dosad upoznali) bilo bi korištenje ugniježdenih try − catch blokova. Međutim, takvo rješenje je
zaista veoma ružno i postaje neprihvatljivo glomazno ukoliko imamo veći broj objekata a ne samo dva
(jer bi za svaki objekat trebao poseban try − catch blok).
Da bi se riješili gore opisani problemi, u jezik C++ su pored konstruktora uvedeni i destruktori, koji
rješavaju sve ove probleme “jednim udarcem”. Za razliku od konstruktora, koji definiraju skupinu akcija
koje se automatski izvršavaju prilikom stvaranja nekog objekta, destruktori predstavljaju skupinu akcija
koje se automatski izvršavaju prilikom uništavanja objekta, tačnije kada objekat prestaje postojati (tipično
na kraju bloka unutar kojeg je objekat deklariran, ili po izlasku iz funkcije u kojoj je objekat deklariran
pomoću naredbi “return” ili “throw”). Zadatak destruktora je najčešće da oslobodi dodatne resurse koje je
objekat zauzeo tokom svog života, recimo prilikom poziva konstruktora ili neke metode koja vrši
zauzimanje dodatnih resursa (ti resursi su obično dinamički alocirana memorija, ali mogu biti i otvorene
datoteke na disku i razni drugi dopunski resursi koje smo spominjali). Kako se destruktori automatski
pozivaju, ne može se dogoditi da ih zaboravimo pozvati, odnosno svako uništavanje objekta koji
posjeduje destruktore biće praćeno pozivom destruktora. Slično 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 možemo
u klasu “VektorNd” dodati destruktor, koji će preuzeti ulogu metode “OslobodiMemoriju” (naravno,
destruktor treba staviti u javni dio klase, inače se neće moći koristiti van funkcija članica klase):
class VektorNd {

~VektorNd() { delete[] koordinate; } // umjesto "OslobodiMemoriju"

};

Sada upotreba klase “VektorNd” postaje mnogo jednostavnija, jer se više ne moramo eksplicitno
brinuti o oslobađanju dinamički 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 može
desiti da zaboravimo osloboditi memoriju. To će se desiti ne samo u slučaju prirodnog završetka bloka,
nego i u slučaju da dođe do napuštanja bloka usljed bacanja nekog izuzetka, tako da nas ni nepredviđeni
izuzeci ne trebaju ni najmanje brinuti. Također, lijepa je stvar što se destruktori pozivaju samo nad
objektima koji su zaista stvoreni (pod stvorenim objektom smataramo objekat čiji se konstruktor završio
regularno, a ne bacanjem izuzetka). Tako, ukoliko na primjer svaranje objekta “v1” uspije, a objekta “v2”
ne uspije, doći će do bacanja izuzetka i napuštanja try-bloka. Tada će automatski biti pozvan destruktor
nad objektom “v1”, koji će ga uništiti. Destruktor nad objektom “v2” koji nije stvoren neće se ni pozvati,
a to nam upravo i treba.

4
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

U već pomenutoj RAII filozofiji, podrazumijeva se da se resursi koji se zauzmu prilikom inicijalizacije
objekta (tj. unutar konstruktora) obavezno oslobađaju prilikom uništavanja objekta (tj. unutar
pripadnog destruktora). Zbog toga se ovaj pristup često naziva i akronimom CADRe (Constructor
Acquires, Desctructor Releases). Na taj način se vrijeme u toku kojeg je neki resurs zauzet izjednačuje
sa vremenom u toku kojeg živi objekat kojem je povjerena briga o tom resursu. Drugim riječima, svako
uništavanje objekta kojem je povjerena briga o nekom resursu, bez obzira na koji način je izazvano,
automatski dovodi i do oslobađanja odgovarajućeg resursa. Na taj način curenje resursa (npr. curenje
memorije, ali ne samo memorije nego i drugih resursa) postaje praktično nemoguće.
Već je rečeno da se destruktori uopće ne pozivaju ukoliko objekat nije stvoren, tj. ukoliko se
konstruktor nije do kraja izvršio. To ima za posljedicu da konstruktor nikada ne smije ostaviti iza sebe
polovično stvoren objekat, jer tada niko neće obrisati niti će biti u stanju da obriše ono što je iza sebe
ostavio konstruktor. Drugim riječima, konstruktor prije nego što baci izuzetak (ukoliko utvrdi da ga
mora baciti) mora iza sebe počistiti svo “smeće” koje je iza sebe ostavio. To se tipično dešava ukoliko se
u konstruktoru dinamički alocira više stvari: ukoliko se prvo izvrši nekoliko uspješnih alokacija, a zatim
jedna neuspješna, konstruktor prije nego što baci izuzetak treba da pobriše sve uspješne alokacije (jer
ih u protivnom niko neće obrisati). To se može uraditi tako što se unutar konstruktora ugradi try-blok
koji će uhvatiti eventualno neuspješnu alokaciju, nakon čega se u catch-bloku može “počistiti zaostalo
smeće” prije nego što se zaista baci izuzetak iz konstruktora.
Bitno je napomenuti da bacanje izuzetaka iz destruktora treba izbjegavati po svaku cijenu (srećom,
ovo je rijetko potrebno). Naime, bacanje izuzetaka iz destruktora može u nekim situacijama izazvati
veoma čudne efekte (koji obično završavaju krahom programa), zbog čega je najbolje bacanje izuzetaka
iz destruktora potpuno izbjeći. Također, destruktore nikada ne treba eksplicitno pozivati kao obične
funkcije članice, mada sintaksa to dozvoljava (npr. konstrukcija poput “v1.~VektorNd()” je sintaksno
legalna). Ukoliko to učinite, samo tražite sebi probleme. Destruktori se ekplicitno pozivaju jedino u
nekim vrlo specifičnim situacijama koje spadaju u neke vrlo napredne programerske tehnike, a koje
znatno prevazilaze opseg ovog kursa. Stoga destruktore treba pustiti da se automatski pozivaju tamo
gdje je to potrebno, a inače ih treba ostaviti na miru (osim ako o njima ne znate znatno više nego što je
ovdje prezentirano). Na prvom mjestu, ukoliko bismo pozvali destruktor eksplicitno, mogli bismo imati
probleme sa brisanjem već obrisanih resursa u slučaju kad se kasnije isti destruktor pozove automatski
(osim ako preduzmemo posebne mjere protiv višestrukog brisanja, koje inače ne bi bile potrebne).
Drugi problem je što destruktori, slično konstruktorima, nisu funkcije članice, i oni uvijek izvršavaju i
neke podrazumijevane akcije koje uopće nisu specificirane u tijelu destruktora (recimo, destruktori
neke klase automatski pozivaju destruktore nad svim njenim atributima čiji tipovi također posjeduju
destruktore). Stoga, ukoliko bismo eksplicitno pozvali destruktor, izvršilo bi se ne samo ono što piše 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 izvršite posve iste naredbe koje su sadržane u tijelu
destruktora, nemojte eskplicitno pozivati destruktor kao funkciju članicu, nego definirajte pomoćnu
funkciju (tipično u privatnoj sekciji klase) unutar koje ćete smjestiti te naredbe, a zatim tu pomoćnu
funkciju pozovite iz destruktora, i sa bilo kojeg drugog mjesta gdje su Vam te naredbe potrebne.
Destruktori se također automatski pozivaju i pri upotrebi operatora “delete” i “delete[]” u slučaju da
je pomoću operatora “new” stvoren objekat koji sadrži konstruktore i destruktore. Ovo nije nimalo
iznenađujuće, jer tom prilikom također dolazi do uništavanja pripadnih objekata (samo što oni nemaju
svoja imena). Stoga “delete”, slično kao i “new”, također pokreće dvoetapni proces: prva etapa je poziv
odgovarajućeg destruktora (ukoliko isti postoji), a tek tada slijedi oslobađanje zauzete memorije. Na
primjer, ukoliko smo dinamički stvorili neki objekat tipa “VektorNd” naredbom poput
VektorNd *pok_na_vektor = new VektorNd(5);

tada će njegovo brisanje pomoću naredbe


delete pok_na_vektor;

izazvati dva efekta: prvo će nad dinamički stvorenim objektom “*pok_na_vektor” biti pozvan destruktor
(koji će osloboditi memoriju koja je zauzeta u konstruktoru objekta), pa će tek tada biti oslobođena
memorija koji je sam objekat “*pok_na_vektor” zauzimao (tj. prostor za njegove atribute “dimenzija” i
“koordinate”). Slično, prilikom upotrebe operatora “delete[]” za brisanje nekog dinamički stvorenog
niza, prije samog brisanja nad svakim elementom niza biće pozvan njegov destruktor (ukoliko takav
postoji). Ovim je pokazana jedna (od mnogih) razlika između operatora “delete” i “delete[]” (u slučaju
da smo umjesto “delete[]” stavili “delete”, destruktor bi bio izvršen samo jednom, a ne nad svakim
elementom niza), koja ukazuje da ova dva operatora ne treba miješati i da svaki treba koristiti isključivo
za ono za šta je namijenjen!

5
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

Ostali smo dužni da objasnimo zbog čega smo konstruktor klase “VektorNd” definirali kao eksplicitni
konstruktor. Da ovo nismo uradili, sasvim bi moguće bilo napisati nešto 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 neočekivan 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 nešto kasnije). Slična neočekivana situacija nastala bi ukoliko bi nekoj funkciji
koja očekuje objekat tipa “VektorNd” kao parametar bio proslijeđen broj (taj broj bi bio proslijeđen
konstruktoru sa jednim parametrom, nakon čega bi konstruisani objekat bio proslijeđen funkciji, a to
sigurno nije željeno ponašanje). Ovako, označavanjem konstruktora sa “explicit”, zabranjuje se njegovo
korištenje za automatsku pretvorbu tipova iz cjelobrojnog u tip “VektorNd”, tako da ovakve besmislene
konstrukcije neće biti ni dozvoljene (tj. dovešće do prijave greške od strane kompajlera). U suštini,
treba se pridržavati jednostavnog pravila: kad god parametar konstruktora sa jednim parametrom ne
služi za inicijalizaciju vrijednosti objekta nego za definiranje interne strukture (građe) objekta (kao u
ovom primjeru), takav konstruktor bi trebao biti eksplicitni konstruktor.
Za sada, za objekte tipa “VektorNd” nema nikakve razlike u inicijalizaciji pomoću okruglih i vitičastih
zagrada (podržane od C++11 nadalje), odnosno konstrukcije poput “VektorNd v(5)” i “VektorNd v{5}” za
sada imaju isto dejstvo. Međutim, lijepo bi bilo da objekte tipa “VektorNd” možemo inicijalizirati
navođenjem elemenata u vitičastim zagradama, odnosno da možemo pisati recimo
VektorNd v{3, 5, 2, 6, 1}; // Ovo zasad ne radi... 
Također, lijepo bi bilo da kroz koordinate objekata tipa “VektorNd” možemo prolaziti rasponskom
for-petljom. Srećom, sve ovo je posve lako postići (od C++11 nadalje). Da bismo omogućili inicijalizaciju
navođenjem elemenata, potrebno je definirati još jedan konstruktor nazvan sekvencijski konstruktor ili
konstruktor iz liste inicijalizatora (engl. sequence constructor odnosno initializer-list constructor). Takav
konstruktor prima objekat tipa “initializer_list” kao parametar. Kada se za inicijalizaciju koristi lista
inicijalizatora, ukoliko postoji odgovarajući sekvencijski konstruktor koji odgovara tipu elemenata u listi
inicijalizatora, biće pozvan on umjesto običnih konstruktora, što ćemo detaljnije objasniti u nastavku. S
druge strane, da bismo podržali prolazak rasponskom 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 ponaša makar kao iterator sa kretanjem unaprijed i koji može
prolaziti kroz elemente kolekcije. Naravno, “begin” treba vratiti takav objekat koji je na neki način
vezan za prvi element kolekcije, a “end” na element iza kraja kolekcije. U našem primjeru, dovoljno je
vratiti obične pokazivače, jer je odgovarajuća kolekcija koja čuva elemente vrlo jednostavna (obični 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. Inače, implementacijom ovih metoda omogućili smo ne samo rasponske for-petlje sa objektima
tipa “VektorNd”, nego i korištenje takvih objekata sa funkcijama iz biblioteke “algorithm”. Razlog zbog
kojeg ovo radi je što kompajler sve rasponske for-petlje oblika
for(Tip promjenljiva : kolekcija) tijelo_petlje;

tretira identično kao da smo napisali nešto poput


for(auto it = kolekcija.begin(); it != kolekcija.end(); it++) {
Tip promjenljiva = *it; tijelo_petlje;
}

6
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

Što se tiče sekvencijskog konstruktora kojeg smo ovdje deklarirali, on će biti pozvan kad god se za
inicijalizaciju objekta upotrijebi lista inicijalizatora čiji su svi elementi tipa “double”, ili se mogu
automatski konvertirati u tip “double”. On će također biti pozvan i za potrebe automatske konverzije
takvih listi inicijalizatora u odgovarajućih objekte, osim ukoliko je proglašen za eksplicitni konstruktor. U
slučaju da elementi liste inicijalizatora nisu onakvog tipa kakvog očekuju sekvencijski konstruktori, oni
se ignoriraju i razmatraju se ostali konstruktori (pri čemu se njima elementi liste inicijalizatora šalju
“raspakovano”, jedan po jedan u posebne parametre). Što se tiče samog tipa “initializer_list”, to je
jedan vrlo jednostavan tip (za njegovo korištenje treba uključiti istoimenu biblioteku) koji služi za
pristup elementima listi inicijalizatora i sadrži jedino odgovarajući iterator sa direktnim pristupom za
kretanje kroz listu inicijalizatora (elementi listi inicijalizatora se mogu samo čitati, a ne i mijenjati), koji je
zapravo implementiran kao obični pokazivač, te funkcije članice “begin”, “end” i “size” (i ništa drugo).
Ovo će biti jasno iz primjera moguće implementacije sekvencijskog konstruktora za ovu klasu:
VektorNd::VektorNd(std::initializer_list<double> lista) :
dimenzija(lista.size()), koordinate(new double[lista.size()]) {
std::copy(lista.begin(), lista.end(), koordinate);
}

Inače, tip “initializer_list” može se koristiti neovisno od sekvencijskih konstruktora kad god
nam je potreban brz i efikasan pristup elementima liste inicijalizatora. Recimo, neka želimo napraviti
funkciju “NekaFunkcija” koja može primiti listu inicijalizatora kao parametar, tako da možemo pisati
nešto poput “NekaFunkcija({3, 5, 2, 4, 7})”. Ranije smo vidjeli da to možemo postići tako što ćemo
staviti da parametar funkcije “NekaFunkcija” bude recimo tipa “vector”. Međutim, na taj način efektivno
dolazi do konverzije liste inicijalizatora u vektor, što dovodi do nepotrebnih komplikacija i gubitaka na
efikasnosti. Ako će ova funkcija primati samo liste inicijalizatora kao parametre (a ne i prave objekte tipa
“vector”), mnogo je pogodnije staviti da njen parametar bude upravo tipa “initializer_list”.
Dok bi konstruktore trebala da posjeduje praktično svaka klasa, destruktori su u većini slučajeva
neophodni jedino ukoliko klasa koristi dodatne resurse u odnosu na resurse koji predstavljaju njeni
atributi sami po sebi, npr. ukoliko kreira dinamički 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 oslobađaju nas mnogih problema, a
njihova deklaracija (a često i implementacija) je sasvim jednostavna, može smatrati pravilom da svaka
klasa koja vrši dodatno zauzimanje računarskih resursa (recimo dinamičku 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. U nekim slučajevima čak i klase koje ne
vrše nikakvo zauzimanje resursa mogu imati destruktore, u slučaju da nam je potrebno da se određena
skupina akcija automatski izvrši kad god neki primjerak te klase prestane postojati (recimo, ukoliko zbog
nekog razloga želimo brojati koliko ima aktivnih primjeraka neke klase, brojač primjeraka možemo
uvećavati unutar konstruktora, a umanjivati unutar destruktora). Tehnički gledano, destruktore zapravo
ima svaka klasa čak i ukoliko nije posebno definiran, jer u slučaju kada se ne definira destruktor,
kompajler automatski generira podrazumijevani destruktor sa praznim tijelom (i koji stoga izvršava
samo podrazumijevane akcije koje inače svaki destruktor uvijek izvršava).
Nažalost, mada destruktori rješavaju brojne probleme, oni također i stvaraju neke nove (srećom
lako rješive) probleme. Naime, činjenica da se destruktori uvijek automatski pozivaju nad objektom
neposredno prije nego što objekat prestane postojati, može dovesti do nepredvidljivog i veoma opasnog
ponašanja u slučajevima kada postoji više identičnih primjeraka iste klase. Ovi problemi su u osnovi
uzrokovani plitkim kopiranjem i rješavaju se definiranjem vlastitog kopirajućeg konstruktora (umjesto
podrazumijevanog automatski generiranog) i vlastitog operatora dodjele (umjesto podrazumijevanog).
Oni su toliko važni za ispravno funkcioniranje klase da se kao pravilo može uzeti da svaka klasa koja
posjeduje vlastiti destruktor (tj. onaj koji nije automatski generiran), gotovo uvijek mora posjedovati i
vlastiti kopirajući konstruktor i vlastiti operator dodjele. Ovo se često naziva zakon velike trojke (engl.
The Law of Big Three). Stoga iznenađuje da postoje brojne (loše) knjige o jeziku C++ koje kopirajuće
konstruktore spominju više uzgredno ili čak i nikako. Definiranju vlastitog operatora dodjele se također
ne pridaje dovoljna pažnja: on se obično opisuje u okviru općenite priče o preklapanju operatora i to
više kao kuriozitet nego kao nešto što je vitalno za ispravno funkcioniranje klase.
Već smo rekli da je kopirajući konstruktor (ili konstruktor kopije) specijalan slučaj konstruktora sa
jednim parametrom, čiji je formalni parametar referenca na konstantni objekat klase kojoj pripada.
Njegova uloga je da omogući upravljanje postupkom koji se odvija prilikom kopiranja jednog objekta u
drugi. Da bismo uvidjeli potrebu za definiranjem vlastitog kopirajućeg konstruktora, razmotrimo šta se

7
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

tipično dešava kada se jedan objekat kopira u drugi (npr. objekat A u objekat B). Do ovakvog kopiranja
može doći 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 odgovarajući formalni
parametar), zatim kada se neki objekat vraća kao rezultat iz funkcije, te kada se vrši dodjeljivanje nekog
objekta drugom objektu istog tipa. U svim ovim slučajevima osim posljednjeg (o čemu ćemo posebno
govoriti) to kopiranje vrši se posredstvom kopirajućeg konstruktora (dakle, prilikom dodjele, kopiranje se
ne vrši posredstvom kopirajućeg konstruktora, što je još jedan od razloga zbog kojeg treba praviti razliku
između inicijalizacije i dodjele). Pri tome podrazumijevani kopirajući konstruktor prosto kopira sve
atribute objekta A u odgovarajuće atribute objekta B. Međutim, kada god objekti sadrže atribute koji su
pokazivači koji pokazuju na dinamički alocirane resurse koji logički pripadaju tom objektu (ovo ne
uključuje pokazivače koji pokazuju na objekte koji nisu dinamički alocirani, ili objekte koji logički ne
pripadaju tom objektu, o čemu smo ranije govorili u kontekstu odnosa između objekata tipa “Knjiga” i
“Student”), ovo podrazumijevano kopiranje može dovesti do kreiranja tzv. plitke kopije o čemu smo
također govorili (kada smo opisivali strukturu “Matrica”), u kojoj nakon kopiranja oba objekta sadrže
pokazivače koje pokazuju na iste resurse u memoriji, koji pri tome logički pripadaju objektu. U
kombinaciji sa destruktorima, plitke kopije mogu biti fatalne (često se kaže da se destruktori i plitke
kopije “ne vole”). Na primjer, pretpostavimo da objekti A i B sadrže pokazivače na isti dinamički niz u
memoriji i da zbog nekog razloga objekat A u nekom trenutku prestane postojati. Tom prilikom će se
pozvati njegov destruktor, koji najvjerovatnije uništava taj dinamički niz. Međutim, objekat B (koji i
dalje živi) sada sadrži pokazivač na uništeni niz (tj. viseći 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” proširiti funkcijom “ZbirVektora” koja vraća
kao rezultat zbir dva 𝑛-dimenzionalna vektora koji su joj proslijeđeni 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 omogućili pristup privatnim članovima klase, deklariraćemo 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);
};

Na prvi pogled je sve u redu, tako da je, uz pretpostavku da su “a” i “b” dva vektora iste dimenzije,
moguće napisati naredbu poput
VektorNd c = ZbirVektora(a, b); // OVO ĆE NAPRAVITI PROBLEM!!! 
Na žalost, nije sve baš tako lijepo kao što izgleda. Napisana klasa sadrži ozbiljan propust, a gore prikazana
naredba može dovesti do teških posljedica koje se mogu očitovati tek nakon izvjesnog vremena. Naime,
nakon gornje naredbe sva tri vektora “a”, “b” i “c” sadržavaće viseće pokazivače! Da bismo vidjeli zašto,
razmotrimo šta se zaista dešava 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” sadrže pokazivače na iste dijelove memorije. Nakon toga se formira lokalni vektor “v3” koji se
popunjava zbirom vektora “v1” i “v2”. Ovaj vektor se vraća kao rezultat funkcije pri čemu dolazi do
njegovog kopiranja u vektor “c” (stoga će pokazivači u vektorima “v3” i “c” pokazivati na isti dio
memorije). Međutim, po završetku funkcije, uništavaju se sva tri lokalna vektora “v1”, “v2” i “v3” (formalni
parametri su također lokalni objekti). Prilikom ovog uništavanja dolazi do pozivanja destruktora klase
“VektorNd” nad svakim od ova tri objekta, koji će osloboditi memoriju koju su zauzimali dinamički
alocirani nizovi na koje pokazuju pokazivači unutar vektora “v1”, “v2” i “v3” (što i jeste osnovni zadatak
destruktora). Međutim, vektori “a”, “b” i “c” sadrže pokazivače koji pokazuju na iste dijelove memorije,
tako da će nakon izvršenja ove naredbe sva tri vektora sadržavati pokazivače koji pokazuju na upravo
oslobođene dijelove memorije. Stoga, njihovo dalje korištenje može imati kobne posljedice!

8
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

Šta da se radi? Kopiranje vektora “a” i “b” u “v1” i “v2” bismo mogli lako izbjeći 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 preporučeni način
prenosa primjeraka klasa u funkcije. Međutim, kopiranje rezultata koji se nalazi u vektoru “ v3” nije
moguće izbjeći čak ni vraćanjem reference na njega kao rezultata (ne smijemo vratiti referencu na objekat
koji prestaje postojati). Očito, probleme pravi interakcija između plitkog kopiranja i destruktora (već je
rečeno da se destruktori i plitke kopije “ne vole”). Pravo rješenje je promijeniti mehanizam kopiranja,
tako da kopija objekta dobija ne samo kopiju pokazivača nego i kopiju odgovarajućeg dinamičkog niza
pridruženog pokazivaču. Na taj način će destruktor kopiranog objekta uništiti “svoj” a ne i “tuđi”
dinamički niz. Sljedeća slika ilustrira razliku između plitke i potpune (duboke) kopije:
Plitka kopija: Duboka kopija:
a v1 a v1
dimenzija koordinate dimenzija koordinate dimenzija koordinate dimenzija koordinate

4 4 4 4

Neko se može zapitati zbog čega podrazumijevani kopirajući konstruktori u jeziku C++ ne vrše
postupak dubokog nego plitkog kopiranja. Za to postoje dva razloga. Prvo, kompajler ne može znati na šta
pokazuju pokazivači koji se nalaze unutar objekta (recimo, da li pokazuju na dinamički alocirane
resurse koji logički pripadaju klasi, ili na nešto što logički uopće nije sastavni dio klase), jer to može
zavisiti od toga šta je programer radio sa tim pokazivačima u čitavom ostatku programa (kompajler
može znati samo koju adresu sadrži pokazivač, ali ne može znati šta logički 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 omogućeno da se definiraju vlastiti kopirajući konstruktori, koji
omogućavaju projektantu klase da sâm definira postupak kako se objekti te klase trebaju kopirati,
odnosno da po potrebi implementira duboko kopiranje objekata na način kako mu to odgovara.
Na osnovu prethodnog izlaganja, slijedi da je rješenje opisanog problema sa klasom “VektorNd”
definiranje vlastitog kopirajućeg konstruktora koji će kreirati potpunu (duboku) kopiju objekta. Stoga
ćemo u javni dio klase dodati i deklaraciju vlastitog kopirajućeg konstruktora:
class VektorNd {

VektorNd(const VektorNd &v); // Deklaracija kopirajućeg konstruktora

};

S obzirom da implementacija kopirajućeg konstruktora nije sasvim kratka, izvešćemo je izvan


deklaracije klase. Razmotrimo šta taj konstruktor zapravo treba da obavi. Atribut “dimenzija” svakako
treba kopirati, ali atribut-pokazivač “koordinate” ne treba prosto da se kopira. Umjesto toga, treba
stvoriti novi dinamički niz i kopirati izvorni dinamički niz u novostvoreni niz. Radi bolje efikasnosti, sve
atribute koji se mogu inicijalizirati u konstruktorskoj inicijalizacijskoj listi ćemo tako i inicijalizirati.
Također, za kopiranje elemenata, efikasnije je koristiti funkciju “copy” iz biblioteke “algorithm” nego
vršiti kopiranje element po element uz pomoć for-petlje. Stoga bi implementacija kopirajućeg
konstruktora 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 pozivi kopirajućeg konstruktora “nužno zlo” i da kompajleri imaju pravo
(ali ne i obavezu) da izbjegnu njihovo pozivanje kada je god to moguće (ovo pravo je pretvoreno u
obavezu u standardu C++17). To izbjegavanje poznato je pod nazivom elizija kopirajućeg konstruktora.
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 nizašta (recimo,
prestaju postojati), kompajler ima pravo direktno kopirati objekat A u objekat D uz samo jedan poziv

9
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

kopirajućeg konstruktora (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
nizašta, kompajler ima pravo da u potpunosti izbjegne kopiranja (i pozive kopirajućeg konstruktora) i da
direktno konstruira objekat C na isti način kako bi konstruirao objekat A. Stoga kopirajući konstruktori
nikada ne bi trebali obavljati ništa što izlazi iz okvira postupaka za kopiranje objekata, jer nikada ne
možemo biti sigurni koliko će se puta i kada kopirajući konstruktor zaista pozvati (s obzirom da
kompajler ima pravo izbjeći njihov poziv kad god je to moguće). Posebno, logika programa ne bi smjela
ovisiti od toga koliko će se puta kopirajući konstruktor pozvati. Elizija kopirajućeg konstruktora zapravo
predstavlja jednu od rijetkih optimizacija koja narušava tzv. as-if pravilo (engl. as-if rule), prema kojem
kompajler u fazi optimizacije ima pravo da transformira program na bilo kakav način s ciljem da mu
poboljša efikasnost, ali ukoliko je taj program legalan (tj. ukoliko program ne sadrži nikakva nedefinirana
ponašanja), ta transformacija ne smije izmijeniti funkcioniranje programa, tj. vidljivi rezultati izvršavanja
programa moraju biti potpuno isti kao da optimizacija uopće nije vršena (naziv pravila potiče upravo od
ove fraze “kao da”).
Često se javlja situacija da objekti koji se kopiraju imaju samo privremeni status i zna se da će biti
uništeni čim se obavi njihovo kopiranje. Recimo, pretpostavimo da nekoj funkciji koja prima objekat tipa
“VektorNd” po vrijednosti pošaljemo kao parametar izraz poput “ZbirVektora(a, b)”. U ovom slučaju,
kreira se privremeni objekat koji predstavlja rezultat funkcije “ZbirVektora”, koji se potom kopira u novu
funkciju posredstvom kopirajućeg konstruktora i odmah uništava. Prirodno se postavlja pitanje ako već
znamo da će taj privremeni objekat biti uništen (i neće kasnije trebati nizašta), zar nije bolje izbjeći
kopiranje i prosto “ukrasti” tom objektu pokazivač na dinamički alocirani niz (kao kod plitkog kopiranja),
ali da pri tom nekako spriječimo brisanje tog dinamički alociranog niza kada bude nad tim privremenim
objektom pozvan destruktor. To bi, u izvjesnom smislu bilo “premještanje” (odnosno “pomjeranje”)
dinamički alociranog niza iz objekta koji se kopira u objekat u koji se vrši kopiranje. Da bi se omogućilo
tako nešto, od verzije C++11 nadalje uvedena je specijalna vrsta kopirajućeg konstruktora nazvana
pomjerajući konstruktor (engl. move constructor). Za razliku od klasičnog kopirajućeg 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 pomjerajući konstruktor za klasu
“VektorNd” (s obzirom da na kratkoću, njegova implementacija je data odmah unutar definicije klase):
class VektorNd {

VektorNd(VektorNd &&v) : dimenzija(v.dimenzija), koordinate(v.koordinate) {
v.koordinate = nullptr;
}

};

Razmotrimo kako ovaj konstruktor radi. Kako mu je parametar r-vrijednosna referenca, on sigurno
zna da je ono što mu je proslijeđeno neki privremeni a ne imenovani objekat, koji će svakako biti
uništen. On zato prosto kopira pokazivač na dinamički alocirani niz iz objekta koji se kopira u odredišni
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 neće uraditi ništa, jer će se izvršiti naredba “delete” nad
nul-pokazivačem. Na taj način su resursi prosto “premješteni” iz izvorišnog u odredišni objekat.
Ukoliko klasa ima definiran i pomjerajući konstruktor, za tu klasu automatski postaje podržana i
move-semantika prilikom inicijalizacije objekata ili prenosa parametara u funkciju, odnosno prilikom
vraćanja rezultata iz funkcije. Recimo, pretpostavimo da je “a” neki objekat tipa “VektorNd” i da izvršimo
inicijalizaciju poput
VektorNd b = std::move(a); // Ovdje se koristi pomjerajući konstruktor!

Sve što funkcija “move” radi je da vraća svoj argument kao rezultat, ali sa izmijenjenim statusom tako da
se tretira kao r-vrijednost. Tačnije, ona vraća r-vrijednosnu referencu na svoj argument (što sâmo po
sebi ne bi bilo moguće, s obzirom da je njen argument l-vrijednost). Stoga će se za inicijalizaciju objekta
“b” koristiti pomjerajući, a ne klasični kopirajući konstruktor, koji će prosto premjestiti resurse iz objekta
“a” u “b” (a objekat “a” ostaviti u neupotrebljivom stanju sa nul-pokazivačem u atributu “koordinate”, ali
svakako smo pozivom funkcije “move” signalizirali da nam taj objekat više ne treba). Sve u svemu,
vidimo da čitava “magija” move-semantike uopće ne leži u samoj “move” funkciji, nego u pomjerajućim
konstruktorima (i pomjerajućim operatorima dodjele, koje ćemo uskoro upoznati), a funkcija “ move” tu

10
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

služi samo da “pripremi teren” za primjenu ovih mehanizama (dakle, usprkos svome imenu, funkcija
“move” zapravo ne pomjera ništa). Inače, ukoliko klasa nema definiran pomjerajući konstruktor, prilikom
kopiranja primjeraka te klase se uvijek koristi klasični kopirajući konstruktor, bez obzira da li se kopira
l-vrijednost ili r-vrijednost.
Vidjeli smo da kopirajući konstruktori koje kreira programer mogu obezbjediti duboko kopiranje,
čime možemo biti sigurni da će svi različiti objekti imati svoje neovisne primjerke alociranih resursa,
odnosno da se neće “jedan drugom petljati u posao”. Ipak, kopirajući konstruktor se eventualno poziva
samo u tri od četiri situacije u kojima bi mogle nastati plitke kopije. Naime, već smo rekli da se kopirajući
konstruktor eventualno poziva pri inicijalizaciji novostvorenog objekta drugim objektom istog tipa, pri
prenosu po vrijednosti objekata u funkcije i pri vraćanju objekata kao rezultata iz funkcije. Međutim,
prilikom dodjeljivanja nekog objekta drugom objektu koji od ranije postoji, kopirajući konstruktor se
nikada ne poziva! To znači da ukoliko bismo izvršili naredbe poput
b = a; // Dodjela!
c = ZbirVektora(a, b); // Također dodjela!

pri čemu sva tri vektora (tj. objekta tipa “VektorNd”) “a”, “b” i “c” postoje od ranije, u oba slučaja će biti
izvršena plitka kopija, bez obzira što smo definirali vlastiti kopirajući konstruktor! Razlog zašto se
kopirajući konstruktor ne poziva i u ovom slučaju je što se ovaj slučaj razlikuje od prva tri po tome što
objekat kojem se vrši dodjela već od ranije postoji, pa kopirajući konstruktor ne može da zna šta treba
da radi sa prethodnim sadržajem objekta (u sva tri preostala slučaja 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 izvršimo dodjelu “b = a”. Prije izvršene dodjele, oba vektora “a” i “b” sadrže pokazivače
koji pokazuju na dva različita dinamički alocirana niza različite veličine. Nakon obavljenog plitkog
kopiranja, oba pokazivača pokazivaće na dinamički niz dodijeljen prvom objektu, dok na dinamički niz
koji je bio pridružen objektu “b” više ne pokazuje niko. Dakle, u ovom slučaju dolazi i do curenja
memorije, s obzirom da taj dinamički niz više ne može obrisati niko. Ova situacija prikazana je na
sljedećoj slici:
Prije kopiranja: Poslije kopiranja:
a b a b
dimenzija koordinate dimenzija koordinate dimenzija koordinate dimenzija koordinate

4 3 4 4

Važno je uočiti da čak ni duboko kopiranje kakvo je implementirano u kopirajućem konstruktoru


ne bi riješilo problem. Pri takvom kopiranju bila bi doduše izvršena alokacija novog dinamičkog niza u
koji bi bio iskopiran niz koji pripada objektu “a”, ali dinamički niz koji pripada objektu “b” ne bi bio
uništen (niti bi ga iko kasnije mogao uništiti). Dakle, mada ne bismo imali plitku kopiju, i dalje bismo
imali curenje memorije. Očito se prilikom dodjeljivanja nekog objekta nekom drugom objektu koji je već
postojao od ranije, trebaju poduzeti drugačije akcije nego što su predviđene kopirajućim konstruktorom.
To je razlog zbog kojeg se kopirajući konstruktor i ne poziva u ovom slučaju. Sada postaje posve jasno
zbog čega smo stalno insistirali na pravljenju oštre razliku između inicijalizacije i dodjele (na ovu razliku
ćemo mnogo lakše misliti ukoliko se od samog početka naviknemo da za inicijalizaciju iole složenijih
objekata bez velike potrebe nikada ne koristimo sintaksu koja koristi znak dodjele “=”).

Opisani problem bi se mogao riješiti kada bi se prilikom dodjele poput “b = a” prvo izvršio destruktor
nad objektom “b”, a zatim iskoristio kopirajući konstruktor za kopiranje objekta “a” u objekat “b”.
Međutim, tvorci jezika C++ namjerno nisu željeli automatski podržati ovakvo ponašanje, jer ono uglavnom
nije i najbolji način da se ostvari ispravna funkcionalnost. Na primjer, u slučaju da su vektori “a” i “b” iste
dimenzije, najprirodnije ponašanje prilikom dodjele “b = a” je prosto samo iskopirati sve elemente
dinamičkog niza pridruženog objektu “a” u dinamički niz dodijeljen objektu “b” (koji već postoji od
ranije). Nikakve dodatne alokacije niti dealokacije memorije nisu potrebne. Stoga, jezik C++ dopušta da
sami definiramo kako će se interpretirati izraz oblika “b = a” u slučaju kada objekat “b” odranije postoji.
Kao što znamo, podrazumijevano ponašanje ovog izraza je isto kao prosto kopiranje svih atributa objekta

11
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

“a” u odgovarajuće atribute objekta “b”, jedan po jedan (odnosno, isto kao i kod podrazumijevanog
kopirajućeg konstruktora). Međutim, treba znati da u slučaju 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
između ključne riječi “operator” i znaka “=” nije obavezan). Ukoliko projektant klase ne definira sam takvu
funkciju (ili više njih sa različitim tipovima parametara), kompajler će sam generirati podrazumijevanu
verziju te funkcije koja prima kao parametar referencu na konstantni objekat istog tipa, a koja obavlja
podrazumijevani postupak dodjele (tj. prosto kopiranje atributa). Tada govorimo o podrazumijevanom
operatoru dodjele. Međutim, projektant klase može napisati vlastite verzije funkcije “operator =” i na taj
način može sam odrediti kako će se tačno izvoditi postupak dodjele. To se naziva preklapanje operatora
dodjele (engl. assignment operator overloading).

Mada je preklapanje operatora dodjele specijalan slučaj općeg preklapanja operatora o kojem ćemo
govoriti kasnije, očekivano ponašanje operatora dodjele je tijesno vezano za ponašanje kopirajućeg
konstruktora, tako da je o njegovom preklapanju prirodno govoriti odmah nakon opisa kopirajućeg
konstruktora. U principu, funkcija “operator =” može da prima parametar bilo kojeg tipa i da vraća
rezultat bilo kojeg tipa. Međutim, za ostvarenje one funkcionalnosti koju ovdje želimo postići (međusobno
dodjeljivanje objekata istog tipa), njen formalni parametar treba biti referenca na konstantni objekat
pripadne klase (dakle, isto kao i kod kopirajućeg konstruktora), dok tip rezultata treba također biti
referenca na objekat pripadne klase. U tom slučaju govorimo o kopirajućem operatoru dodjele (engl.
copy assignment operator). Ukoliko želimo optimizirati dodjelu (izbjegavanjem kopiranja) u slučaju
kada je izraz sa desne strane dodjele r-vrijednost, možemo definirati i verziju koja kao parametar
prima r-vrijednosnu referencu (ovo je podržano od C++11 nadalje) i tada govorimo o pomjerajućem
operatoru dodjele (engl. move assignment operator). U skladu s tim, u klasu “VektorNd” ćemo dodati
sljedeće deklaracije:
class VektorNd {

VektorNd &operator =(const VektorNd &v); // Kopirajuća verzija
VektorNd &operator =(VektorNd &&v); // Pomjerajuća verzija

};

Očigledno, ovdje smo se odlučili da podržimo kako kopirajući, tako i pomjerajući operator dodjele
(vlastiti kopirajući operator dodjele svakako moramo podržati da bi klasa ispravno funkcionirala). U
slučaju da pomjerajući operator dodjele nije definiran, kopirajući operator dodjele će se koristiti bez
obzira da li je izraz sa desne strane l-vrijednost ili ne.

Naravno, ove deklarirane funkcije članice treba i implementirati. Počećemo od kopirajuće verzije,
koja je složenija. Na prvom mjestu, važno je uočiti razliku između neophodnog ponašanja kopirajućeg
konstruktora i kopirajućeg operatora dodjele. Operator dodjele se nikad ne poziva pri inicijalizaciji
objekata (čak ni ukoliko se inicijalizacija vrši pomoću sintakse koja koristi znak dodjele “=”), nego samo
kada se dodjela vrši nad objektom koji od ranije postoji. tako da je za njegove potrebe već alocirana
memorija. Jedna mogućnost (mada često neefikasna) je da operator dodjele prvo izvrši sve ono što bi
radio destruktor, a zatim sve ono što bi radio kopirajući konstruktor (ovo je upravo ono što tvorci
jezika C++ nisu željeli da se dešava automatski, upravo zbog potencijalne neefikasnosti). Bez obzira što
ovo često nije najbolje rješenje, razmotrimo prvo kako bi se takvo rješenje moglo izvesti. Naivna
izvedba ove ideje mogla bi izgledati ovako:
VektorNd &VektorNd::operator =(const VektorNd &v) {
if(&v != this) { // Samododjela?
delete[] koordinate;
dimenzija = v.dimenzija; koordinate = new double[dimenzija];
std::copy(v.koordinate, v.koordinate + v.dimenzija, koordinate);
}
return *this;
}

Osim nekih dodatnih detalja, implementacija je jasna sama po sebi. Prvi detalj na koji treba obratiti
pažnju je test koji glasi “if(&v != this)”. Ovim testom se ispituje da li su izvorni i odredišni objekat
(vektor) identični, i ukoliko jesu, ne radi se ništa. Svrha ove naredbe je da se izbjegne opasnost od tzv.
destruktivne samododjele (engl. destructive self-assignment). Samododjela je naredba koja je logički
ekvivalentna naredbi “a = a”. Naime, u slučaju da dođe do samododjele, u slučaju da nismo preduzeli

12
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

posebne mjere opreza, objekat sa lijeve strane bio bi uništen, ali bi samim tim bio uništen i objekat sa
desne strane, pošto se radi o istom objektu! Razumije se da niko neće eksplicitno pisati naredbe poput
“a = a”, ali se skrivene situacije koje su logički 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” suštinski
ekvivalentna dodjeli “a = a”, a situacije da su dvije reference vezane na isti objekat (aliasing) uopće nije
rijetka, naročito u programima gdje se mnogo koristi prenos parametara po referenci. Samododjela
može nastati i u mnogim drugim situacijama (npr. “v[i] = v[j]” je samododjela kad god “i” i “j” imaju
istu vrijednost, “*p = *q” je samododjela kadgod “p” i “q” pokazuju na isti objekat, itd.). Zbog toga,
funkcija koja realizira preklopljeni operator dodjele nikada ne smije brisati odredišni objekat bez
prethodne garancije da on nije ekvivalentan izvornom objektu.
Drugi detalj je to što preklopljeni operator dodjele kao rezultat vraća referencu na objekat nad
kojim se sama dodjela vrši (što se posiže dereferenciranjem pokazivača “this”). Mada nema pravila koje
nalaže da se tako mora raditi, to se gotovo uvijek radi. Razlog za to je što se na taj način omogućava
ulančavanje operatora dodjele, odnosno konzistentno izvršavanje 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. izvršavanje prvo dodjele “b = c” a zatim
dodjele “a = b”), njegov podizraz “b.operator =(c)” mora vratiti kao rezultat objekat “b”, a upravo to se
postiže na opisani način.
Pored potencijalne neefikasnosti, prikazano naivno rješenje posjeduje još jedan ozbiljan nedostatak:
ne posjeduje sigurnost pri izuzecima. Naime, pri izvršavanju dodjele poput “a = b” prvo se uništava
dinamički alocirana memorija koju je posjedovao objekat “a”, nakon čega se izvršava nova alokacija.
Međutim, ukoliko ta alokacija ne uspije, baca se izuzetak, a objekat “a” ostaje u krajnje neispravnom
stanju (memorija koju je posjedovao taj objekat je uništena, a prije bacanja izuzetka je još i prekopirana
informacija o dimezniji iz objekta “b” u objekat “a”). Da bismo dobili izvedbu koja posjeduje sigurnost
pri izuzecima (i to jaku sigurnost pri izuzecima, koja dodatno garantira da će u slučaju bacanja izuzetka
odredišni objekat ostati neizmijenjen, kao da se ništa nije ni dogodilo), izmijenićemo malo redoslijed
pojedinih operacija, odnosno prvo ćemo zauzeti novi prostor, pa tek tada vršiti korekcije na
odredišnom objektu (što uključuje i brisanje memorije koju je objekat do tada postojao). U tom slučaju,
ukoliko alokacija baci izuzetak, odredišni objekat ostaje u potpuno istom stanju kakav je bio i prije toga:
VektorNd &VektorNd::operator =(const VektorNd &v) {
double *novi_prostor = new double[v.dimenzija];
std::copy(v.koordinate, v.koordinate + v.dimenzija, novi_prostor);
delete[] koordinate;
dimenzija = v.dimenzija; koordinate = novi_prostor;
return *this;
}

Interesantno je da je u prikazanom rješenju također otpala i potreba za testiranjem na destruktivnu


samododjelu (iako ne bi bilo ni štete od tog testiranja). Naime, nije teško vidjeti da uz ovakvo rješenje
eventualna samododjela ne bi bila destruktivna (iako bi bilo “presipanja iz šupljeg u prazno”). Naravno,
moguće je dodati taj test da se izbjegnu nepotrebne operacije u slučaju samododjele, ali mnogi smatraju
da za tim nema potrebe, s obzirom da su situacije u kojima dolazi do samododjele rijetke.
U oba prethodno prikazana rješenja, praktično se dupliraju naredbe prisutne u destruktoru kao i u
kopirajućem konstruktoru. U slučajevima kada su destruktor i kopirajući konstruktor još složeniji,
takvo dupliranje može biti jako nepraktično. Međutim, postoji jedna veoma interesantna tehnika,
poznata pod nazivom kopiraj-i-razmijeni (engl. copy & swap) koja u potpunosti eliminira ova dupliranja,
uz vrlo jednostavnu izvedbu, a također posjeduje jaku sigurnost na izuzetke. Primjenom ove tehnike,
kopirajući operator dodjele izveo bi se ovako:
VektorNd &VektorNd::operator =(VektorNd v) {
std::swap(dimenzija, v.dimenzija); std::swap(koordinate, v.koordinate);
return *this;
}

Na prvom mjestu, treba uočiti da parametar “v” nije referenca, odnosno koristi se prenos parametra
po vrijednosti. Time, kada pokušamo izvršiti dodjelu poput “a = b”, parametar “v” će biti kopija objekta
koji se dodjeljuje (“b” u navedenom primjeru). Ova kopija se kreira putem kopirajućeg konstruktora,

13
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

koji vrši i neophodnu alokaciju resursa. Unutar tijela se vrši razmjena svih atributa između novokreirane
kopije i odredišnog objekta, tj. objekta kojem se dodjeljuje (“a” u navedenom primjeru), čime odredišni
objekat i novostvorena kopija faktički razmjenjuju uloge (odredišni objekat postaje ta novostvorena
kopija, a novostvorena kopija postaje ono što je do tada bio odredišni objekat). Na kraju, parametar “v”
(koji je sad preuzeo ulogu onoga što je prethodno bio odredišni objekat) se uništava, kao i svaka druga
lokalna promjenljiva. Prilikom tog uništavanja, poziva se destruktor, koji će osloboditi resurse koje je
odredišni objekat ranije zauzimao!

Kao što vidimo, tehnika kopiraj-i-razmijeni je u osnovi vrlo jednostavna, i izvodi se rutinski. Jedino
treba paziti da se razmjena atributa između novokreiranog i odredišnog objekta izvede što je god
moguće efikasnije. Ukoliko su atributi iole komplesnijih tipova, razmjenu obavezno treba izvesti koristeći
move-semantiku, uz pretpostavku da je razmatrani tip podržava (a podržavaju je svi standardni
bibliotečki tipovi). U suprotnom, ova tehnika vjerovatno nije isplativa. Najbolje je za tu svrhu koristiti
bibilotečku funkciju “swap”, koja razmjenu izvodi primjenom move-semantike kad god je to moguće.

Bez obzira na jednostavnost opisane tehnike, svođenje dodjele na destrukciju odredišnog objekta
te kopiranje izvornog u odredišni objekat nije uvijek najbolja ideja (da jeste, bilo bi izvedeno da se to
dešava automatski). Naime, kako se operator dodjele primjenjuje nad objektom koji od ranije postoji,
za njegove njegove potrebe su već alocirani izvjesni resursi (u našem primjeru memorija). Stoga bi
bolja ideja bila da se provjeri da li je alocirana količina memorije dovoljna da prihvati podatke koje
treba kopirati. Ukoliko jeste, dovoljno je samo izvršiti kopiranje (uz prilagođavanje informacije o
dimenziji). Međutim ukoliko nije, potrebno je alocirati neophodnu količinu memorije, obaviti kopiranje
u novi prostor, te dealocirati stari prostor. Uz ovakve preporuke, moguća implementacija funkcije
članice koja realizira preklopljeni operator dodjele mogla bi izgledati recimo ovako (primijetimo da nije
neophodan test na destruktivnu samododjelu, jer se brisanje svakako ne vrši ako su veličine jednake):
VektorNd &VektorNd::operator =(const VektorNd &v) {
if(dimenzija < v.dimenzija) {
double *novi_prostor = new double[v.dimenzija]; // Realokacija...
delete[] koordinate; koordinate = novi_prostor;
}
dimenzija = v.dimenzija;
std::copy(v.koordinate, v.koordinate + v.dimenzija, koordinate);
return *this;
}

U prikazanoj implementaciji, realokacija memorije se izvodi samo u slučaju kada se vektoru manje
dimenzionalnosti dodjeljuje vektor veće dimenzionalnosti. Na primjer, ukoliko je od ranije “a” bio
sedmodimenzionalni a “b” petodimenzionalni vektor, dodjela poput “b = a” mora izvršiti realokaciju da
bi “b” bio spreman da prihvati sve komponente vektora “a”. Međutim, u slučaju da je dimenzionalnost
vektora “b” veća od dimenzionalnosti vektora “a”, dinamički niz vezan za vektor “b” već sadrži dovoljno
prostora da prihvati sve komponente vektora “a”, tako da realokacija nije neophodna. U svakom slučaju,
ovo je mnogo efikasnije nego kad bi se realokacija vršila uvijek. Naravno, u slučaju da je dimenzionalnost
vektora “a” mnogo manja od dimenzionalnosti vektora “b”, također je mudro izvršiti realokaciju, da se
bespotrebno ne zauzima mnogo veći blok memorije nego što je zaista potrebno (ova ideja nije ugrađena u
gore prikazanu izvedbu). Uglavnom, poenta je da preklapanje operatora dodjele omogućava projektantu
klase da specificira šta će se tačno dešavati prilikom izvršavanja dodjele i na koji način. Recimo, moguće je
dodjelu poput “b = a” podržati samo ukoliko su “a” i “b” iste dimenzije, a u suprotnom recimo baciti
izuzetak. Ili, moguće je realizirati takvu dodjelu pri kojoj bi dimenzija vektora “b” nakon dodjele ostala
ista kao prije dodjele, uz odbacivanje suvišnih elemenata u slučaju da vektor “a” ima veću dimenziju, ili
dopunjavanje nulama u slučaju da vektor “a” ima manju dimenziju od vektora “b”. Takva dodjela naziva se
Procrustovska dodjela (prema Procrustu, gostioničaru i razbojniku iz grčke mitologije, koji je prema
legendi svoje žrtve stavljao u krevet i istezao ih ili im je sjekao udove sve dok im dužina ne bude jednaka
dužini kreveta). Ipak, mada je moguće izvesti Procrustovsku dodjelu, ona na izvjestan način “nije u duhu”
jezika C++ (za razliku od nekih drugih jezika u kojima je sasvim prirodna). Naime, u duhu jezika C++ je da
kad god dodjela poput “b = a” radi, nakon nje objekat “b” treba da po svemu bude identičan objektu “a”.
Implementacija pomjerajućeg operatora dodjele je znatno jednostavnija, s obzirom da je dovoljno
obrisati resurse objekta nad kojim se vrši dodjela, a zatim “ukrasti” resurse objektu koji se dodjeljuje,
uz postavljanje pokazivača na njegove resurse na nul-pokazivač (čime se sprečava njegov destruktor da
oslobodi resurse koji su sada premješteni u odredišni objekat). To bi moglo izgledati recimo ovako:

14
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

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


if(&v != this) { // Samododjela?
delete[] koordinate; koordinate = v.koordinate;
dimenzija = v.dimenzija; v.koordinate = nullptr;
}
return *this;
}

Pomjerajući operator dodjele omogućava nam da konstrukcije poput “c = ZbirVektora(a, b)” postanu
mnogo efikasnije, jer je objekat sa desne strane privremeni objekat koji će svakako biti uništen čim se
iskoristi, pa je mnogo pametnije prosto “ukrasti” njegove resurse. Također, definiranjem pomjerajućeg
operatora dodjele automatski je podržana i move-semantika prilikom dodjeljivanja. Zaista, napišemo li
nešto poput “b = std::move(a)”, dodjela nad objektom “b” izvršiće se posredstvom pomjerajućeg a ne
kopirajućeg operatora dodjele. Zaista, poziv funkcije “move” doveo je do toga da se izraz sa desne strane
dodjele ne tretira više kao l-vrijednost nego kao r-vrijednost. Treba još napomenuti da mada na prvi
pogled izgleda da je kod pomjerajućeg operatora dodjele nepotreban test na destruktivnu samododjelu,
on je ipak potreban da se spriječe problemi koji bi mogli nastati ukoliko se izvrši naredba poput
“a = std::move(a)” ili neka druga koja joj je logički ekvivalentna.
Mogućnost da se prilikom kopiranja objekata razdvoji tretman privremenih bezimenih i trajnih
imenovanih objekata i na taj način drastično poboljša efikasnost i podrži move-semantika, smatra se
jednom od najrevolucionarnijih inovacija uvedenih u C++11. Štaviše, nekad je dovoljno stare programe
koji nisu kompajlirani kompajlerom koji podržava C++11 standard samo rekompajlirati nekim novijim
kompajlerom da on postane brži i efikasniji, iako sam program nije koristio nikakve C++11 specifičnosti
(to je posljedica činjenice da su u novijim kompajlerima bibliotečke funkcije redizajnirane da interno
koriste move-semantiku kad god je to moguće). Radi podrške move-semantici, na zakon velike trojke se
od C++11 nadalje dodaje “amandman” po kojem bi klasa koja ima definirane destruktore trebala imati
ne samo vlastiti kopirajući konstruktor i vlastiti (kopirajući) operator dodjele, nego vrlo vjerovatno i
pomjerajući konstruktor, te pomjerajući operator dodjele. Tako dopunjeni zakon velike trojke ponegdje
se naziva zakon velike petorke (engl. The Law of Big Five).
Interesantno je da se pomjerajući operator dodjele može efikasno i jednostavno napisati praktično na
posve isti način kao i kopirajući operator dodjele izveden primjenom kopiraj-i-razmijeni tehnike (samo
što ovdje “v” nije kopija nego referenca na privremeni bezimeni objekat):
VektorNd &VektorNd::operator =(VektorNd &&v) {
std::swap(dimenzija, v.dimenzija); std::swap(koordinate, v.koordinate);
return *this;
}

Zapravo, zahvaljujući eliziji kopirajućeg konstruktora, kopirajući operator dodjele izveden tehnikom
kopiraj-i-razmijeni svešće se tačno na ovo u slučaju kada je operand sa desne strane dodjele privremeni
bezimeni objekat. Stoga, kad god je kopirajući operator dodjele izveden na takav način, tada pomjerajući
operator dodjele uopće i ne treba praviti. Štaviše, u tom slučaju, pomjerajući operator dodjele ne samo
da nije potreban, nego se i ne smije pisati. Da bismo vidjeli zašto, pretpostavimo da u isto vrijeme u klasi
imamo kako kopirajući operator dodjele izveden tehnikom kopiraj-i-razmijeni, tako i pomjerajući
operator dodjele:
class VektorNd {

VektorNd &operator =(VektorNd v);
VektorNd &operator =(VektorNd &&v);
// Kopiraj-i-dodijeli verzija
// Pomjerajuća verzija pravi konflikt! 

};

Ukoliko bismo ovo uradili nastao bi problem kada bismo nekom objektu tipa “VektorNd” pokušali
dodijeliti neki privremeni objekat istog tipa, jer obje ponuđene verzije operatora dodjele mogu kao
parametar prihvatiti privremeni objekat, pa bi bilo nejasno koju od njih treba pozvati!

Već smo rekli da se u slučaju da ne definiramo vlastiti kopirajući konstruktor, automatski generira
podrazumijevani kopirajući konstruktor, koji prosto kopira sve atribute jednog objekta u drugi. Pri
tome, ukoliko je neki atribut tipa klase koja posjeduje vlastiti kopirajući konstruktor, njen kopirajući
konstruktor će biti iskorišten za kopiranje odgovarajućeg atributa. Interesantno je da se u tom slučaju
također generira i podrazumijevani pomjerajući konstruktor (od C++11 nadalje), koji će za pomjeranje

15
Dr. Željko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 11_a
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2021/22

atributa (pri kopiranju privremenih objekata ili prilikom upotrebe move-semantike) iskoristiti njihove
vlastite pomjerajuće konstruktore (ukoliko postoje). Slično vrijedi i za operator dodjele, odnosno ukoliko
nije definiran vlastiti kopirajući operator dodjele, automatski se generiraju podrazumijevani kako
kopirajući, tako i pomjerajući operator dodjele. Slijedi da nije potrebno definirati vlastite kopirajuće i
pomjerajuće konstruktore niti operatore dodjele za bezbjedno kopiranje, pomjeranje i međusobno
dodjeljivanje objekata koji sadrže npr. atribute tipa “vector” ili “string” (pa ni atribute tipa “VektorNd”
koji smo sami razvili), jer će se za ispravno obavljanje tih operacija pobrinuti kopirajući i pomjerajući
konstruktori odnosno operatori dodjele odgovarajućih atributa (naravno, vlastiti kopirajući konstruktori
i prateći rekviziti će sigurno biti potrebni u slučaju da pored takvih atributa, klasa posjeduje i dodatne
pokazivače koji pokazuju na dinamički alocirane resurse). Ova činjenica pruža mogućnost da se u
velikom broju praktičnih slučajeva u potpunosti izbjegne potreba za definiranjem kopirajućeg
konstruktora i pratećih rekvizita. Naime, umjesto korištenja dinamičke alokacije memorije, možemo
koristiti tipove poput “vector” i “string” koji pružaju sve pogodnosti koje pruža i dinamička alokacija
memorije (što nije nikakvo iznenađenje, s obzirom da je njihova implementacija zasnovana upravo na
dinamičkoj alokaciji memorije). S obzirom da se ovi tipovi kopiraju i pomjeraju bez problema (zahvaljujući
njihovim vlastitim kopirajućim konstruktorima i srodnim elementima), korisnik se ne mora brinuti o
ispravnom kopiranju i pomjreranju. Na primjer, pogledajmo kako bismo mogli realizirati klasu “VektorNd”
koristeći tip “vector” umjesto dinamičke 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, kopirajući i pomjerajući konstruktor, te operatori dodjele više nisu
potrebni. Obratimo pažnju kako je konstruktor klase “VektorNd” iskorišten za inicijalizaciju broja
elemenata atributa “koordinate” koji je tipa “vector<double>”, te kako sada izgleda odgovarajući
sekvencijski konstruktor. Metoda “PromijeniDimenziju” je morala također biti promjenjena, dok
implementacije privatne metode “TestIndeksa”, metoda “PostaviKoordinatu”, “DajKoordinatu” i “Ispisi”,
te prijateljske funkcije “ZbirVektora” mogu ostati iste kao i do sada. U suštini, sa ovakvim izmjenama,
čak i atribut “dimenzija” postaje suvišan, jer je njegova vrijednost uvijek jednaka veličini atributa
“koordinate”, koja se može dobiti pozivom funkcije “size”. Naravno, uklonimo li atribut “dimenzija”,
moramo načiniti i odgovarajuće 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 dosada uopće patili sa dinamičkom alokacijom memorije,
destruktorima, kopirajućim i pomjerajućim konstruktorima te preklapanjem operatora dodjele kada
problem možemo jednostavnije riješiti 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 ponašaju poput njih. Pored toga, korištenje tipa
“vector” samo sa ciljem izbjegavanja dinamičke alokacije memorije i pratećih rekvizita (kopirajućih
konstruktora, itd.) jeste najlakši, ali ne i najefikasniji način za rješavanje problema. Naime, deklariranjem
nekog atributa tipa “vector”, u klasu koju razvijamo ugrađujemo sva svojstva klase “vector”, uključujući
i svojstva koja vjerovatno nećemo uopće koristiti (isto vrijedi i za upotrebu atributa tipa “ string”). Na
taj način, klasa koju razvijamo postaje opterećena suvišnim detaljima, što dovodi do gubitka efikasnosti
i bespotrebnog trošenja računarskih resursa.

16

You might also like