You are on page 1of 16

Dr.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

Predavanje 11_b
Vidjeli smo da definiranje destruktora, kopirajuih i pomjerajuih konstruktora, te odgovarajuih
operatora dodjele omoguava da lokaliziramo upravljanje resursima, odnosno da svo upravljanje
zauzimanjem i oslobaanjem resursa vrimo na tano odreenim mjestima. Zajedno sa injenicom da se
izvjesni elementi klase kao to su konstruktori i destruktori automatski pozivaju u tano odreenim
prilikama ini upravljanje resursima mnogo jednostavnijim. Recimo, tipovi podataka poput vector i
string, sa kojima smo se do sada u vie navrata susretali (kao uostalom i gotovo svi kontejnerski
tipovi podataka u standardnim bibliotekama), implementirani su upravo kao klase (tanije, kao generike
klase o kojima emo kasnije jo govoriti) koje interno koriste dinamiku alokaciju memorije, i koje
posjeduju propisno izvedene kopirajue i pomjerajue konstruktore, te odgovarajue operatore dodjele.
Zahvaljujui tome, njih je mogue bezbjedno prenositi kao parametre po vrijednosti, vraati kao
rezultate iz funkcije, i vriti njihovo meusobno dodjeljivanje. U sutini, objekti tipa vector i
string i slini, uope u sebi (tj. u svojim atribtima) ne sadre svoje elemente. Primjerci ovih
tipova unutar sebe samo sadre pokazivae na blokove memorije u kojem se nalaze njihovi elementi i
jo pokoji atribut neophodan za njihov ispravan rad. Meutim, zahvaljujui kopirajuim konstruktorima i
prateim rekvizitima, korisnik ne moe primijetiti ovu injenicu, s obzirom da pripadni elementi prate u
stopu svako kretanje samog objekta, odnosno ponaaju se kao prikolica trajno zakaena na objekat.
Jedini nain kojim se korisnik moe uvjeriti u ovu injenicu je primjena operatora sizeof, kojeg je
nemogue prevariti. Naime, ovaj operator primijenjen na ma kakav objekat (ukljuujui i objekte tipa
vector ili string) iji je tip struktura ili klasa dae kao rezultat ukupan broj bajtova koji
zauzimaju atributi strukture odnosno klase, bez obzira na to ta je eventualno prikaeno na objekte.
Stoga e rezultat primjene sizeof operatora na objekte tipa vector ili string biti uvijek isti,
bez obzira na broj elemenata koje sadri vektor ili dinamiki string.
Definiranje kopirajueg konstruktora i kopirajueg operatora dodjele koji realiziraju duboko
kopiranje nije niti jedini niti najefikasniji nain za rjeavanje problema interakcije izmeu destruktora i
plitkih kopija (tj. neeljenog brisanja blokova memorije na koje istovremeno pokazuje vie pokazivaa, a
koji nastaju usljed plitkih kopija). Naime, nedostatak ovog rjeenja je preesto kopiranje ponekad i
prilino velikih blokova memorije. Mada je uvoenjem pomjerajuih konstruktora i operatora dodjele u
C++11 ovaj problem drastino ublaen, s obzirom da se pokazuje da se najee javlja potreba za
kopiranjem upravo privremenih objekata, a ije se kopiranje uz pomo pomjerajuih konstruktora i
operatora dodjele moe u potpunosti eliminirati, ostaje injenica da se kopiranje imenovanih objekata ne
moe eliminirati. S druge strane, nekada smo radi dobitka efikasnosti spremni prihvatiti da radimo sa
plitkim kopijama, ali smo ve vidjeli da se destruktori i plitke kopije ne vole. Razumije se da su
destruktori isuvie vani da bi od njih lako odustali. Stoga se postavlja pitanje da li je nekako mogue
izmiriti destruktore i plitke kopije.
Rjeenje za pomirenje destruktora i plitkih kopija zasniva se na strategiji poznatoj pod nazivom
brojanje pristupa odnosno brojanje referenciranja (engl. reference counting). Ova tehnika se koristi
recimo u pametnim pokazivaima, koji su realizirani upravo kao klase koje koriste tu tehniku. Ideja je da
se za svaki resurs ili eventualno skupinu resursa (zavisno od konkretne situacije) uvede neki broja koji
broji koliko primjeraka neke klase sadri pristupnike (recimo pokazivae) kojima se pristupa istom
resursu (recimo istom dinamiki alociranom bloku memorije). Konstruktor e ovaj broja postaviti na 1,
a kopirajui konstruktor e i dalje obavljati kopiranje samo atributa klase (tj. plitko kopiranje), ali e pri
tome uveavati pomenuti broja za 1. Kako pri tome efektivno ne dolazi do kopiranja pridruenih
resursa, pomjerajui konstruktor postaje nepotreban, odnosno njegovim uvoenjem nita ne dobijamo na
efikasnosti. Oigledno je ovakva strategija mnogo efikasnija od bezuvjetnog kopiranja, ali je cijena koja
je tim plaena prihvatanje plitkih kopija. to se tie destruktora, on e pridrueni broja smanjivati za 1,
ali e vriti oslobaanje nekog resursa samo samo u sluaju da njemu pridrueni broja dostigne nulu,
to znai da vie nema ni jednog objekta koji koristi taj resurs. U veini sluajeva dovoljno je imati jedan
broja za sve resurse. Vie brojaa je potrebno recimo ukoliko primjerci klase tokom svog ivota mogu
zaduivati odnosno razduivati resurse (tj. kada se moe desiti da nisu svi resursi istovremeno zadueni).
Kljuno je pitanje gdje uvati brojae koji broje pristupe resursima. Ti brojai ne mogu biti obini
atributi klase, s obzirom da njih trebaju zajedniki dijeliti sve kopije nekog objekta. Oni takoer ne mogu
biti statiki atributi klase, s obzirom da oni trebaju da budu zajedniki samo za one primjerke klase koji
1

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

su kopija jedan drugog, ali ne i za ostale primjerke kleuase. Jedno mogue rjeenje da ti brojai budu
dinamike promjenljive (koje se, prema tome, nalaze izvan same klase) a da klasa kao atribut sadri
pokazivae na nih, preko kojih im se moe pristupiti (alternativno rjeenje je da se ti brojai nalaze
unutar samih resursa kojima su pridrueni, a da li je to mogue, zavisi od vrste resursa). Ovo je inae
jedna od rijetkih primjena u kojima treba dinamiki alocirati promjenljive jednostavnih tipova kao to je
tip int. Njihovo kreiranje e izvriti konstruktor, a unitavanje destruktor, onog trenutka kada vie
nisu potrebni. Kao ilustraciju ove tehnike, izmijenimo deklaraciju prethodno razvijene klase
VektorNd na sljedei nain:
class VektorNd {
int dimenzija;
double *koordinate;
int *pnb;
// Pokaziva na broja
void TestIndeksa(int indeks) {
if(indeks < 1 || indeks > dimenzija)
throw std::range_error("Pogrean indeks!");
}
public:
... // Slijedi ostatak, uz izmjene navedene u tekstu...
};

Unutar klase smo uveli novi atribut pnb (skraenica od pokaziva na broja), koji predstavlja
pokaziva na broja identinih kopija. Deklaracije konstruktora, destruktora, kopirajueg konstruktora i
kopirajueg operatora dodjele emo zadrati (samo emo im promijeniti implementacije), dok emo radi
jednostavnosti izbaciti pomjerajui konstruktor i operator dodjele, s obzirom da u ovom sluaju oni nee
biti prijeko potrebni (kako e destruktor postati neto sloeniji, implementiraemo ga izvan deklaracije
klase). Metodu PromijeniDimenziju emo posve izbaciti jer se konceptualno ne uklapa u ono o
emu emo ovdje priati (uskoro emo shvatiti i zato). Ostale metode (PostaviKoordinatu,
DajKoordinatu i Zbir) ostaju iste kao to su bile i ranije, a isto vrijedi i za prijateljsku funkciju
ZbirVektora.
Osnovni konstruktor sa jednim parametrom kao i sekvencijski konstruktor ostaju praktino isti kao
to su bili ranije, osim to se jo kreira dinamika promjenljiva koja predstavlja broja inicijaliziran na
jedinicu (pomou konstrukcije new int(1)), ija se adresa pridruuje pokazivau pnb:
VektorNd::VektorNd(int dimenzija) : dimenzija(dimenzija),
koordinate(new double[dimenzija]), pnb(new int(1)) {
std::fill(koordinate, koordinate + dimenzija, 0);
}
VektorNd::VektorNd(std::initializer_list<double> lista) :
dimenzija(lista.size()), koordinate(new double[lista.size()]),
pnb(new int(1)) {
std::copy(lista.begin(), lista.end(), koordinate);
}

Konstruktor kopije prosto kopira sve atribute klase (isto to bi uradio i podrazumijevani konstruktor
kopije) i poveava broja kopija za jedinicu:
VektorNd:: VektorNd (const VektorNd &v) : dimenzija(v.dimenzija),
koordinate(v.koordinate), pnb(v.pnb) { (*pnb)++; }

Destruktor umanjuje broja za 1, i vri oslobaanje resursa samo ukoliko je broja dostigao nulu (istom
prilikom se unitava i sam broja, s obzirom da vie nije potreban):
VektorNd::~VektorNd() {
if(--(*pnb) == 0) {
delete[] koordinate; delete pnb;
}
}

Situacija koja sada nastaje prilikom kopiranja objekata tipa VektorNd moe se prikazati
sljedeom slikom:
2

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

dimenzija koordinate pnb

dimenzija koordinate pnb

2
Ostaje jo implementacija preklopljenog operatora dodjele, koji je neto sloeniji. On treba umanjiti
za 1 broja pridruen resursima objekta sa lijeve strane operatora dodjele (s obzirom da on sada vie
nee koristiti resurse koje je do tada koristio) i izvriti oslobaanje resursa (tj. dealokaciju memorije) u
sluaju da je broja dostigao nulu. Broja pristupa pridruen resursima objekta sa desne strane treba
uveati za 1 (s obzirom da e iste resurse sada koristiti i objekat sa lijeve strane operatora dodjele) i
nakon toga izvriti plitko kopiranje. Ukoliko prvo izvrimo uveanje ovog brojaa, pa tek onda umanjenje
brojaa pridruenog objektu sa lijeve strane (uz eventualnu dealokaciju memorije), izbjei emo i
probleme usljed eventualne samododjele, tako da taj sluaj ne moramo posebno razmatrati:
VektorNd &VektorNd::operator =(const VektorNd &v) {
(*v.pnb)++;
if(--(*pnb) == 0) {
delete[] koordinate; delete pnb;
}
dimenzija = v.dimenzija; koordinate = v.koordinate; pnb = v.pnb;
return *this;
}

to se tie pomjerajueg konstruktora i pomjerajueg operatora dodjele, njih takoer ne bi bilo osobito
teko napraviti (mada je sada malo tee obezbijediti kako da destruktor objekta kojem su ukradeni
resursi ne napravi nikakvu tetu), ali za njima ovdje nema prevelike potrebe, s obzirom da je postupak
kopiranja vrlo jeftin (od minimalistikog kopiranja atributa razlikuje se samo u auriranju brojaa).
Opisana tehnika brojanja referenciranja je prilino jednostavna i vrlo efikasna. Meutim, u praksi je
situacija neto sloenija. Naime, kako se i dalje koriste plitke kopije, bilo koja modifikacija resursa
pridruenih jednom objektu (npr. elemenata dinamiki alociranog niza) mijenja i resurse pridruene
drugom objektu (jer se, zapravo, radi o istim resursima). Ovo nije prevelik problem ukoliko smo svjesni
da radimo sa plitkim kopijama, ali kao to smo ve ranije istakli, moe djelovati kontraintuitivno (osim
ukoliko klasa koju pravimo nije zamiljena da glumi pokaziva, to je recimo sluaj sa pametnim
pokazivaima). Interesantno je da postoji i veoma efikasan, ali neto sloeniji nain rjeavanja ovog
problema. Osnovna ideja je u tome da korisnik klase ne moe vidjeti razliku izmeu plitke i duboke
kopije sve dok ne pokua na neki nain da modificira kopiju (ukoliko se radi o plitkoj kopiji, to e se
nuno odraziti i na original).Stoga su plitke kopije savrene za potrebe kopiranja objekata koji nemaju
metode koji vre izmjenu resursa pridruenih objektu. ak i ako postoje takve metode, plitko kopiranje
je sasvim dobro sve dok se ne pojavi potreba za modificiranjem pridruenih resursa (npr. izmjenom
elemenata dinamiki alociranog niza pridruenog objektu). Stoga je mogue duboko kopiranje obavljati
samo u metodama koje vre neku izmjenu prirduenih resursa, i to samo u sluajevima kada broja
pristupa resursima ima vrijednost veu od 1 (to je siguran znak da jo neki objekat koristi iste resurse,
npr. jo neki pokaziva u nekom drugom objektu pokazuje na isti dinamiki alocirani niz). Opisana
tehnika naziva se kopiranje pri upisu (engl. copy on write ili, skraeno, COW). Ona se relativno
jednostavno realizira u sluaju kada postoje jasno razdvojene metode koje samo itaju i koje
modificiraju pripadne resurse (mada se situacija moe prilino zakomplicirati kada primjerci klase
koriste vie resursa kojima su pridrueni razliiti brojai). S druge strane, efikasna realizacija ove
tehnike moe postati iznimno komplicirana u sluajevima kada postoje metode koje se, zavisno od
situacije, mogu koristiti kako za itanje, tako i za modificiranje elemenata dinamikog niza (poput
verzije metode DajKoordinatu iz klase VektorNd koja vraa referencu kao rezultat). Stoga, u
detalje ove tehnike neemo ulaziti. Cilj je bio samo da se ukae na osnovne ideje kako se rad sa klasama
koje zauzimaju mnogo memorijskih resursa moe uiniti efikasnijim. Ako zanemarimo aspekt
efikasnosti (koji se, kao to smo vidjeli, drastino poboljava uvoenjem pomjerajuih konstruktora i
operatora dodjele) , moemo rei da postupak kreiranja dubokih kopija koji smo detaljno objasnili u
potpunosti zadovoljava sve druge aspekte za ispravno koritenje klasa.

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

Potrebno je jo objasniti zbog ega je bilo potrebno izbaciti metodu PromijeniDimenziju.


Poenta je da se ta metoda sutinski ne uklapa u koncept plitkih kopija. Na primjer, pretpostavimo da
imamo objekat a tipa VektorNd koji smo kopirali u objekat b koji je sada plitka kopija objekta
a. Pretpostavimo sada da smo nad objektom b pozvali metodu PromijeniDimenziju sa ciljem
da mu poveamo dimenziju. Jasno je da e se u objektu b morati izvriti realokacija, ali ta e se desiti
sa objektom a? Zapravo, zapitajmo se prvo ta bi trebalo da se desi sa tim objektom? Ukoliko
doslovno prihvatimo ideju plitkih kopija, ispada da bi se ista promjena dimenzije trebala da odrazi i na
objekat a. Meutim, to je nemogue izvesti, jer metoda PromijeniDimenziju primijenjena nad
objektom b nema naina da pristupi atributu dimenzija objekta a da ga promijeni, niti da pristupi
njegovom atributu koordinate da ga preusmjeri na novoalocirani niz. Zapravo, kad se pozove nad
objektom b, ta metoda nema pojma da objekat a uope postoji, a pogotovo da je objekat b
njegova plitka kopija. Jedino to bismo mogli uraditi je pustiti da nakon poziva ove metode objekti a i
b koriste razliite dinamiki alocirane nizove i da imaju razliite dimenzije, ali to ve vie nee biti
plitka kopija (da bi sve ovo funkcioniralo kako treba, trebalo bi jo i razdvojiti brojae koji su vezani uz
ova dva alocirana niza). Ovo ve spada u vrstu COW tehnika, a ne u klasino plitko kopiranje (dakle,
klasino plitko kopiranje je nemogue u prisustvu ove metode). To samo po sebi ne bi bio problem, ali
klasa bi se tada ponaala nekonzistentno, s obzirom da slinu tehniku ne bi koristila recimo metoda
PostaviKoordinatu. Dakle, COW tehniku bi trebalo koristiti ili dosljedno, ili nikako. Slijedi da je
mogue da postojanje nekih metoda klasu uini nepodobnom za koritenje plitkog kopiranja.
Sada emo ilustrirati razvoj jedne malo sloenije kontejnerske klase kroz program koji obavlja istu
funkciju kao program za obradu rezultata uenika u razredu koji smo prikazali na nekom od ranijih
predavanja, samo to je napisan u duhu objektno zasnovanog programiranja. Pri tome, za poetak
neemo koristiti kontejnerske tipove podataka definirane u standardnim bibliotekama kao to su
vector itd. Program je prilino dugaak i relativno sloen, pa e nakon njegovog prikaza uslijediti
detaljna analiza njegovih pojedinih dijelova. Bez obzira to je ovako napisan program skoro dvostruko
dui od slinog programa koji je koristio samo strukture i bio napisan u isto proceduralnom duhu,
uloeni trud se, dugorono gledano, svakako isplati, s obzirom da je on mnogo pogodniji za eventualna
proirenja koja se mogu lako realizirati proirujui razvijene klase novim metodama, pri emu od velike
pomoi mogu biti metode koje su do tada razvijene. Pored toga, ovaj program ima strogi sistem zatite
od unosa besmislenih podataka, to nije bio sluaj sa izvornim programom:
#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <stdexcept>
using namespace std;
class Datum {
int dan, mjesec, godina;
public:
Datum(int d, int m, int g);
void Ispisi() const {
std::cout << dan << "/" << mjesec << "/" << godina;
}
};
class Ucenik {
static const int BrojPredmeta = 10;
// Pri testiranju smanjite
char ime[20], prezime[20];
//
broj predmeta...
Datum datum_rodjenja;
int ocjene[BrojPredmeta];
double prosjek;
bool prolaz;
public:
Ucenik(const char ime[], const char prezime[], int d, int m, int g);
void PostaviOcjenu(int predmet, int ocjena);
static int DajBrojPredmeta() { return BrojPredmeta; }
double DajProsjek() const { return prosjek; }
bool DaLiJeProsao() const { return prolaz; }
void Ispisi() const;
};

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

class Razred {
const int kapacitet;
int broj_evidentiranih;
Ucenik **ucenici;
static bool BoljiProsjek(const Ucenik *u1, const Ucenik *u2) {
return u1->DajProsjek() > u2->DajProsjek();
}
public:
explicit Razred(int broj_ucenika) : kapacitet(broj_ucenika),
broj_evidentiranih(0), ucenici(new Ucenik*[broj_ucenika]) {}
~Razred();
Razred(const Razred &r) = delete;
// Zabrana kopiranja
Razred &operator =(const Razred &r) = delete;
// Zabrana dodjele
void EvidentirajUcenika(Ucenik *ucenik);
void UnesiNovogUcenika();
void IspisiIzvjestaj() const;
void SortirajUcenike() {
std::sort(ucenici, ucenici + broj_evidentiranih, BoljiProsjek);
}
};
int main() {
try {
int broj_ucenika;
std::cout << "Koliko ima ucenika: ";
std::cin >> broj_ucenika;
if(!std::cin) throw "";
// Ovdje je nebitno ta bacamo...
Razred razred(broj_ucenika);
for(int i = 1; i <= broj_ucenika; i++) {
std::cout << "Unesi podatke za " << i << ". ucenika:\n";
razred.UnesiNovogUcenika();
}
razred.SortirajUcenike();
razred.IspisiIzvjestaj();
}
catch(...) {
std::cout << "Problemi sa memorijom!\n";
}
return 0;
}
Datum::Datum(int d, int m, int g) {
int broj_dana[12]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if(g % 4 == 0 && g % 100 != 0 || g % 400 == 0) broj_dana[1]++;
if(g < 1 || d < 1 || m < 1 || m > 12 || d > broj_dana[m - 1])
throw std::domain_error("Neispravan datum!");
dan = d; mjesec = m; godina = g;
}
Ucenik::Ucenik(const char ime[], const char prezime[], int d, int m,
int g) : datum_rodjenja(d, m, g), prosjek(1), prolaz(false) {
if(std::strlen(ime) > 19 || std::strlen(prezime) > 19)
throw std::domain_error("Predugacko ime ili prezime!");
std::strcpy(Ucenik::ime, ime);
std::strcpy(Ucenik::prezime, prezime);
for(int &ocjena : ocjene) ocjena = 1;
}
void Ucenik::PostaviOcjenu(int predmet, int ocjena) {
if(ocjena < 1 || ocjena > 5)
throw std::domain_error("Pogresna ocjena!");
if(predmet < 1 || predmet > BrojPredmeta)
throw std::domain_error("Pogresna sifra predmeta!");
ocjene[predmet - 1] = ocjena;
prosjek = 1; prolaz= false;
double suma_ocjena(0);

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

for(int ocjena : ocjene) {


if(ocjena == 1) return;
suma_ocjena += ocjena;
}
prosjek = suma_ocjena / BrojPredmeta; prolaz = true;
}
void Ucenik::Ispisi() const {
std::cout << "Ucenik " << ime << " " << prezime << " rodjen ";
datum_rodjenja.Ispisi();
if(DaLiJeProsao())
std::cout << " ima prosjek "
<< std::setprecision(3) << DajProsjek() << std::endl;
else
std::cout << " mora ponavljati razred\n";
}
Razred::~Razred() {
for(int i = 0; i < broj_evidentiranih; i++) delete ucenici[i];
delete[] ucenici;
}
void Razred::EvidentirajUcenika(Ucenik *ucenik) {
if(broj_evidentiranih >= kapacitet)
throw std::range_error("Previse ucenika!");
ucenici[broj_evidentiranih++] = ucenik;
}
void Razred::UnesiNovogUcenika() {
bool pogresan_unos(true);
while(pogresan_unos) {
Ucenik *ucenik(nullptr);
try {
char ime[20], prezime[20];
int d, m, g;
char znak1, znak2;
std::cout << " Ime: "; std::cin >> std::setw(20) >> ime;
std::cout << " Prezime: "; std::cin >> std::setw(20) >> prezime;
std::cout << " Datum rodjenja (D/M/G): ";
std::cin >> d >> znak1 >> m >> znak2 >> g;
if(!std::cin || znak1 != '/' || znak2 != '/')
throw std::domain_error("Pogresan datum!");
ucenik = new Ucenik(ime, prezime, d, m, g);
for(int predmet = 1; predmet <= Ucenik::DajBrojPredmeta();
predmet++) {
int ocjena;
std::cout << " Ocjena iz " << predmet << ". predmeta: ";
std::cin >> ocjena;
if(!std::cin) throw std::domain_error("Pogresna ocjena!");
ucenik->PostaviOcjenu(predmet, ocjena);
}
EvidentirajUcenika(ucenik);
pogresan_unos = false;
}
catch(std::domain_error greska) {
cout << "Greska: " << greska.what()
<< "\nMolimo, ponovite unos!\n";
std::cin.clear();std::cin.ignore(10000, '\n');
delete ucenik; ucenik = nullptr;
}
}
}
void Razred::IspisiIzvjestaj() const {
std::cout << std::endl;
for(int i = 0; i < broj_evidentiranih; i++) ucenici[i]->Ispisi();
}

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

U ovom programu su definirane tri klase: Datum, Ucenik i Razred. O klasi Datum nema
se nita vie rei nego to je do sada ve reeno. Klasa Ucenik deklarira konstantni statiki atribut
BrojPredmeta te obine atribute ime, prezime, datum rodjenja, ocjene, prosjek i
prolaz, pri emu atribut ocjene predstavlja klasini niz iji je broj elemenata odreen
konstantnim statikim atributom BrojPredmeta. U interfejsu klase Ucenik nalazi se konstruktor
koji inicijalizira atribute ime, prezime i datum rodjenja u skladu sa parametrima koji su mu
proslijeeni, zatim sve ocjene inicijalizira na 1 (tako da se smatra da uenik nije zadovoljio predmet sve
dok ne dobije pozitivnu ocjenu iz njega), dok atribute prosjek i prolaz inicijalizira respektivno
na vrijednosti 1 i false (atribut prolaz sadri vrijednost true ukoliko je uenik proao a
false ako nije). Dalje su deklarirane i implementirane trivijalne metode DajProsjek i
DaLiJeProsao koje vraaju respektivno vrijednosti atributa prosjek i prolaz, zatim statika
metoda DajBrojPredmeta koja vraa vrijednost statikog atributa BrojPredmeta (ova metoda je
statika jer je nebitno nad kojim se konkretno primjerkom klase Ucenik poziva), kao i metoda
Ispisi koja ispisuje podatke o ueniku. Pored toga, imamo i metodu PostaviOcjenu sa dva
parametra koji predstavljaju broj predmeta i ocjenu. Ova metoda vri upis odgovarajue ocjene, vodei
rauna da se ne zada pogrean broj predmeta ili ocjena izvan opsega od 1 do 5 (u suprotnom se baca
izuzetak), a zatim aurira atribute prosjek i prolaz u skladu sa novonastalom situacijom.
Interesantno je napomenuti da su se atributi prosjek i prolaz mogli izostaviti, pri emu bi se
tada metode DajProsjek i DaLiJeProsao trebale izmijeniti tako da prilikom poziva raunaju
prosjek i indikator prolaznosti, umjesto da prosto vrate vrijednosti odgovarajuih atributa. Naravno,
metoda PostaviOcjenu tada ne bi trebala raunati prosjek i indikator prolaznosti. Korisnik tako
izmijenjene klase Ucenik ne bi primijetio nikakvu izmjenu. Ovaj primjer ilustrira nain na koji
interfejs klase sakriva njenu internu implementaciju od korisnika klase. Ipak, ovakva implementacija je
neto efikasnija, jer se prosjek i indikator prolaznosti raunaju samo prilikom upisa nove ocjene, a uz
izmijenjenu implementaciju, oni bi se iznova raunali pri svakom pozivu metode DajProsjek
odnosno DaLiJeProsao. Ovo poboljanje naroito dolazi do izraaja prilikom sortiranja spiska
uenika, u kojem se veoma esto poziva metoda DajProsjek.
Klasa Razred je neto sloenija, i nju emo razmotriti malo detaljnije. Njeni atributi su dva
cjelobrojna polja kapacitet i broj evidentiranih, kao i dvojni pokaziva ucenici.
Konstantni atribut kapacitet predstavlja kapacitet razreda, odnosno maksimalni broj uenika koji
razred moe primiti, dok atribut broj evidentiranih predstavlja broj uenika koji su zaista
evidentirani u razredu. Preko pokazivaa ucenici pristupa se dinamiki alociranom nizu koji
predstavlja niz pokazivaa na uenike (zbog toga je pokaziva ucenici dvojni pokaziva). Niz
pokazivaa na uenike se koristi umjesto niza uenika da se izbjegnu problemi o kojima smo govorili u
vezi sa nizovima iji su elementi primjerci neke klase i konstruktorima. Konstruktor klase Razred
postavlja atribut kapacitet na vrijednost zadanu parametrom, atribut broj evidentiranih
postavlja na nulu, te obavlja dinamiku alokaciju niza preko pokazivaa ucenici u skladu sa
eljenim kapacitetom razreda. Predvien je i destruktor, dok su kopirajui konstruktor i kopirajui
operator dodjele samo deklarirani, a umjesto njihove implementacije stoji konstrukcija = delete.
Smisao ove konstrukcije objasniemo kasnije.
Interfejs klase Razred dalje sadri metode EvidentirajUcenika, UnesiNovogUcenika,
IspisiIzvjestaj i SortirajUcenike. Metoda EvidentirajUcenika prima kao parametar
pokaziva na objekat tipa Ucenik, smjeta ga u dinamiki niz pokazivaa na evidentirane uenike
(preko pokazivaa ucenici) i poveava broj evidentiranih uenika za 1. Pri tome se baca izuzetak
ukoliko je razred eventualno ve popunjen. Metoda UnesiNovogUcenika trai da se sa tastature
unesu osnovni podaci o jednom ueniku (ime, prezime i datum roenja), kreira dinamiki novi objekat
tipa Ucenik koji inicijalizira unesenim podacima, trai da se unesu ocjene za uenika iz svih
predmeta, upisuje unesene ocjene u kreirani objekat (pozivom metode PostaviOcjenu) i, konano,
evidentira uenika (tj. upisuje pokaziva na njega u dinamiki kreirani niz pokazivaa na uenike)
pozivom metode EvidentirajUcenika. Pri tome se hvataju svi izuzeci koji bi eventualno mogli biti
baeni (npr. zbog pogreno unesenog datuma), a unos se ponavlja sve dok se ne unesu ispravni podaci. U
sluaju da ulazni tok dospije u neispravno stanje zbog besmislenog unosa vrimo runo bacanje izuzetka,
tako da sve tipove neispravnog unosa obraujemo na jednoobrazan nain. Metoda IspisiIzvjestaj
ispisuje izvjetaj o svim upisanim uenicima u razredu prostim pozivom metode Ispisi nad svakim

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

od upisanih uenika. Primijetimo da se metode PostaviOcjenu i Ispisi pozivaju indirektno (tj.


pomou operatora ->) jer se primjenjuju na pokaziva a ne na sam objekat.
Metoda SortirajUcenike je krajnje jednostavna, s obzirom da se njena implementacija sastoji
samo od poziva funkcije sort iz biblioteke algorithm. Meutim, za tu potrebu je potrebno
definirati funkciju koja definira kriterij po kojem se vri sortiranje. Najpraktinije je za tu svrhu
iskoristiti lambda funkciju. Meutim, umjesto toga, ovdje smo za tu svrhu u privatnoj sekciji klase
Razred definirali statiku funkciju lanicu BoljiProsjek. Treba naglasiti da je ova funkcija
lanica morala biti deklarirana kao statika funkcija lanica. U suprotnom, ona ne bi mogla biti prosto
proslijeena funkciji sort kao parametar, jer se nestatike funkcije lanice moraju pozivati nad nekim
konkretnim objektom, a nad kojim, to funkcija sort zaista ne moe saznati. Generalno, bilo kojoj
funkciji iji je formalni parametar funkcija (odnosno pokaziva na funkciju) moemo kao stvarni
parametar proslijediti obinu funkciju ili statiku funkciju lanicu (pod uvjetom da su im broj i tip
parametara kao i tip povratne vrijednosti odgovarajui), ali ne i nestatiku funkciju lanicu. Ovo je jedan
od primjera u kojima se ne mogu koristiti nestatike funkcije lanice. Naravno, kao alternativu, funkciju
kriterija smo mogli definirati i kao obinu funkciju, ali je ovakvo rjeenje bolje, s obzirom da se ona ne
treba koristiti nigdje osim unutar metode SortirajUcenike kao parametar funkcije sort.
Destruktor klase Razred je posebno interesantan. On svakako brie dinamiki alociran niz
pokazivaa na uenike kojem se pristupa preko pokazivaca ucenici i koji je kreiran unutar
konstruktora, ali prije toga brie i sve dinamiki kreirane uenike na koje pokazuju elementi ovog niza,
bez obzira to oni nisu kreirani unutar konstruktora, nego na nekom drugom mjestu (preciznije, u metodi
UnesiNovogUcenika). Bez obzira na mjesto njihovog kreiranja, nakon to se pozove metoda
EvidentirajUcenika, objekat klase Razred nad kojim je metoda pozvana u izvjesnom smislu
zna za njih, i u stanju je da ih i obrie. Ovaj primjer ilustrira da se destruktoru moe povjeriti i
generalno ienje svih resursa koji su na bilo koji nain vezani za objekat koji se unitava, a ne nuno
samo resursa zauzetih unutar konstruktora klase. Kaemo da je klasa Razred vlasnik (engl. owner)
svih svojih uenika, odnosno klasa Razred posjeduje objekte tipa Ucenik. Dakle, smatra se da se
nakon poziva metode EvidentirajUcenika objekat na koji pokazuje njen parametar predaje u
vlasnitvo objektu klase Razred. Bilo koja klasa koja je vlasnik objekata neke druge klase, preuzima
brigu za njih i odgovorna je i za njihovo unitavanje (recimo, pametni pokazivai su vlasnici objekata na
koji pokazuju, za razliku od obinih pokazivaa). O tome da li nekoj klasi treba prepustiti vlasnitvo nad
drugim objektima ili objekte treba pustiti da se brinu sami o sebi (ukljuujui i problematiku ko e i kada
obrisati te objekte), postoje brojne diskusije. Generalnog odgovora na ovo pitanje nema i preporuena
strategija zavisi od sluaja do sluaja. Uglavnom, za koju god strategiju se odluimo, ukoliko ne pazimo
dobro ta radimo i ukoliko nismo dosljedni u primjeni izabrane strategije, postoji velika mogunost da
stvorimo visee pokazivae (ovo se obino deava ukoliko u jednom dijelu programa doe do
unitavanja nekog objekta za koji se u drugom dijelu programa podrazumijeva da e i dalje postojati).
Moe se rei da je u objektno orijentiranom programiranju problem vlasnitva veoma teko pitanje (kao,
uostalom, i u stvarnom ivotu).
Osvrnimo se malo i na glavni program (funkciju main). Nakon to smo praktino sve poslove
povjerili klasama, glavni program postaje trivijalan. U njemu se, nakon to se sa tastature unese eljeni
broj uenika u razredu (pri emu se baca izuzetak ukoliko unesemo besmislice), prvo deklarira jedna
konkretna instanca klase Razred sa traenim kapacitetom, a zatim se nad ovom instancom u petlji
poziva metoda UnesiNovogUcenika sve dok se ne unesu svi uenici. Nakon toga se pozivom
metode SortirajUcenike svi uenici sortiraju u opadajui redoslijed po prosjeku, i na kraju se
pozivom metode IspisiIzvjestaj ispisuje traeni izvjetaj. Sve ovo je uklopljeno u try blok
koji hvata eventualne izuzetke koji mogu biti baeni sa raznih mjesta ukoliko neka od dinamikih
alokacija memorije ne uspije.
Razmotrimo sada ta se deava sa konstruktorom kopije i kopirajuim operatorom dodjele. Jasno je
da klasi Razred treba destruktor, jer ne samo da alocira dodatne memorijske resurse koje koristi, nego
preuzima u svoje vlasnitvo i druge dinamiki alocirane objekte. Meutim, prema zakonu velike trojke,
svaka klasa koja ima destruktor, po pravilu bi morala imati i kopirajui konstruktor, te kopirajui
operator dodjele. Naelno, ukoliko smo posve sigurni da primjerci neke klase nee biti prenoeni po
vrijednosti kao parametri u funkcije, nee biti vraani kao rezultati iz funkcije, i nee biti koriteni za

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

inicijalizaciju drugih objekata iste klase, mogli bismo izbjei definiranje vlastitog kopirajueg
konstruktora. Kompajler bi u tom sluaju automatski generirao podrazumijevani kopirajui konstruktor,
koji ne bi bio adekvatan, ali ga svakako ne bismo ni koristli. Slina stvar vrijedi i za vlastiti kopirajui
operator dodjele, ukoliko smo sigurni da se nikada objekti te klase nee dodjeljivati jedan drugom.
Meutim, prosto nepisanje vlastitog kopirajueg konstruktora i operatora dodjele nije osobito dobra
ideja. Naime, ukoliko piemo klasu za kasnije koritenje koja e se moi upotrebljavati i u drugim
programima (pogotovo onim koje nije pisao isti autor kao i autor klase), ne moemo unaprijed znati ta
e korisnik klase sa njom raditi, a svaki pokuaj kopiranja primjeraka klase Razred pomou
podrazumijevanog kopirajueg konstruktora kao i pokuaj dodjele pomou podrazumijevanog
kopirajueg operatora dodjele izazvali bi nevolje. Zbog toga, ukoliko ve ne elimo da pravimo vlastiti
konstruktor kopije zbog toga to smatramo da kopiranje primjeraka neke klase nje potrebno, tada je bolje
kopiranje te klase potpuno zabraniti. To se moe postii time to emo ukloniti podrazumijevani
konstruktor kopije koji kompajler automatski generira. Upravo to se postie konstrukcijom = delete
umjesto implementacije (ova konstrukcija ima smisla samo za automatski generirane elemente klase).
Nakon to obriemo konstruktor kopije, kompajler nema vie ta pozivati prilikom kopiranja primjeraka
te klase, te e svaki pokuaj kopiranja dovesti do prijave greke od strane kompajlera. Slina pria vrijedi i
za automatski generirani podrazumijevani kopirajui operator dodjele. Stoga smo u implementaciji ove
klase takoer uklonili podrazumijevani kopirajui operator dodjele generiran od strane kompajlera, ime
smo efektivno zabranili meusobno dodjeljivanje primjeraka klase Razred.
Treba napomenuti da je mogunost uklanjanja automatski kreiranih elemenata klase pomou
konstrukcije = delete uvedena tek u C++11. Ranije to nije mogue, te se slian efekat postizao
raznim trikovima (koji se i danas mogu vidjeti u starijim kdovima). Najjednostavniji takav trik je
formalno deklariranje prototipa kopirajueg konstruktora i kopirajueg operatora dodjele unutar privatne
sekcije klase, ali bez njihovog implementiranja (u sluaju da ih implementiramo, kopiranje i dodjela e
ipak biti mogui, ali samo iz funkcija lanica klase i prijateljskih funkcija, s obzirom da su deklarirani u
privatnoj sekciji). Deklariranje je potrebno izvesti u privatnom a ne u javnom dijelu klase, jer e tada
kompajler prijaviti greku ve pri prvom pokuaju kopiranja objekta. Kada bi deklaracija bila izvedena u
javnom dijelu klase, kompajler ne bi prijavio greku pri pokuaju kopiranja objekta, ve tek na samom
kraju prevoenja (u fazi povezivanja), kada ustanovi da kopirajui konstruktor nije nigdje implementiran,
a iz tako prijavljene greke ne bismo mogli znati gdje je zapravo nastupio problem. Meutim, to su sve
tehnike koje vie ne treba koristiti.
Projektant klase se moe odluiti za zabranu kopiranja i dodjele ukoliko zakljui da bi kopiranje
odnosno dodjela mogli biti isuvie neefikasni. Na taj nain, on moe sprijeiti korisnika klase da prenosi
primjerke te klase po vrijednosti u funkcije i da vri inicijalizaciju ili dodjelu pomou primjeraka te
klase. Na alost, time se onemoguava i vraanje primjeraka te klase kao rezultata iz funkcije. Sreom,
postoje brojne situacije u kojima to nije neophodno. Ipak, ak i u takvim sluajevima ima smisla
definirati vlastiti pomjerajui konstruktor i pomjerajui operator dodjele, nakon ega e biti zabranjeni
kopiranje, ali ne i pomjeranje objekata (to ukljuuje i vraanje rezultata iz funkcija, koje se uvijek
tretira kao pomjeranje). Tako neto je uraeno (ali iz drugih razloga) recimo pri izvedbi tipa
unique_ptr koji definira jedinstvene pametne pokazivae. Kao zakljuak, ukoliko neka klasa
posjeduje destruktor, jedina ispravna rjeenja su ili da deklariramo i implementiramo kako kopirajui
konstruktor tako i kopirajui operator dodjele, ili da potpuno zabranimo kopiranje i meusobno
dodjeljivanje primjeraka te klase (uz eventualno doputanje njihovog pomjeranja) na prethodno opisani
nain. Uostalom, postoje samo dva razloga zbog kojih bi projektant klase izbjegao da primijeni jedan od
dva opisana pristupa: neznanje i lijenost. Protiv prvog razloga relativno se lako boriti, jer to se ne zna,
uvijek se da nauiti. Protiv drugog razloga (lijenosti) znatno se tee boriti.
Razmotrimo sada kako bi se mogao implementirati kopirajui konstruktor klase Razred u sluaju
da se odluimo da ipak dozvolimo kopiranje primjeraka te klase strategijom dubokog kopiranja. Da
bismo odgovorili na ovo pitanje, razmotrimo kakva je logika struktura ove klase, s obzirom da je cilj
konstruktora kopije da napravi tanu logiku kopiju objekta, a ne puku kopiju njenih atributa, kao i
kakva je veza te logike strukture sa atributima klase (tj. njenom fizikom strukturom). Logiki, objekat
klase Razred predstavlja kolekciju podataka o uenicima, ali fiziki gledano, ti podaci se ne sadre u
samom objektu, nego u posebnim dinamiki alociranim objektima. Tim objektima se pristupa preko niza
pokazivaa koji na njih pokazuju, ali ni taj niz pokazivaa nije sadran u samom objektu klase

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

Razred, nego mu se pristupa preko drugog pokazivaa (koji jeste sadran u objektu klase Razred).
Na primjer, jedan objekat tipa Razred koji ima kapacitet od 10 uenika a u kojem su registrirana 3
uenika izgleda zapravo ovako:

kapacitet

??? ??? ??? ??? ??? ??? ???

10
broj_evidentiranih

3
ucenici

ime

prezime

ime

prezime

ime

prezime

datum_rodjenja

datum_rodjenja

datum_rodjenja

dan

dan

dan

mjesec godina

ocjene
prosjek

mjesec godina

ocjene
prolaz

prosjek

mjesec godina

ocjene
prolaz

prosjek

prolaz

Sad ako malo razmislimo, vidjeemo da trebamo napraviti kopije svih objekata koji sadre podatke
o uenicima, ali da ne trebamo kopirati niz pokazivaa na njih, s obzirom da oni svakako trebaju
pokazivati na novostvorene kopije, a ne na izvorne podatke o uenicima. Najbolji nain da se to izvede
je sljedei:
Razred::Razred(const Razred &r) : ucenici(new Ucenik*[r.kapacitet]),
kapacitet(r.kapacitet), broj evidentiranih(r.broj_evidentiranih) {
for(int i = 0; i < r.broj_evidentiranih; i++)
ucenici[i] = new Ucenik(*r.ucenici[i]);
}

Ovdje se unutar for petlje dinamiki alocira memorija za objekte tipa Ucenik kopiju podataka
za svakog od uenika, pri emu se svaki novostvoreni objekat inicijalizira upravo onim objektom tipa
Ucenik iju kopiju treba da predstavlja. Ovdje se zapravo prilikom kreiranja objekta vri eksplicitni
poziv kopirajueg konstruktora klase Ucenik (naravno, ovdje se radi o automatski generiranom
podrazumijevanom kopirajuem konstruktoru, s obzirom da klasa Ucenik nema definiran vlastiti
kopirajui konstruktor). S obzirom da ovdje moe doi do kopiranja velike koliine podataka, od velike
koristi e biti i definiranje pomjerajueg konstruktora sa ciljem da se izbjegne kopiranje kad god se ono
pouzdano moe izbjei. Kao i obino, pomjerajui konstruktor prosto treba ukrasti sve resurse od
objekta koji se pomjera, pri emu objekat koji se pomjera treba ostaviti u takvom stanju da kad se na
njim izvri destruktor, ne doe do nikakve tete (najbolje da se ne desi nita), to nije teko postii:
Razred::Razred(Razred &&r) : ucenici(r.ucenici),
kapacitet(r.kapacitet), broj_evidentiranih(r.broj_evidentiranih) {
r.ucenici = nullptr; r.broj_evidentiranih = 0;
}

Izvedba operatora dodjele je (odluimo li se da podrimo dodjelu), kao i uvijek sloenija, zbog
injenice da odredini objekat nije prazan i da se trebamo pobrinuti ta raditi sa resursima koje on ve
posjeduje. Pored toga, oteavajua okolnost je da je atribut kapacitet definiran kao konstantan, tako
da niko (pa ni operator dodjele) ne moe da ga promijeni. Zbog toga emo podrati samo meusobnu
dodjelu objekata tipa Razred istog kapaciteta (alternativna mogunost bila bi sasjecanje, odnosno
odbacivanje prekobrojnih uenika u sluaju da kapacitet odredinog objekta nije dovoljan da primi sve
uenike koji su registrirani u izvorinom objektu), u suprotnom emo baciti izuzetak. Nakon toga, treba
obaviti kopiranje. Najjednostavniji nain da se to izvede je prosto obrisati sve uenike koji su bili u
odredioj klasi, a zatim je napuniti kopijama uenika pohranjenim u izvornoj klasi, uz obaveznu zatitu
od destruktivne samododjele (da su se kapaciteti izvorne i odredine klase mogli razlikovati, bilo bi
eventualno potrebno i izvriti realokaciju odgovarajueg niza pokazivaa na uenike):

10

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

Razred &Razred::operator =(const Razred &r) {


if(r.kapacitet != kapacitet)
throw std::logic_error("Razredi nesaglasnih kapaciteta!");
if(&r != this) {
for(int i = 0; i < broj_evidentiranih; i++) delete ucenici[i];
for(int i = 0; i < r.broj_evidentiranih; i++)
ucenici[i] = new Ucenik(*r.ucenici[i]);
broj_evidentiranih = r.broj_evidentiranih;
}
return *this;
}

Znatno efikasniji ali i sloeniji nain da se to uradi je sljedei, u kojem se ne vri niti jedna suvina
alokacija niti dealokacija. Ukoliko odredite ne sadri dovoljno prostota za uenike, oni se kreiraju, a
ukoliko ih sadri previe, suvini se uklanjaju. Samo kopiranje objekata Ucenik iz jednog alociranog
prostora u drugi obavlja se konstrukcijom poput *ucenici[i] = *r.ucenici[i]. Temeljita
analiza i razumijevanje ove implementacije od presudne je vanosti za stvaranje kompletne slike o
nainima manipulacije sa sloenijim kontejnerskim tipovima:
Razred &Razred::operator =(const Razred &r) {
if(r.kapacitet != kapacitet)
throw std::logic_error("Razredi nesaglasnih kapaciteta!");
if(r.broj_evidentiranih > broj_evidentiranih) {
for(int i = 0; i < broj_evidentiranih; i++)
*ucenici[i] = *r.ucenici[i];
for(int i = broj_evidentiranih; i < r.broj_evidentiranih; i++)
ucenici[i] = new Ucenik(*r.ucenici[i]);
}
else {
for(int i = 0; i < r.broj_evidentiranih; i++)
*ucenici[i] = *r.ucenici[i];
for(int i = r.broj_evidentiranih; i < broj_evidentiranih; i++)
delete ucenici[i];
}
broj_evidentiranih = r.broj_evidentiranih;
return *this;
}

Ostaje jo i pomjerajui operator dodjele, ija analiza ne bi trebala predstavljati problem:


Razred &Razred::operator =(Razred &&r) {
if(r.kapacitet != kapacitet)
throw std::logic_error("Razredi nesaglasnih kapaciteta!");
if(&r != this) {
for(int i = 0; i < broj_evidentiranih; i++) delete ucenici[i];
ucenici = r.ucenici; broj_evidentiranih = r.broj_evidentiranih;
r.ucenici = nullptr; r.broj_evidentiranih = 0;
}
return *this;
}

Treba napomenuti da klasa Razred takoer nije pogodna za realizaciju zasnovanu na strategiji
plitkog kopiranja i brojanja pristupa (ali je zato izuzetno pogodna za razne tipove COW strategija, u ta
ne moemo ulaziti). Da bismo uvidjeli u to, samo treba razmotriti ta bi se trebalo dogoditi i ta bi se
dogodilo ukoliko bi se nad nekim objektom tipa Razred koji je plitka kopija drugog takvog objekta
pozvala neka metoda koja bi dodala novog uenika u razred (npr. metoda EvidentirajUcenika).
Istraimo sada kako bismo mogli realizirati klasu Razred koristei malo modernije koncepte.
Umjesto da uenike uvamo u dinamiki alociranom nizu pokazivaa na objekte tipa Ucenik, mogli
bismo umjesto toga koristiti vektor iji su elementi pokazivai na objekte tipa Ucenik. To bi nas
oslobodilo brige o brisanju dinamiki alociranog niza, ali bi se i dalje morali brinuti o brisanju dinamiki
alociranih objekata tipa Ucenik. Stoga bi bilo jo bolje da atribut ucenici bude vektor pametnih
pokazivaa na objekte tipa Ucenik. To bi nas u potpunosti oslobodilo brige o oslobaanju memorije.
Razmotrimo stoga kakve bi to izmjene trailo u do sada razmotrenom programu.
11

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

Na prvom mjestu, atribut ucenici u klasi Razred treba promijeniti da bude vektor pametnih
pokazivaa na objekte tipa Ucenik. Atributi kapacitet i broj evidentiranih mogu se
izbaciti, prvi zbog toga to vektori nemaju fiksnu veliinu (tj. kapacitet im nije fiksiran) a drugi zbog
injenice da se trenutni broj elemenata u vektoru uvijek moe saznati pozivom funkcije size.
Naravno, deklaracije gdje se javljaju obini pokazivai treba prepraviti u deklaracije pametnih
pokazivaa (ukljuujui i deklaracije parametara funkcija). U funkciji SortirajUcenike trebae
izmijeniti poziv funkcije sort, jer se ona ne moe tako pozivati sa vektorima (prva dva argumenta
trebaju se zamijeniti sa ucenici.begin() i ucenici.end(). Na svim mjestima gdje se koristio
atribut broj evidentiranih (na primjer u funkciji IspisiIzvjestaj), treba ga zamijeniti
konstrukcijom ucenici.size() (alternativno, u za potrebe ove funkcije moe se sada koristiti i
rangovska for-petlja). U funkciji EvidentirajUcenika nema vie potrebe provjeravati da li je
dostignut kapacitet razreda i runo aurirati informaciju o broju uenika, nego je dovoljno prosto izvriti
konstrukciju ucenici.push back(ucenik). U funkciji UnesiUcenika nije neophodno runo
inicijalizirati pametni pokaziva ucenik na nul-pokaziva (pametni pokazivai se automatski
inicijaliziraju na nul-pokazivae ako se drugaije ne zada). Dinamiku alokaciju objekta tipa Ucenik
u istoj funkciji trebalo bi izvriti pozivom funkcije make shared, dok naredbe delete ucenik i
ucenik = nullptr u catch bloku iste funkcije postaju nepotrebne (prva zapravo vie nije ni
mogua), jer se pametni pokazivai sami brinu o oslobaanju resursa na koji pokazuju.
Najvee izmjene nastaju u upravljakim elementima klase (konstruktorima, destruktorima, itd.).
Konstruktor vie ne treba imati parametre, jer se kapacitet klase ne zadaje (samim tim kapacitet se ne
zadaje ni kada deklariramo primjerak klase Razred u main funkciji). Konstruktor bi trebao da
inicijalizira parametar ucenici na prazan vektor. Meutim, to bi isto uradio i automatski generirani
konstruktor bez parametara. Problem je to kompajler nee automatski generirati takav konstruktor.
Naime, ako mi definiramo ma kakav vlastiti konstruktor (pa ak i kopirajui), kompajler ne generira
automatski konstruktor bez parametara (sa praznim tijelom). Stoga takav konstruktor moramo napisati
sami, odnosno unutar klase Razred moramo napisati neto poput Razred() {}. Alternativno,
umjesto toga moemo pisati i Razred() = default;. Konstrukcija = default ima znaenje
odgovara mi ono to bi radio automatski generirani konstruktor (ili destruktor, ili bilo ta to se u
nekim okolnostima automatski generira). to se tie destruktora, on postaje potpuno nepotreban. Naime,
podrazumijevani automatski generirani destruktor obrisao bi vektor ucenici i sve njegove elemente.
Meutim, kako su njegovi elementi pametni pokazivai, njihovim nestankom automatski nestaju i
objekti na koji su oni pokazivali!
Postavlja se pitanje trebaju li nam vlastiti kopirajui konstruktor i kopirajui operator dodjele.
Podrazumijevani kopirajui konstruktor dodue napravio bi potpunu kopiju vektora ucenici, ali bi ta
kopija i dalje sadravala (pametne) pokazivae na iste objekte tipa Ucenik kao i original. Stoga bismo
i dalje imali neku vrstu plitke kopije. Dodue, takva plitka kopija bi se ponaala mnogo konzistentnije
nego plitka kopija koju bismo imali sa nizom (obinih) pokazivaa (razmislite sami zato). Ukoliko nam
to odgovara, onda nam ne oni trebaju (pri emu se ne trebamo bojati interakcija izmeu plitkih kopija i
destruktora, jer neemo ni pisati vlastiti destruktor koji bi mogao napraviti problem). Meutim, ukoliko
nam je potrebno duboko kopiranje, moramo implementirati vlastiti kopirajui konstruktor i operator
dodjele. Najjednostavniji nain da se to izvede izgleda ovako (za one koji su razumjeli dosadanja
izlaganja, analiza je dovoljno jednostavna da ne zahtijeva detaljnija objanjenja):
Razred::Razred(const Razred &r) : ucenici(r.ucenici.size()) {
for(int i = 0; i < r.ucenici.size(); i++)
ucenici[i] = std::make_shared<Ucenik>(*r.ucenici[i]);
}
Razred &Razred::operator =(const Razred &r) {
ucenici.resize(r.ucenici.size());
for(int i = 0; i < r.ucenici.size(); i++)
ucenici[i] = std::make_shared<Ucenik>(*r.ucenici[i]);
return *this;
}

ta je sa pomjerajuim konstruktorom i pomjerajuim operatorom dodjele? Njih takoer ne bi bilo


teko napisati pjeke. Meutim, s obzirom da tip vector podrava move-semantiku (zahvaljujui

12

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

pomjerajuim konstruktorom i operatorom dodjele koji su definirani unutar klase vector), najlaki
nain da izvedemo i ove elemente klase bio bi sljedei:
Razred::Razred(Razred &&r) : ucenici(std::move(r.ucenici)) {}
Razred &Razred::operator =(Razred &&r) {
ucenici = std::move(r.ucenici);
return *this;
}

Meutim, upravo istu stvar bi uradili i podrazumijevani pomjerajui konstruktor i pomjerajui


operatorom dodjele, koje kompajler nee automatski definirati zbog toga to smo definirali vlastiti
kopirajui konstruktor i operator dodjele (to spreava generiranje njihovih automatskih pomjerajuih
pratilaca). Meutim, kao alternativu, umjesto gore prikazanih implementacija unutar deklaracije klase
moemo pisati sljedee (podsjetimo se da = default ima znaenje odgovara mi ono to bi bilo
automatski generirano, mada nije automatski generirano):
Razred(Razred &&r) = default;
Razred &operator =(Razred &&r) = default;

Kao neto sloeniju ilustraciju veine do sada izloenih koncepata, daemo prikaz programa koji
definira generiku klasu Matrica, koja veoma efektno enkapsulira u sebe tehnike za dinamiko
upravljanje memorijom. Detaljan opis rada programa slijedi odmah nakon njegovog prikaza:
#include
#include
#include
#include
#include

<iostream>
<iomanip>
<cstring>
<stdexcept>
<new>

template <typename TipEl>


class Matrica {
int br_redova, br_kolona;
TipEl **elementi;
char ime_matrice;
void AlocirajMemoriju(int br_redova, int br_kolona);
void DealocirajMemoriju();
void KopirajElemente(TipEl **elementi);
public:
Matrica(int br_redova, int br_kolona, char ime = 0);
Matrica(const Matrica &m);
Matrica(Matrica &&m);
~Matrica() { DealocirajMemoriju(); }
Matrica &operator =(const Matrica &m);
Matrica &operator =(Matrica &&m);
void Unesi();
void Ispisi(int sirina_ispisa) const;
template <typename Tip2>
friend Matrica<Tip2> ZbirMatrica(const Matrica<Tip2> &m1,
const Matrica<Tip2> &m2);
};
template <typename TipEl>
void Matrica<TipEl>::AlocirajMemoriju(int br_redova, int br_kolona) {
elementi = new TipEl*[br_redova];
for(int i = 0; i < br_redova; i++) elementi[i] = nullptr;
try {
for(int i = 0; i < br_redova; i++)
elementi[i] = new TipEl[br_kolona];
}
catch(...) {
DealocirajMemoriju();
throw;
}
}

13

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

template <typename TipEl>


void Matrica<TipEl>::DealocirajMemoriju() {
for(int i = 0; i < br_redova; i++) delete[] elementi[i];
delete[] elementi;
}
template <typename TipEl>
Matrica<TipEl>::Matrica(int br_redova, int br_kolona, char ime) :
br_redova(br_redova), br_kolona(br_kolona), ime_matrice(ime) {
AlocirajMemoriju(br_redova, br_kolona);
}
template <typename TipEl>
void Matrica<TipEl>::KopirajElemente(TipEl **elementi) {
for(int i = 0; i < br_redova; i++)
for(int j = 0; j < br_kolona; j++)
Matrica::elementi[i][j] = elementi[i][j];
}
template <typename TipEl>
Matrica<TipEl>::Matrica(const Matrica<TipEl> &m) :
br_redova(m.br_redova), br_kolona(m.br_kolona),
ime_matrice(m.ime_matrice) {
AlocirajMemoriju(br_redova, br_kolona);
KopirajElemente(m.elementi);
}
template <typename TipEl>
Matrica<TipEl>::Matrica(Matrica<TipEl> &&m) :
br_redova(m.br_redova), br_kolona(m.br_kolona),
elementi(m.elementi), ime_matrice(m.ime_matrice) {
m.br_redova = 0; m.elementi = nullptr;
}
template <typename TipEl>
Matrica<TipEl> &Matrica<TipEl>::operator =(const Matrica<TipEl> &m) {
if(br_redova < m.br_redova || br_kolona < m.br_kolona) {
DealocirajMemoriju(); AlocirajMemoriju(m.br_redova, m.br_kolona);
}
else if(br_redova > m.br_redova)
for(int i = m.br_redova; i < br_redova; i++) delete elementi[i];
br_redova = m.br_redova; br_kolona = m.br_kolona;
ime_matrice = m.ime_matrice; KopirajElemente(m.elementi);
return *this;
}
template <typename TipEl>
Matrica<TipEl> &Matrica<TipEl>::operator =(Matrica<TipEl> &&m) {
if(&m != this) {
DealocirajMemoriju();
br_redova = m.br_redova; br_kolona = m.br_kolona;
ime_matrice = m.ime_matrice; elementi = m.elementi;
m.br_redova = 0; m.elementi = nullptr;
}
return *this;
}
template <typename TipEl>
void Matrica<TipEl>::Unesi() {
for(int i = 0; i < br_redova; i++)
for(int j = 0; j < br_kolona; j++) {
std::cout << ime_matrice << "(" << i + 1 << ","
<< j + 1 << ") = ";
std::cin >> elementi[i][j];
}
}
template <typename TipEl>
void Matrica<TipEl>::Ispisi(int sirina_ispisa) const {
for(int i = 0; i < br_redova; i++) {

14

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

for(int j = 0; j < br_kolona; j++)


std::cout << std::setw(sirina_ispisa) << elementi[i][j];
std::cout << std::endl;
}
}
template <typename TipEl>
Matrica<TipEl> ZbirMatrica(const Matrica<TipEl> &m1,
const Matrica<TipEl> &m2) {
if(m1.br_redova != m2.br_redova || m1.br_kolona != m2.br_kolona)
throw std::domain_error("Matrice nemaju jednake dimenzije!");
Matrica<TipEl> m3(m1.br_redova, m1.br_kolona);
for(int i = 0; i < m1.br_redova; i++)
for(int j = 0; j < m1.br_kolona; j++)
m3.elementi[i][j] = m1.elementi[i][j] + m2.elementi[i][j];
return m3;
}
int main() {
int m, n;
std::cout << "Unesi broj redova i kolona za matrice:\n";
std::cin >> m >> n;
try {
Matrica<double> a(m, n, 'A'), b(m, n, 'B');
std::cout << "Unesi matricu A:\n"; a.Unesi();
std::cout << "Unesi matricu B:\n"; b.Unesi();
std::cout << "Zbir ove dvije matrice je:\n";
ZbirMatrica(a, b).Ispisi(7);
}
catch(std::bad_alloc) {
std::cout << "Nema dovoljno memorije!\n";
}
return 0;
}

Mada do sada nije eksplicitno reeno da klase takoer mogu biti generike, to nije neoekivano s
obzirom da strukture mogu biti generike, a klase su samo poopenje struktura. Slino kao kod
generikih struktura, samo ime generike klase (npr. Matrica) nije tip, nego tip dobijamo tek nakon
specifikacije parametara ablona (stoga npr. Matrica<double> jeste tip). Unutar same deklaracije
klase, parametar ablona ne treba navoditi, s obzirom da je ve sama deklaracija klase uklopljena u
ablon (tako da se parametar ablona podrazumijeva). S druge strane, izvan deklaracije klase, parametar
ablona se ne podrazumijeva (s obzirom da je podruje primjene ablona unutar kojeg je deklarirana
generika klasa ogranieno na samu deklaraciju). Stoga se bilo gdje izvan deklaracije same klase gdje se
oekuje ime tipa, ime Matrica ne smije koristiti samostalno kao ime tipa, ve se uvijek mora navesti
parametar ablona. Kao parametar ablona moemo iskoristiti neki konkretni tip (ukoliko se elimo
vezati za konkretan tip), ali moemo i ponovo iskoristiti neki neodreeni tip, koji je parametar nekog
novog ablona. Tako je uraeno i u prikazanom primjeru, u kojem su sve metode klase (osim
destruktora) implementirane izvan deklaracije klase. Svaka od njih je uklopljena u ablon, tako da po
formi sve implementacije metoda podsjeaju na implementacije obinih generikih funkcija.
Pogledajmo sada implementaciju ove klase. Vidimo da pored konstruktora, destruktora i vlastitih
operatora dodjele, interfejs ove klase sadri metode Unesi i Ispisi, koje su dovoljno jednostavne,
tako da se o njima nema mnogo toga rei (metoda Ispisi zahtijeva kao parametar eljenu irinu
ispisa koji e zauzeti svaki element matrice). Definirana je i prijateljska generika funkcija
ZbirMatrica koja vri sabiranje dvije matrice, koja je takoer jasna sama po sebi. Razumije se da bi
stvarna klasa Matrica koja bi bila upotrebljiva u vie razliitih programa morala imati znatno bogatiji
interfejs (na primjer, ne postoji ni jedna metoda koja omoguava pristup individualnim elementima
matrice). Meutim, ovdje nismo eljeli da program ispadne predugaak, jer je osnovni cilj programa da
uvidimo kako su implementirani konstruktori, destruktori i operatori dodjele ove generike klase.
Meutim, potrebno je obratiti posebnu panju na to kako je uspostavljeno prijateljstvo izmeu
generike klase Matrica i generike funkcije ZbirMatrica. Naime, unutar deklaracije klase
moramo naglasiti da je ZbirMatrica generika funkcija, pri emu moramo upotrijebiti neko drugo
ime parametra ablona (Tip2 u naem primjeru) da bismo izbjegli konflikt sa imenom parametra
15

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


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 11_b
Akademska godina 2013/14

ablona koji definira klasu Matrica (ne moemo sa typename uvesti ime nekog metatipa ukoliko
se to isto ime ve ranije definirano i jo uvijek vaee na tom mjestu gdje ga pokuavamo deklarirati).
Da nismo unutar deklaracije klase naglasili da je ZbirMatrica generika funkcija, tada ne bi bilo
uspostavljeno prijateljstvo izmeu generike klase Matrica i generike funkcije ZbirMatrica,
nego izmeu svake od konkretnih specifikacija generike klase Matrica (kao to su recimo
Matrica<int>, Matrica<double>, itd.) i odgovarajue obine (tj. negenerike) funkcije
ZbirMatrica iji parametri po tipu odgovaraju konkretnoj specifikaciji generike klase Matrica,
ako takva obina funkcija uope postoji (na primjer, izmeu klase Matrica<double> i obine
funkcije ZbirMatrica iji su parametri i povratna vrijednost tipa Matrica<double>, ako takve
funkcije uope ima). Ovo vjerovatno nije ono to elimo, stoga treba raditi ovako kako je gore opisano.
Konstruktor klase Matrica ima tri parametra: dimenzije matrice i znak koji se koristi pri ispisu
prilikom unosa elemenata matrice (ovaj parametar je opcionalan, a u sluaju izostavljanja podrazumjieva
se nul-karakter). Ovaj konstruktor, pored inicijalizacije atributa br redova, br kolona i
ime matrice, vri i dinamiku alokaciju matrice kojoj se pristupa preko dvojnog atributa-pokazivaa
elementi. Meutim, kako je sama dinamika alokacija dvodimenzionalnih nizova neto sloeniji
postupak, a isti postupak e biti potreban u konstruktoru kopije i preklopljenom operatoru dodjele,
alokaciju smo povjerili privatnoj metodi AlocirajMemoriju, koja e se pozivati kako iz obinog
konstruktora, tako i iz konstruktora kopije i funkcije lanice koja realizira preklopljeni operator dodjele.
Na taj nain znatno skraujemo program. Metodu AlocirajMemoriju smo uinili privatnom, jer
korisnik klase nikada nee imati potrebu da ovu metodu poziva eksplicitno. Ovo je lijepa ilustracija kada
moe biti od koristi da se neka metoda deklarira kao privatna.
Sama metoda AlocirajMemoriju vri postupak dinamike alokacije dvodimenzionalnog niza na
slian nain kao u funkciji StvoriMatricu koju smo demonstrirali na predavanjima na kojima smo
govorili o strukturama. Pri tome je bitno naglasiti da se ova metoda brine da poisti iza sebe sve izvrene
alokacije u sluaju da alokacija memorije ne uspije do kraja. Naime, eventualni izuzetak koji baci ova
funkcija bie baen i iz konstruktora, jer unutar konstruktora ne vrimo hvatanje izuzetaka. U sluaju da
se izuzetak baci iz konstruktora, smatra se da objekat nije ni kreiran, pa nee biti pozvan ni destruktor.
Stoga, ukoliko funkcija AlocirajMemoriju ne poisti svoje smee iza sebe u sluaju neuspjene
alokacije, niko ga drugi nee poistiti (niti e ga moi poistiti), to naravno vodi ka curenju memorije.
Pomenuto ienje povjereno je funkciji DealocirajMemoriju, koja se poziva iz konstruktora (u
sluaju greke), iz destruktora (destruktor zapravo ne radi nita drugo osim poziva ove funkcije) i, u
sluaju potrebe, iz funkcije koja realizira preklopljeni operator dodjele.
Konstruktor kopije klase Matrica pored kopiranja atributa br redova, br kolona i
ime matrice obavlja novu dinamiku alokaciju (pozivom metode AlocirajMemoriju) nakon
ega kopira element po element sve elemente izvorne dinamike matrice u novokreirani prostor. Ovo
kopiranje se takoer moglo optimizirati koritenjem funkcije copy iz biblioteke algorithm na
sljedei nain, koji nije ba oigledan na prvi pogled (ovdje se petljom kopiraju individualni redovi
matrice, a funkcija copy se koristi za kopiranje jednog reda):
for(int i = 0; i < br redova; i++)
std::copy(elementi[i], elementi[i] + br kolona, Matrica::elementi[i]);

Funkcija koja realizira kopirajui operator dodjele takoer obavlja duboko kopiranje, vodei pri
tome rauna da ne obavlja realokaciju memorije u sluaju kada to nije neophodno (npr. pri dodjeli neke
matrice drugoj matrici koja je prethodno bila istog ili veeg formata). Treba primijetiti da je u sluaju
kada je odredina matrica prije dodjele imala vei broj redova nego izvorna matrica potrebno osloboditi
memoriju koju su zauzimali dodatni redovi. Ukoliko to ne uinimo, imaemo curenje memorije. Zaista,
nakon obavljene dodjele, odredina matrica gubi informaciju o tome koliko je redova imala prije dodjele.
Stoga kada doe vrijeme da se odredina matrica brie, destruktor nee imati nikakvu informaciju o tome
da su prije postojali dodatni redovi, tako da ih nee moi ni obrisati (drugim rijeima, ukoliko prethodno
ne obriemo dodatne redove, kasnije to nee uiniti niko drugi). Pomjerajui konstruktor i operator
dodjele su izvedeni na manje-vie uobiajeni nain. Ostaje jo glavni program (odnosno funkcija
main) koji je dovoljno jednostavan da ne zahtijeva nikakva posebna objanjenja.

16

You might also like