You are on page 1of 13

Dr.

Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

Predavanje 8_b
Moguće je formirati i generičke strukture, odnosno strukture kod kojih tipovi nekih (a moţda i svih)
atributa nisu unaprijed poznati. Ovakve strukture se takoĎer deklariraju uz pomoć šablona, odnosno
ključne riječi “template”. Slijedi jedan vrlo jednostavan primjer deklaracije generičke strukture, kao i
konkretnih primjeraka ovakvih struktura sa primjerima upotrebe:

template <typename TipPrvogClana, typename TipDrugogClana>


struct UredjeniPar {
TipPrvogClana prvi_clan;
TipDrugogClana drugi_clan;
};
...
UredjeniPar<double, double> tacka_u_ravni, par_brojeva;
UredjeniPar<std::string, int> osoba_i_maticni_broj;
tacka_u_ravni.prvi_clan = 5.17; tacka_u_ravni.drugi_clan = 2.5;
osoba_i_clanski_broj.prvi_clan = "Meho Mehić";
osoba_i_clanski_broj.drugi_clan = 174;

Primijetimo da se, prilikom deklariranja konkretnih primjeraka generičke strukture “UredjeniPar”


poput “tacka_u_ravni”, “par_brojeva” odnosno “osoba_i_clanski_broj”, obavezno unutar
šiljastih zagrada “<>” treba specificirati značenje formalnih parametara šablona, odnosno smisao
nepoznatih tipova upotrijebljenih unutar strukture. Drugim riječima, za razliku od generičkih funkcija,
ovdje nisu moguće automatske dedukcije tipova. TakoĎer je bitno napomenuti da, strogo posmatrano,
samo ime generičke strukture (u ovom primjeru “UredjeniPar”) ne predstavlja tip, odnosno ne postoje
objekti tipa “UredjeniPar”. Tip dobijamo tek nakon specifikacije parametara šablona, tako da
konstrukcije poput “UredjeniPar<double, double>” ili “UredjeniPar<std::string, int>”
jesu tipovi. Pri tome, konkretni tipovi dobijeni iz istog šablona uz različite parametre šablona
predstavljaju različite tipove. Tako su, u prethodnom primjeru, promjenljive “tacka_u_ravni” i
“par_brojeva” istog tipa, ali koji je različit od tipa promjenljive “osoba_i_clanski_broj”. Stoga
na primjer, dodjela poput “par_brojeva = osoba_i_clanski_broj” nije legalna, ali je dodjela
poput “par_brojeva = tacka_u_ravni” legalna.

Moguće je napraviti funkcije koje olakšavaju kreiranje ovakvih struktura. Primjer je sljedeća
generička funkcija, koja olakšava kreiranje objekata izvedenih iz generičke strukture “UredjeniPar”:
template <typename TipPrvogClana, typename TipDrugogClana>
UredjeniPar<TipPrvogClana, TipDrugogClana> NapraviPar(
TipPrvogClana prvi, TipDrugogClana drugi) {
UredjeniPar<TipPrvogClana, TipDrugogClana> novi_par;
novi_par.prvi_clan = prvi; novi_par.drugi_clan = drugi;
return novi_par;
};

Ova funkcija se zahvaljujući tretmanu inicijaliziacionih listi uvedenom u C++11 mogla i mnogo
kraće napisati tako da joj se u tijelu nalazi samo jedna jedina naredba “ return {prvi, drugi}”, ali
smo je namjerno napisali ovako, da bude jasnije šta se ovdje tačno dešava. Sa ovako napisanom
funkcijom “UredjeniPar”, moţemo pisati konstrukcije poput sljedećih:
tacka_u_ravni = NapraviPar(5.17, 2.5);
osoba_i_clanski_broj = NapraviPar(std::string("Meho Mehić"), 174);
osoba_i_clanski_broj = NapraviPar<std::string, int>("Meho Mehić", 174);

MeĎutim, sljedeća dodjela nije korektna, s obzirom da niz znakova izmeĎu navodnika nije tipa
“string” nego tipa “const char []”), tako da dedukcija tipa neće ispravno odrediti tip parametra
drugog člana strukture:
osoba_i_clanski_broj = NapraviPar("Meho Mehić", 174); // NE RADI!

1
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

Gore uvedena generička struktura “UredjeniPar” veoma je praktična za predstavljanje pojmova


koji se mogu predstaviti kao ureĎeni par dvije različite vrijednosti. Stoga je generička struktura slična
ovoj (samo dopunjena nekim dodatnim funkcionalnostima poput mogućnosti automatske konverzije
izmeĎu ureĎenih parova različitih tipova elemenata, ali koji su takvi da im se elementi mogu meĎusobno
konvertirati) definirana u jeziku C++ kao jedan od standardnih tipova podataka pod nazivom “ pair” u
biblioteci “utility”. Polja koja smo u generičkoj strukturi “UredjeniPar” nazvali “prvi clan” i
“drugi clan”, u generičkoj strkuturi “pair” nazivaju se “first” i “second”. Stoga bi se gore
navedeni primjeri uz korištenje standardnog tipa “pair” mogli napisati recimo ovako:

std::pair<double, double> tacka_u_ravni, par_brojeva;


std::pair<std::string, int> osoba_i_maticni_broj;
tacka_u_ravni.first = 5.17; tacka_u_ravni.second = 2.5;
osoba_i_maticni_broj.first = "Meho Mehić";
osoba_i_maticni_broj.second = 174;

Isto tako, podrţana je funkcija “make_pair” analogna funkciji “NapraviPar” koju smo pisali, i koja
se moţe koristiti kao u sljedećim primjerima:
tacka_u_ravni = std::make_pair(5.17, 2.5);
osoba_i_maticni_broj = std::make_pair("Meho Mehić", 174);

Primijetimo da u posljednjem primjeru nije neophodno eksplicitno konvertirati tekst izmeĎu navodnika u
tip “string” niti pisati konstrukcije “std::make_pair<std::string, int>” u kojima se koristi
eksplicitna specifikacija tipa, upravo zbog činjenice da generička struktura “pair” podrţava razne
automatske konverzije izmeĎu sličnih tipova polja koje mi u generičkoj strukturi “UredjeniPar” nismo
podrţali (niti uopće znamo podrţati sa dosada uvedenim znanjem).

Sama po sebi, generička struktura “pair” i nije toliko interesantna koliko činjenica da je ona
tijesno vezana sa jednim kontejnerskim tipom podataka koji se u teoriji podataka naziva mapa,
asocijativni niz (engl. associative array), ili rječnik (engl. dictionary). U jeziku C++ taj tip podataka se
naziva “map” i definiran je u istoimenoj biblioteci. Mape su tijesno vezane sa ureĎenim parovima (tj. sa
tipom “pair”), i u mnogim aspektima se ponašaju poput skupova čiji su elementi ureĎeni parovi, što se
moţe vidjeti i iz sljedećeg primjera, iz kojeg se ujedno vidi i kako se deklariraju promjenljive ovog tipa:

std::map<std::string, int> stanovnistvo;


stanovnistvo.insert(std::make_pair("Sarajevo", 450000));
stanovnistvo.insert(std::make_pair("Banja Luka", 200000));
stanovnistvo.insert(std::make_pair("Mostar", 85000));
for(auto it = stanovnistvo.begin(); it != stanovnistvo.end(); it++)
std::cout << "Grad: " << it->first << " Broj stanovnika: "
<< it->second << std::endl;

Obratimo paţnju na konstrukciju poput “it->first” koja je praktično ekvivalentna konstrukciji


“(*it).first”, kao i na činjenicu da nas je “auto” deklaracija spasila od potrebe za navoĎenjem
glomaznog tipa iteratora “it” (koji ovdje glasi “std::map<std::string, int>::iterator”).
Inače, počev od standarda C++11, elementi mape se mogu inicijalizirati pomoću inicijalizacionih listi, a
takoĎer, mapama se mogu dodjeljivati inicijalizacione liste. Te inicijalizacione liste izgledaju kao u
sljedećoj deklaraciji koja je praćena inicijalizacijom:
std::map<std::string, int> stanovnistvo{{"Sarajevo", 450000},
{"Banja Luka", 200000}, {"Mostar", 85000}};

Glavna razlika izmeĎu mapa i skupova ureĎenih parova je što se kod mapa prvo polje pripadnih
parova tretira kao ključno polje, po kojem se vrši pretraţivanje, dok se drugo polje tretira kao prateća
pridružena vrijednost. Dakle, kada vršimo pretragu pomoću funkcija “find”, “count” i sličnih (koje
smo objašnjavali kod opisa tipa “set”), kao parametar se zadaje samo vrijednost ključnog polja, a ne
čitav par. Isto tako, kada se vrši brisanje elementa iz mape, kao parametar se zadaje samo vrijednost
ključnog polja. Slijedi primjer koji pronalazi i ispisuje koliko stanovnika ima Mostar, ukoliko takav
podatak postoji upisan u mapu:

2
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

auto it(stanovnistvo.find("Mostar"));
if(it == stanovnistvo.end()) std::cout << "Nema podataka!";
else std::cout << "Broj stanovnika: " << it->second;

MeĎutim, ukoliko smo sigurni da traţena informacija postoji, moţemo joj pristupiti mnogo jednostavnije,
navoĎenjem vrijednosti ključnog polja unutar uglastih zagrada, odnosno istom sintaksom kao kada se
koristi indeksiranje (bez obzira što se ovdje ne radi o indeksu i bez obzira što ta vrijednost uopće ne
mora biti cijeli broj):
std::cout << "Broj stanovnika " << stanovnistvo["Mostar"];

Odavde vidimo da se mape sintaksno ponašaju kao da se radi o nizovima čijim se elementima
pristupa ne preko indeksa, nego prema vrijednosti ključnog polja (tj. koji se “indeksiraju” ne prema
poziciji elementa u nizu, nego prema vrijednosti ključnog polja). Ova sintaksa radi i u slučaju kada
ţelimo dodati novi par ključa i pridruţene vrijednosti u mapu. Recimo, ţelimo li dodati broj stanovnika za
Tuzlu, umjesto rogobatne kombinacije funkcija “insert” i “make_pair”, moţemo pisati prosto ovako:

stanovnistvo["Tuzla"] = 130000;

Podrška ovoj mogućnosti ima i jednu nuspojavu. Naime, svaki put kada se unutar uglaste zagrade
upotrijebi vrijednost ključnog polja koja trenutno ne postoji u mapi, kreira se novi par u mapi koji sadrţi
upravo tu vrijednost, čak i ukoliko se ne koristi naredba dodjele (u tom slučaju se kao vrijednost
pridruţenog polja upisuje podrazumijevana vrijednost za tip prirduţenog polja). Recimo, ukoliko
izvršimo nešto poput
std::cout << stanovnistvo["Kifino Selo"];

s obzirom da par sa ključem “Kifino Selo” ne postoji, biće kreiran i upisan u mapu novi par koji sadrţi
ključ “Kifino Selo” i pridruţenu vrijednost 0 (bez obzira što nismo izvršili nikakvu dodjelu), nakon čega
će se ta pridruţena vrijednost (0) ispisati na ekran. Da bismo izbjegli neţeljeno kreiranje novog para
ukoliko nismo sigurni da li ţeljena informacija postoji ili ne, neophodno je izvršiti odgovarajuću
provjeru, kao u sljedećem isječku:
if(!stanovnistvo.count("Mostar")) std::cout << "Nema podataka!";
else std::cout << "Broj stanovnika " << stanovnistvo["Mostar"];

Bitno je naglasiti da u mapama ne smiju postojati dva para sa istim ključnim poljem, tj. ključ mora
biti jedinstven. Ukoliko ubacimo novi par sa ključem koji već postoji, onaj stari par se briše, tj. mijenja
se novim parom. Ukoliko nam to ne odgovara, moţemo umjesto mape koristiti multimapu, koja se
takoĎer nalazi u biblioteci “map”, ali pod imenom “multimap”. MeĎutim, kod multimapa nije podrţan
pristup informacijama navoĎenjem vrijednosti ključa u uglastim zagradama kao kod mapa, s obzirom da
kod multimapa vrijednost ključnog polja ne odreĎuje jedinstveno odgovarajuću pridruţenu vrijednost.

Interesantna mogućnost nastaje ukoliko kreiramo mapu čiji su ključevi takoĎer cjelobrojnog tipa.
Takva mapa moţe se koristiti na način koji izuzetno podsjeća na način kako se koriste klasični nizovi.
Ovaj primjer to ilustrira:
std::map<int, double> a;
a[5] = 12.5; a[32] = 1.24; a[1149] = 0.712; a[23398] = 91;
std::cout << a[5] << " " << a[32] << " " << a[1149] << " " << a[23398];

MeĎutim, ono što je vaţno uočiti je činjenica da bi realizacija iste konstrukcije sa običnim nizom (ili
vektorom) zahtijevala niz od barem 23399 elemenata, s obzirom da je najveći “indeks” koji se koristi
23398. S druge strane, ukoliko je “a” asocijativni niz (mapa), u njemu su zapravo zapamćena samo četiri
para ključ/vrijednost, odnosno parovi (5, 12.5), (32, 1.24), (1149, 0.712) i (23398, 91). Drugim riječima,
upotreba asocijativnih nizova moţe dovesti do izuzetne uštede u memoriji pri radu sa tzv. rijetkim
nizovima, odnosno nizovima kod kojih je samo mali broj elemenata različit od nule. Ipak, treba se čuvati
problema poput sljedećih. Ukoliko sad neko naivno napiše nešto poput

3
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

double suma(0);
for(int i=0; i < 1000; i++) suma += a[i]; // OVO NIJE DOBRA IDEJA!

kreiraće se veliki broj novih parova, zbog pristupa nepostojećim ključevima. Da bi se izbjegao ovaj
problem i potreba za testiranjem da li ključ postoji ili ne, kroz ovakve “nizove” treba se kretati ili
pomoću iteratora, ili rangovskim for-petljama.

Izlaganje o mapama završićemo sa još nekoliko konstatacija. Prvo, ni tip ključa ni tip pridruţene
vrijednosti ne moraju biti jednostavni tipovi. Sasvim je moguće (i korisno) da tip pridruţene vrijednosti
nekom ključu bude vektor (npr. ključ moţe biti ime i prezime studenta, a pridruţena vrijednost vektor
njegovih ocjena). Dalje, slično kao kod skupova, mape zahtijevaju da tip ključa bude tip koji podrţava
poreĎenje pomoću operatora “<” (osim ukoliko nismo spremni zadati vlastiti poredak, u šta se nećemo
upuštati). Kao alternativu, C++11 u biblioteci “unordered map” nudi i tipove “unordered map” i
“unordered multimap” slične tipovima “map” i “multimap”, samo zasnovane na tzv. hash tabelama
slično kao i tipovi “unordered set” i “unordered multiset”.

Parametri generičkih struktura ne moraju biti samo imena tipova (tj. metatipovi), već mogu biti i
neke druge stvari, poput cjelobrojnih konstanti. Ovo je iskorišteno u sljedećem primjeru:
template <typename TipElemenata, int broj_elemenata>
struct UmotaniNiz {
TipElemenata elementi[broj_elemenata];
};
...
UmotaniNiz<double, 5> a, b;
UmotaniNiz<int, 10> c;
UmotaniNiz<int, 5> d;

Prilikom zadavanja parametara šablona, parametar “broj elemenata” mora biti konstantan (tj. ne
moţe biti promjenljiva ili proizvoljan nekonstantni izraz). TakoĎer, treba istaći da su tipovi poput
“UmotaniNiz<int, 10>” i “UmotaniNiz<int, 5>” dva posve različita tipa koji se ne mogu
meĎusobno dodjeljivati. Slična logika da parametri šablona ne moraju biti isključivo metatipovi vrijedi i
za generičke funkcije. Na primjer, sljedeća generička funkcija “IspisiUmotaniNiz” prihvata kao
parametre različite vrste objekata izvedenih iz generičke strukture “ UmotaniNiz” i ispisuje ih na ekran
(dakle, legalni su pozivi poput “IspisiUmotaniNiz(a)” i “IspisiUmotaniNiz(d)”):
template <typename TipElemenata, int broj_elemenata>
void IspisiUmotaniNiz(UmotaniNiz<TipElemenata, broj_elemenata> niz) {
for(int i = 0; i < broj_elemenata; i++)
std::cout << niz.elementi[i] << std::endl;
}

Neko će se vjerovatno zapitati čemu ovakve generičke strukture poput strukture “UmotaniNiz”
mogu posluţiti. Odgovor je u činjenici da se pomoću takvih struktura mogu prevazići neki nedostaci
nizova naslijeĎenih iz jezika C, ali bez uvoĎenja komplikacija zasnovanih na dinamičkoj alokaciji
memorije koju interno koriste tipovi poput tipa “vector”. Naime, poznato je da kada se u funkciju
prenose nizovi kao parametri, tada se gotovo uvijek kao dodatni parametar mora prenositi i broj
elemenata niza, koji funkcija drugačije ne bi mogla saznati. Isto tako, funkcije ne mogu vratiti nizove
kao rezultate. Sve ovo se moţe izbjeći ukoliko formiramo strukturu koja će u sebe nekako “upakovati”
kako sam niz, tako i informaciju o broju njegovih elemenata (što je upravo uraĎeno u gore prikazanoj
strukturi). Na ovaj način, sve informacije koje opisuju niz (njegovi elementi i broj elemenata) upakovane
su u jednu strukturu, koju moţemo prenijeti kao jedinstven parametar u funkciju, pa čak i vratiti kao
rezultat iz funkcije. Biblioteka “array” uvedena u standardu C++11 uvodi generičku strukturu nazvanu
isto “array” koja je u osnovi vrlo slična gore definiranoj generičkoj strukturi “ UmotaniNiz”, samo
proširena još nekim detaljima, kao što su podrška radu sa iteratorima, i još neki detalji u koje zbog
nedostatka prostora nećemo ulaziti.

Veoma interesantne mogućnosti dobijamo kreiranjem struktura koje kao svoje atribute koriste
pokazivače. Na primjer, moguće je kreirati strukturu “Matrica”, koja kao svoje atribute sadrţi dvojni

4
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

pokazivač na fragmentirano alocirani dvodimenzionalni niz, kao i dimenzije matrice (broj redova i
kolona). Naravno, za praktičan rad sa matricama mnogo je jednostavnije i sigurnije koristiti vektor čiji
su elementi vektori, ali razmatranje ovakvih struktura će nam postupno omogućiti razumijevanje internih
mehanizama koji stoje u pozadini rada dinamičkih struktura podataka. Stoga ćemo ilustrirati ovaj
koncept kroz jedan relativno sloţen program, koji deklarira generičku strukturu “Matrica”, sa
neodreĎenim tipom elemenata matrice, koja sadrţi (dvojni) pokazivač na dinamički alociranu matricu,
kao i dimenzije matrice. S obzirom da se radi o generičkoj strukturi, sve funkcije koje s njom rade
moraju biti ili generičke funkcije (ukoliko ţele da rade sa najopćenitijom formom generičke strukture
“Matrica”), ili se moraju ograničiti isključivo na rad sa nekim konkretnim tipom izvedenim iz
generičke strukture “Matrica” (npr. tipom “Matrica<double>”. Ovdje je prikazana univerzalnija
varijanta, koja koristi generičke funkcije. Najsloţenija funkcija u ovom programu je funkcija
“StvoriMatricu” koja kreira matricu dinamički, i vraća kao rezultat odgovarajuću strukturu
“Matrica”. Pored toga, ona vodi brigu o tome da ne doĎe do curenja memorije ukoliko u procesu
kreiranja matrice ponestane memorije, o čemu smo ranije detaljno govorili. Njoj komplementarna
funkcija je funkcija “UnistiMatricu”, čiji je zadatak oslobaĎanje memorije. Primijetimo da u ovom
slučaju, za razliku od slične funkcije koju smo napisali prilikom demonstracije dinamičke alokacije i
dealokacije višedimenzionalnih nizova, nije potrebno u funkciju prenositi podatke o dimenzijama
matrice, s obzirom da su ti podaci već sadrţani u strukturi “ Matrica” koja se prosljeĎuje ovoj funkciji
kao parametar.
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <new>
template <typename TipElemenata>
struct Matrica {
int br_redova, br_kolona;
TipElemenata **elementi = nullptr; // VEOMA BITNA INICIJALIZACIJA!
};
template <typename TipElemenata>
void UnistiMatricu(Matrica<TipElemenata> mat) {
if(!mat.elementi) return;
for(int i = 0; i < mat.br_redova; i++) delete[] mat.elementi[i];
delete[] mat.elementi;
mat.elementi = nullptr;
}
template <typename TipElemenata>
Matrica<TipElemenata> StvoriMatricu(int br_redova, int br_kolona) {
Matrica<TipElemenata> mat;
mat.br_redova = br_redova; mat.br_kolona = br_kolona;
mat.elementi = new TipElemenata*[br_redova];
for(int i = 0; i < br_redova; i++) mat.elementi[i] = nullptr;
try {
for(int i = 0; i < br_redova; i++)
mat.elementi[i] = new TipElemenata[br_kolona];
}
catch(...) {
UnistiMatricu(mat);
throw;
}
return mat;
}
template <typename TipElemenata>
void UnesiMatricu(char ime_matrice, Matrica<TipElemenata> &mat) {
for(int i = 0; i < mat.br_redova; i++)
for(int j = 0; j < mat.br_kolona; j++) {
std::cout << ime_matrice
<< "(" << i + 1 << "," << j + 1 << ") = ";
std::cin >> mat.elementi[i][j];
}
}

5
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

template <typename TipElemenata>


void IspisiMatricu(const Matrica<TipElemenata> &mat,
int sirina_ispisa) {
for(int i = 0; i < mat.br_redova; i++) {
for(int j = 0; j < mat.br_kolona; j++)
std::cout << std::setw(sirina_ispisa) << mat.elementi[i][j];
std::cout << std::endl;
}
}
template <typename TipElemenata>
Matrica<TipElemenata> ZbirMatrica(const Matrica<TipElemenata> &m1,
const Matrica<TipElemenata> &m2) {
if(m1.br_redova != m2.br_redova || m1.br_kolona != m2.br_kolona)
throw std::domain_error("Matrice nemaju jednake dimenzije!");
auto m3(StvoriMatricu<TipElemenata>(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() {
Matrica<double> a, b, c; // AUTOMATSKA INICIJALIZACIJA!!!
int m, n;
std::cout << "Unesi broj redova i kolona za matrice:\n";
std::cin >> m >> n;
try {
a = StvoriMatricu<double>(m, n);
b = StvoriMatricu<double>(m, n);
std::cout << "Unesi matricu A:\n";
UnesiMatricu('A', a);
std::cout << "Unesi matricu B:\n";
UnesiMatricu('B', b);
std::cout << "Zbir ove dvije matrice je:\n";
IspisiMatricu(c = ZbirMatrica(a, b), 7);
}
catch(std::bad_alloc) {
std::cout << "Nema dovoljno memorije!\n";
}
UnistiMatricu(a); UnistiMatricu(b); UnistiMatricu(c);
return 0;
}

Mada je prikazani program vrlo elegantan, u njemu se javljaju neki prilično nezgodni detalji na koje
treba posebno obratiti paţnju. Prvo, primijetimo da smo pored matrica “a” i “b” definirali i matricu “c”,
a umjesto jednostavnog poziva poput
IspisiMatricu(ZbirMatrica(a, b), 7);

koristili smo znatno nezgrapniji poziv


IspisiMatricu(c = ZbirMatrica(a, b), 7);

koji je funkcionalno ekvivalentan slijedu od dvije naredbe


c = ZbirMatrica(a, b);
IspisiMatricu(c, 7);

Postavlja se pitanje zašto smo uopće morali definirati promjenljivu “c”. Problem je u tome što funkcija
“ZbirMatrica” dinamički kreira novu matricu (pozivom funkcije “StvoriMatricu”) i kao rezultat
vraća strukturu koja sadrţi pokazivač na zauzeti dio memorije. Ova struktura bi se mogla neposredno
prenijeti u funkciju “IspisiMatricu” bez ikakve potrebe za pamćenjem vraćene strukture u
promjenljivoj “c”. MeĎutim, na taj način bismo izgubili pokazivač na zauzeti dio memorije i ne bismo
kasnije imali priliku da oslobodimo zauzeti dio memorije (pozivom funkcije “ UnistiMatricu”).

6
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

Naime, problem je u tome što se sav dinamički zauzet prostor mora eksplicitno obrisati upotrebom
operatora “delete”. Ova potreba da se eksplicitno brinemo o brisanju svakog dinamičkog objekta koji
je kreiran, moţe nam zadati mnogo glavobolja ako ne ţelimo (a svakako ne bismo trebali da ţelimo) da
uzrokujemo neprestano curenje memorije. Kasnije ćemo vidjeti da se ovaj problem moţe riješiti
primjenom tzv. destruktora koji na sebe mogu automatski preuzeti brigu za brisanje memorije koju je
neki objekat zauzeo onog trenutka kada taj objekat više nije potreban (pametni pokazivači nam ovdje ne
bi bili od koristi, s obzirom da koristimo dinamičku alokaciju nizova, a ne individualnih objekata).

Drugi detalj koji upada u oči je da smo u deklaraciji strukture “Matrica” eksplicitno inicijalizirali
polje “elementi” na nul-pokazivač (ne zaboravimo da je ta mogućnost uvedena tek u C++11). Ovo je
bitno zbog sljedećeg razloga. Na kraju programa potrebno je eksplicitno uništiti sve tri matrice “a”, “b” i
“c” (tj. osloboditi prostor koji je rezerviran za smještanje njihovih elemenata). Pretpostavimo sada da
stvaranje matrice “a” uspije, ali da prilikom stvaranja matrice “b” doĎe do bacanja izuzetka (npr. zbog
nedovoljne količine raspoloţive memorije). Izuzetak će biti uhvaćen u “catch” bloku, ali stvorenu
matricu “a” treba obrisati. Njeno brisanje će se ionako desiti nakon “catch” bloka pozivom funkcije
“UnistiMatricu”, ali šta je sa matricama “b” i “c”? Naredna dva poziva funkcije “UnistiMatricu”
trebali da unište i njih, ali problem je što one nisu ni stvorene! Ukoliko sada paţljivije pogledamo
funkciju “UnistiMatricu”, vidjećemo da ona ne radi ništa u slučaju da polje “elementi” u matrici
sadrţi nulu (tj. nul-pokazivač). Kako su na početku polja “elementi” u sve tri matrice inicijalizirana na
nul-pokazivač, one matrice koje nisu ni stvorene i dalje će imati nul-pokazivač u ovom polju, tako da
funkcija “UnistiMatricu” neće nad njima ništa ni uraditi. Da nismo na početku izvršili inicijalizaciju
polja “elementi” na nul-pokazivač, nastali bi veliki problemi ukoliko funkciji “UnistiMatricu”
proslijedimo matricu koja nije ni stvorena (tj. za čije elemente nije alociran prostor). Naime, pokazivač
“elementi” bi imao neku slučajnu vrijednost (jer sve klasične promjenljive koje nisu inicijalizirane
imaju slučajne početne vrijednosti), pa bi unutar funkcije “UnistiMatricu” operator “delete” bio
primijenjen nad pokazivačima za koje je potpuno neizvjesno na šta pokazuju (najvjerovatnije ni na šta
smisleno). Stoga je ishod ovakvih akcija posve nepredvidljiv. Stoga, da polje “elementi” nije
automatski inicijalizirano na nul-pokazivač, morali bismo ili ručno inicijalizirati ova polja na nul-
pokazivač prilikom deklaracije matrica “a”, “b” i “c” (ili ih postaviti na nul-pokazivač odmah nakon
njihove deklaracije), ili koristiti ugnijeţdene “try” – “catch” strukture, što je, kao što smo već vidjeli,
veoma neelegantno i nezgrapno. Uskoro ćemo vidjeti i kako se ovaj problem moţe riješiti i pomoću tzv.
konstruktora, bez obzira što smo pokazali da C++11 nudi sasvim elegantno rješenje korištenjem
inicijalizacija unutar definicije strukture (rješenje zasnovano na konstruktorima primjenljivo je i u
starijim dijalektima C++-a).

Neophodno je ukazati na još jednu nezgodnu pojavu koja moţe nastati kod upotrebe struktura koje
kao svoja polja sadrže pokazivače. Pretpostavimo da imamo generičku strukturu “Matrica” deklariranu
kao u prethodnom programu, i da smo izvršili sljedeću sekvencu naredbi:

Matrica<double> a, b;
a = StvoriMatricu<double>(10, 10);
b = a;
a.elementi[5][5] = 13;
b.elementi[5][5] = 18;
std::cout << a.elementi[5][5];

Mada bi se moglo očekivati da će ovaj program ispisati broj 13, on će zapravo ispisati broj 18! Ovo
se ne bi desilo da je polje “elementi” u strukturi “Matrica” deklarirano kao običan dvodimenzionalni
niz (umjesto kao pokazivač na dinamički alocirani dvodimenzionalni niz). Šta se zapravo desilo? Kada
se jedna struktura kopira u drugu pomoću znaka dodjeljivanja “=”, kopiraju se sva polja iz jedne
strukture u drugu (i ništa drugo). MeĎutim, treba obratiti paţnju da polje “elementi” nije niz nego
pokazivač, tako da se prilikom kopiranja polja iz strukture “a” u strukturu “b” kopira samo pokazivač, a
ne ono na šta on pokazuje. Stoga, nakon obavljenog kopiranja, obje strukture “ a” i “b” sadrţe polja
nazvana “elementi” koja sadrţe istu vrijednost, odnosno pokazuju na istu adresu tj. isti dinamički niz!
Drugim riječima, kopiranjem polja iz “a” u “b” ne stvara se novi dinamički niz, nego imamo jedan
dinamički niz sa dva pokazivača koja pokazuju na njega (jedan u strukturi “a”, a drugi u strukturi “b”).
Stoga je jasno da se bilo koji pristup dinamičkom nizu bilo preko pokazivača “a.elementi” ili preko
“b.elementi” odnose na isti dinamički niz! Dinamički niz nije element strukture nego se nalazi izvan

7
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

nje i ne kopira se zajedno sa strukturom! Ovo se najbolje moţe vidjeti na sljedećoj slici, koja opisuje šta
se tačno dešava u memoriji:
a
br_redova br_kolona elementi

b
br_redova br_kolona elementi

...

Činjenica da se prilikom kopiranja struktura koje sadrţe pokazivače iz jedne u drugu kopiraju samo
pokazivači, a ne i ono na šta oni pokazuju naziva se plitko kopiranje, a dobijene kopije nazivamo plitkim
kopijama. Plitko kopiranje obično ne pravi neke probleme ukoliko ga imamo u vidu (tj. sve dok imamo u
vidu činjenicu da dobijamo plitke kopije), ali djeluje donekle suprotno intuitivnom poimanju kako bi
dodjeljivanje trebalo da radi. Naime, nakon obavljenog plitkog kopiranja strukturne promjenljive “ a” u
promjenljivu “b”, promjenljiva “b” se ponaša više poput reference na promjenljivu “a” nego poput njene
kopije. Ovakvo ponašanje je u nekim programskim jezicima posve uobičajeno (npr. u jeziku Java
dodjeljivanjem strukturne promjenljive “a” promjenljivoj “b”, promjenljiva “b” zapravo postaje
referenca na promjenljivu “a”), ali ne i u jeziku C++. Doduše, mnogi teoretičari objektno orijentiranog
programiranja (o kojem ćemo govoriti u narednim poglavljima) smatraju da je za strukturne tipove plitko
kopiranje prirodnije (o ovome ćemo diskutirati kasnije), ali autor ovih materijala ne dijeli to mišljenje.
Kasnije ćemo vidjeti da se problem plitkog kopiranja moţe riješiti uz pomoć preklapanja operatora tako
da se operatoru “=” promijeni značenje tako da obavlja kopiranje ne samo pokazivača unutar strukture,
nego i dinamičkih elemenata na koje pokazivači pokazuju (tzv. duboko kopiranje). Bez obzira što je
plitko kopiranje u mnogim kontekstima prihvatljivo, postoji ipak jedna potencijalna opasnost do koje
moţe doći usljed korištenja plitkih kopija (koja će posebno doći do izraţaja kada se upoznamo sa
pojmom destruktora). Posmatrajmo sljedeći programski isječak:
Matrica<double> a, b;
a = StvoriMatricu<double>(10, 10);
b = a;
UnistiMatricu(b);

Mada je cilj poziva “UnistiMatricu(b)” vjerovatno trebao biti uništavanje matrice “b” (tj. oslobaĎanje
prostora zauzetog za njene elemente), ovaj poziv će se odraziti i na matricu “a”. Naime, kako pokazivač
“elementi” u obje matrice pokazuje na isti memorijski prostor, nakon oslobaĎanja zauzetog prostora
pozivom “UnistiMatricu(b)”, pokazivač “elementi” u ,atrici “a” će pokazivati na upravo
oslobođeni prostor, odnosno postaće viseći pokazivač! Time je uništavanje matrice “b” efektivno
uništilo i matricu “a”, što je posljedica činjenice da “b” zapravo nije prava kopija matrice “a” (već nešto
nalik na referencu na nju). Ovaj primjer pokazuje da pri radu sa strukturama koje kao svoje elemente
imaju pokazivače trebamo uvijek biti na oprezu!

Plitko kopiranje ne nastaje samo prilikom dodjeljivanja jedne strukturne promjenljive drugoj, nego i
prilikom prenosa po vrijednosti strukturnih promjenljivih kao parametara u funkcije, kao i prilikom
vraćanja struktura kao rezultata iz funkcije. Na primjer, posmatrajmo sljedeću funkciju:
template <typename TipElemenata>
void AnulirajMatricu(Matrica<TipElemenata> mat) {
for(int i = 0; i < mat.broj redova; i++)
for(int j = 0; j < mat.broj kolona; j++)
mat.elementi[i][j] = 0;
}

8
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

Ova funkcija će postaviti sve elemente matrice koja joj se proslijedi kao parametar na nulu.
MeĎutim, na prvi pogled, ova funkcija ne bi trebala to da uradi, s obzirom da joj se parametar prenosi
po vrijednosti, a ne po referenci. Zar formalni parametar “mat” nije samo kopija stvarnog parametra
prenesenog u funkciju? Naravno da jeste, ali razmotrimo šta je zapravo ta kopija. Ona sadrţi kopiju
dimenzija matrice proslijeĎene kao stvarni argument i kopiju pokazivača na njene elemente. MeĎutim, ta
kopija pokazivača pokazuje na iste elemente kao i izvorni pokazivač, odnosno formalni parametar “mat”
predstavlja plitku kopiju stvarnog argumenta. Zbog toga je pristup elementima matrice preko polja
“elementi” unutar parametra “mat” ujedno i pristup elementima izvorne matrice. Drugim riječima,
mada je parametar zaista prenesen po vrijednosti, izgleda kao da funkcija mijenja sadrţaj stvarnog
parametra (mada ona zapravo mijenja elemente matrice koji u suštini uopće nisu sastavni dio stvarnog
parametra, već se nalaze izvan njega). U ovom slučaju ponovo imamo situaciju koja intuitivno odudara
od očekivanog ponašanja pri prenosu parametara po vrijednosti (ovdje je ponašanje skoro istovjetno kao
da je parametar prenesen po referenci). Ovakvo ponašanje uzrokovano je plitkim kopiranjem, odnosno
činjenicom da ovdje parametar zapravo ne sadrţi u sebi elemente matrice (mada izgleda kao da ih
sadrţi) – oni se nalaze izvan njega.

Ovakav neintuitivan tretman prilikom prenosa po vrijednosti parametara strukturnog tipa koji sadrţe
pokazivače, takoĎer ne dovodi do većih problema, sve dok smo ga svjesni. MeĎutim, ovakvo ponašanje
je moguće promijeniti uz pomoć tzv. konstruktora kopije, koji preciziraju način kako će se tačno vršiti
kopiranje strukturnih parametara prilikom prenosa po vrijednosti, i prilikom vraćanja rezultata iz
funkcije. Na taj način je moguće realizirati tzv. duboko kopiranje, odnosno prenos koji će biti u skladu sa
intuicijom. O ovome ćemo detaljno govoriti u kasnije.

Vjerovatno ćete se sada sa pravom zapitati zbog čega stalno navodimo primjere raznih
problematičnih situacija uz napomenu da će problem biti riješen kasnije, umjesto da odmah ponudimo
rješenje u kojem se navedeni problem ne javlja. Razlog za ovo je sljedeći: jako je teško shvatiti zbog
čega nešto treba raditi onako kako bi se trebalo raditi ukoliko se prethodno ne shvati šta bi se desilo
kada bi se radilo drugačije, odnosno ako bi se radilo onako kako se ne treba raditi. TakoĎer, prilično je
teško shvatiti razloge za upotrebu nekih naprednijih tehnika koji na prvi pogled djeluju komplicirano
(kao što su konstruktori, destruktori, konstruktori kopije, preklapanje operatora dodjele, itd.) ukoliko
prethodno ne shvatimo kakvi se problemi javljaju ukoliko se ove tehnike ne koriste.

Razumije se da ni jedan strukturni tip ne moţe sadrţavati polje koje je istog strukturnog tipa kao i
struktura u kojoj je sadrţano, jer bi to bila “rekurzija bez izlaza” (imali bismo “strukturu koja kao polje
ima strukturu koja kao polje ima strukturu koja kao polje ima...” i tako bez kraja). MeĎutim, struktura
moţe sadrţavati polja koja su pokazivači na isti strukturni tip. Takve strukture nazivaju se čvorovi (engl.
nodes) a odgovarajući pokazivači unutar njih koji pokazuju na primjerke istog strukturnog tipa nazivaju
se veze (engl. links). Na primjer, neki čvor moţe biti deklariran na sljedeći način:
struct Cvor {
int element;
Cvor *veza;
};

Čvorovi predstavljaju osnovni gradivni element na kojem se zasniva rad strkutura podataka kao što
su liste, skupovi, mape i slični tipovi koje smo ukratko spominjali. Na ovom mjestu ćemo ilustrirati
samo osnovnu ideju koja ilustrira smisao čvorova. Pretpostavimo da ţelimo unijeti i zapamtiti slijed
brojeva koji se završava nulom, ali da nam broj brojeva koji će biti uneseni nije unaprijed poznat.
Pretpostavimo dalje da ne ţelimo zauzeti više memorije od one količine koja je zaista neophodna za
pamćenje unesenih brojeva (odnosno, ne ţelimo na primjer deklarirati neki veliki niz koji će sigurno
moći prihvatiti sve unesene brojeve, ali i znatno više od toga). Kako ne moţemo znati unaprijed kada će
biti unesena nula, ne moţemo koristiti niti statičke nizove, niti vektore sa veličinom zadanom u trenutku
deklaracije, niti dinamički alocirane nizove. Tipovi kao što su vektor ili lista doduše nude elegantno
rješenje: moţemo prvo deklarirati prazan vektor (ili listu) a zatim pozivom operacije “push back”
dodavati na kraj unesene brojeve, jedan po jedan, i tako proširivati veličinu vektora (liste). Ovo rješenje
je zaista lijepo, ali dovodi do jednog suštinskog pitanja. Tipovi “vector” ili “list” predstavljaju
bibliotečki definirane tipove (definirane u istoimenim bibliotekama) i oni su na neki način implementirani
koristeći fundamentalna svojstva jezika C++ (tj. svojstva koja nisu definirana u bibliotekama, nego koja

9
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

čine samo jezgro jezika). Stoga se prirodno postavlja pitanje kako bi se slična funkcionalnost mogla
ostvariti bez korištenja bibliotečki definiranih tipova. To je očigledno moguće, jer su sami tipovi poput
“vector” ili “list” napravljen korištenjem onih svojstava koja postoje i bez njih!

Osnovna ideja zasniva se upravo na korištenju čvorova. Neka, na primjer, imamo čvor deklariran
kao u prethodnom primjeru. Razmotrimo sada sljedeći programski isječak:
Cvor *pocetak(nullptr), *prethodni;
for(;;) {
int broj;
std::cin >> broj;
if(broj == 0) break;
Cvor *novi(new Cvor);
novi->element = broj; novi->veza = nullptr;
if(!pocetak) pocetak = novi;
else prethodni->veza = novi;
prethodni = novi;
}

U ovom isječku, pri svakom unosu novog broja, dinamički kreiramo novi čvor i u njegovo polje
“element” upisujemo uneseni broj. Polje “veza” čvora koji sadrţi prethodni uneseni broj (ukoliko
takav postoji, odnosno ukoliko novokreirani čvor nije prvi čvor) usmjeravamo tako da pokazuje na
novokreirani čvor (za tu svrhu uveli smo pokazivačku promjenljivu “ prethodni” koja pamti adresu
čvora koji sadrţi prethodno uneseni broj). Prilikom kreiranja prvog čvora, njegovu adresu pamtimo u
pokazivaču “pocetak”. Polje “veza” novokreiranog čvora postavljamo na nul-pokazivač, čime zapravo
signaliziramo da iza njega ne slijedi nikakav drugi čvor. Kao ilustraciju, pretpostavimo da smo unijeli
slijed brojeva 3, 6, 7, 2, 5 i 0. Nakon izvršavanja prethodnog programskog isječka, u memoriji će se
formirati struktura podataka koja se moţe slikovito prikazati kao na sljedećoj slici:
pocetak prethodni

element veza element veza element veza element veza element veza
null
3 6 7 2 5 ptr

Ovako formirana struktura podataka u memoriji naziva se jednostruko povezana lista (engl. single
linked list), iz očiglednog razloga (tip podataka “forward_list” o kojem smo ranije govorili, interno
je organiziran upravo na ovakav način). Ovim smo zaista smjestili sve unesene brojeve u memoriju, ali
kako im moţemo pristupiti? Primijetimo da pokazivač “pocetak” sadrţi adresu prvog čvora, tako da
preko njega moţemo pristupiti prvom elementu. MeĎutim, ovaj čvor sadrţi pokazivač na sljedeći (drugi)
čvor, pa koristeći ovaj pokazivač moţemo pristupiti drugom elementu. Dalje, drugi čvor sadrţi
pokazivač na treći čvor, pa preko njega moţemo pristupiti i trećem elementu, itd. Na primjer, da
ispišemo prva tri unesena elementa, moţemo koristiti sljedeće konstrukcije:
std::cout << pocetak->element;
std::cout << pocetak->veza->element;
std::cout << pocetak->veza->veza->element;

Sada se postavlja pitanje kako ispisati sve unesene elemente? Za tu svrhu nam je očigledno potreban
neki sistematičniji način od gore prikazanog. Nije teško vidjeti da obična “for” petlja u kojoj se koristi
pomoćni pokazivač “p” koji u svakom trenutku pokazuje na čvor koji sadrţi tekući element (tj. element
koji upravo treba ispisati), i koji se u svakom prolazu kroz petlju pomjera tako da pokazuje na sljedeći
čvor, rješava traţeni zadatak (upravo se nešto ovako interno dešava kada se kroz listu krećemo uz pomoć
iteratora za liste; naime ukoliko je “it” iterator za listu, operacija “*it” interno izvršava nešto poput
“p->element”, a operacija “it++” interno izvršava nešto poput “p = p->veza”):
for(Cvor *p = pocetak; p != nullptr; p = p->veza)
std::cout << p->element << endl;

10
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

Veoma je bitno napomenuti da fizički raspored čvorova u memoriji uopće nije bitan, već je bitna
samo logička veza izmeĎu čvorova, ostvarena pomoću pokazivača. Naime, mi nikada ne znamo gdje će
tačno u memoriji biti smješten čvor konstruisan operatorom “new”. Mi ćemo dobiti kao rezultat ovog
operatora adresu gdje je čvor kreiran, ali ne postoje nikakve garancije da će se čvorovi u memoriji
stvarati tako da uvijek čine rastući slijed adresa (mada je takav ishod najvjerovatniji). Tako je
principijelno sasvim moguće (ali ne i mnogo vjerovatno) da stvarna memorijska slika povezane liste
prikazane na prethodnoj slici zapravo izgleda kao na sljedećoj slici:
pocetak prethodni

element veza element veza element veza element veza element veza
null
6 2 5 ptr 3 7

MeĎutim, kako se pristup čvorovima ostvaruje isključivo prateći veze, povezana lista čija fizička
organizacija u memoriji izgleda ovako ponaša se isto kao i povezana lista čiji čvorovi prirodno slijede
jedan drugog u memoriji.

Povezane liste su veoma fleksibilne i korisne strukture podataka. MeĎutim, već na ovom mjestu
treba uočiti jedan njihov bitan nedostatak u odnosu na nizove, koji smo istakli kada smo govorili o
tipovima “list” i “forward_list”. Naime, elementima smještenim u ovako kreiranu listu moţe se
pristupati isključivo sekvencijalno, jedan po jedan, u redoslijedu kreiranja, tako da nije moguće direktno
pristupiti recimo petom čvoru a da prethodno ne pročitamo prva četiri čvora (s obzirom da se adresa
petog čvora nalazi u četvrtom, adresa četvrtog u trećem, itd.). Na primjer, imali bismo velikih nevolja
ukoliko bismo unesene elemente trebali ispisati recimo u obrnutom poretku. TakoĎer, dodatni utrošak
memorije za pokazivače koji se čuvaju u svakom čvoru mogu biti nedostatak, pogotovo ukoliko sami
elementi ne zauzimaju mnogo prostora. Bez obzira na ova ograničenja, postoji veliki broj primjena u
kojima se elementi obraĎuju upravo sekvencijalno, i u kojima dodatni utrošak memorije nije velika
smetnja, tako da u takvim primjenama ovi nedostaci ne predstavljaju bitniju prepreku.

Kao što smo već ranije istakli, izrazite prednosti korištenja povezanih listi umjesto nizova nastaje u
primjenama u kojima je potrebno često ubacivati nove elemente između do tada ubačenih elemenata, ili
izbacivati elemente koji se nalaze izmeĎu postojećih elemenata. Poznato je da su ove operacije u slučaju
nizova veoma neefikasne. Na primjer, za ubacivanje novog elementa usred niza, prethodno je potrebno
sve elemente niza koji slijede iza pozicije na koju ţelimo ubaciti novi element pomjeriti za jedno mjesto
naviše, da bi se stvorilo prazno mjesto za element koji ţelimo da ubacimo. Ovim se troši mnogo
vremena, pogotovo ukoliko je potrebno pomjeriti mnogo elemenata niza. Slično, da bismo uklonili neki
element iz niza, potrebno je sve elemente niza koji se nalaze iza elementa koji izbacujemo pomjeriti za
jedno mjesto unazad. MeĎutim, ubacivanje elemenata unutar liste moţe se izvesti znatno efikasnije, uz
mnogo manje trošenja vremena. Naime, dovoljno je kreirati novi čvor koji sadrţi element koji umećemo,
a zatim izvršiti uvezivanje pokazivača tako da novokreirani čvor logički doĎe na svoje mjesto. Sve ovo
se moţe izvesti veoma efikasno (potrebno je promijeniti samo dva pokazivača). TakoĎer, brisanje
elementa se moţe izvesti samo promjenom jednog pokazivača (tako da se u lancu povezanih čvorova
“zaobiĎe” čvor koji sadrţi element koji ţelimo izbrisati), i brisanjem čvora koji sadrţi suvišan element
primjenom operatora “delete”. Ovdje su date samo osnovne ideje, a u implementacione detalje na
ovom mjestu nećemo ulaziti.

Sada kada smo naučili nešto o čvorovima, moţemo sagledati i “tamnu stranu” pametnih
pokazivača. Iz dosada izloţenog o pametnim pokazivačima moglo bi se zaključiti da uz upotrebu
pametnih pokazivača, pod uvjetom da se koriste na ispravan način (što, kako što smo vidjeli, i nije
osobito teško), problemi sa curenjem memorije postaju prošlost. Dovoljno je na za čuvanje adresa
dinamičkih kreiranih objekata (i samo njih) na svim mjestima umjesto običnih pokazivača koristiti
pametne pokazivače, zar ne? Naţalost, ne. Vrijeme je za malo otreţnjenje. Sa pametnim pokazivačima,
čak i uz konzistentnu upotrebu ipak je moguće napraviti curenje memorije (zbog toga smo i rekli da oni

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

rješavaju skoro sve probleme vezane za curenje memorije, ali ne sve). Problem koji ćemo opisati
objašnjava zbog čega pametni pokazivači ipak ne mogu u potpunosti zamijeniti sakupljače smeća (iako
su po mnogo čemu bolji, fleksibilniji i efikasniji od njih). Ovaj problem poznat je kao kružno
referenciranje (engl. circular reference) i on je Ahilova peta svih pametnih pokazivača. Objasnićemo
ovaj problem prvo na jednom posve jednostavnom primjeru, a zatim ćemo dati generalizaciju.
Pretpostavimo da imamo neki čvor, odnosno strukturu koja kao neki od svojih atributa sadrţi pokazivač
na neki drugi primjerak iste strukture. Djeluje mudra ideja da se obični pokazivač unutar čvora zamijeni
pametnim pokazivačem (u osnovi, to i jeste mudra ideja). Na primjer, pretpostavimo da nam je data
sljedeća struktura:
struct Cvor {
int element;
std::shared_ptr<Cvor> veza;
};

Razmotrimo sada sljedeći isječak programa, koji djeluje posve bezazleno:


std::shared_ptr<Cvor> c1(new Cvor);
std::shared_ptr<Cvor> c2(new Cvor);
c1->veza = c2; c2->veza = c1;

Ovdje smo dinamički kreirali dva čvora (tj. objekta tipa “Cvor”) i inicijalizirali pametne pokazivače
“c1” i “c2” da pokazuju na njih. Zatim smo atribut “veza” u prvom od njih postavili da pokazuje na
onaj drugi, a atribut “veza” u drugom od njih da pokazuje na onaj prvi. Dakle, uspostavili smo neku
vrstu cikličke (kruţne) veze izmeĎu dva dinamički alocirana objekta. U ovom trenutku, brojači pristupa
za oba dinamički alocirana objekta imaju vrijednost 2, jer na svaki njih pokazuju po dva pametna
pokazivača (jedan od njih je “c1” odnosno “c2”, a drugi je onaj pametni pokazivač u atributu “veza”
koji se nalazi u onom drugom od dva čvora u odnosu na čvor koji razmatramo). Pretpostavimo sada da
pametni pokazivači “c1” i “c2” prestanu postojati (npr. kada doĎemo do kraja bloka u kojem su
definirani). Tom prilikom se brojači pristupa dinamički alociranim čvorovima umanjuju za 1. MeĎutim,
ti brojači će sada imati vrijednost 1, tako da dinamički kreirani objekti neće biti oslobođeni, iako im se
nakon toga više ne može pristupiti. Iako su nestali jedini pametni pokazivači pomoću kojih se ovim
čvorovima moţe pristupiti, ovi čvorovi se i dalje “meĎusobno čuvaju” (putem pametnih pokazivača koji
su pohranjeni u njima) i na taj način sprečavaju da budu uklonjeni. Ukoliko se ova kruţna veza ne
raskine prije nego što pametni pokazivači “c1” i “c2” prestanu postojati (a ta veza moţe se raskinuti
preusmjeravanjem neke od veza izmeĎu čvorova, recimo dodjelom poput “c1->veza = nullptr”),
curenje memorije je neizbježno. Naime, u memoriji će ostati dva objekta koji, iako im niko više ne moţe
pristupiti, na neki način meĎusobno pokazuju jedan na drugog i tako sprečavaju svoje vlastito uništenje.
Oni postaju nedostupni, bez obzira što, tehnički gledano nisu siročad!

Ovaj primjer je vjerovatno najjednostavniji primjer koji ilustrira šta se moţe desiti. MeĎutim,
situacija moţe biti znatno sloţenija. Moţemo, na primjer, imati čitav lanac čvorova od kojih svaki sadrţi
pametni pokazivač koji pokazuje na sljedeći čvor, osim posljednjeg čvora, čiji pametni pokazivač
pokazuje nazad na prvi čvor (ovo se koristi kada se implementiraju recimo kružne povezane liste u
kojima se iza posljednjeg elementa ponovo nalazi prvi element, itd.). Ukoliko se takva kruţna veza ne
raskine prije kraja ţivota posljednjeg pametnog pokazivača pomoću kojeg se moţe pristupiti takvom
lancu, curenje memorije je neizbjeţno. Problem moţe nastati čak i u situaciji kada imamo samo jedan
čvor. Razmotromo recimo sljedeći isječak:
std::shared_ptr<Cvor> c(new Cvor);
c->veza = c;

Atribut “veza” u novokreiranom čvoru pokazuje na taj isti čvor. Kada pametni pokazivač “ c” prestane
postojati, dinamički kreirani čvor neće biti uništen, jer sadrţi u sebi pametni pokazivač koji čuva upravo
njega (tj. čvor u kojem se nalazi).

Problemi ove vrste mogu nastati i kada je u igri više pametnih pokazivača u istom čvoru. Na primjer,
kod implementacije dvostruko povezanih listi svaki čvor sadrţi dva pokazivača, na prethodni i na
sljedeći čvor (pri čemu nul-pokazivač tipično označava da prethodnog odnosno sljedećeg čvora nema):

12
Dr. Ţeljko Jurić: Tehnike programiranja /kroz programski jezik C++/ Predavanje 8_b
Radna skripta za kurs “Tehnike programiranja” na Elektrotehničkom fakultetu u Sarajevu Akademska godina 2013/14

struct Cvor {
int vrijednost;
std::shared_ptr<Cvor> prethodni, sljedeći;
};

Razmotrimo sada sljedeći isječak koji kreira dvostruko povezanu listu koja se sastoji od samo dva
meĎusobno povezana čvora:

std::shared_ptr<Cvor> c1(new Cvor);


std::shared_ptr<Cvor> c2(new Cvor);
c1->prethodni = nullptr; c1->sljedeci = c2;
c2->prethodni = c1; c2->sljedeci = nullptr;

Ovdje smo opet napravili ciklus u koji su upetljani pametni pokazivači. Prvi čvor sadrţi pametni
pokazivač “sljedeci” koji pokazuje na drugi čvor dok drugi čvor sadrţi pametni pokazivač
“prethodni” koji pokazuje na prvi čvor. Kada “c1” i “c2” prestanu postojati, svaki od ova dva čvora
sprečava da onaj drugi bude uništen, analogno kao u prethodnom primjeru. Što je najgore, ovakve
konstrukcije su nam neophodne ukoliko ţelimo kreirati dvostruko povezanu listu.

Opisani primjeri se mogu generalizirati. Do problema očito moţe doći kada god imamo strukture
koje sadrţe pametne pokazivače na druge strukture (to uopće ne moraju biti primjerci istih struktura kao
što je to slučaj kod čvorova) ukoliko se izmeĎu njih uspostavi neka vrsta kruţnog odnosa. Formalni opis
problema moţemo iskazati ovako. Nacrtajmo jedan graf u kojem čvorovi grafa predstavljaju pametne
pokazivače ili objekte koji u sebi sadrţe pametne pokazivače i u kojem strelica (grana) izmeĎu čvorova
X i Y postoji ako i samo ako X pokazuje na Y (ako je X pametni pokazivač) ili ako X u sebi sadrţi
pametni pokazivač koji pokazuje Y. Problem kruţnog referenciranja nastaje ako i samo ako tako kreirani
graf u sebi sadrţi kruţni put (ciklus odnosno konturu).

Šta da se radi? Do sada niko nije uspio kreirati pametne pokazivače koji automatski mogu riješiti
problem kruţnog referenciranja a da pri tome ne doĎe do drastičnog pada perfomansi. Slijedi da ovakve
probleme moramo rješavati ručno. Postoje samo dva načina za rješavanje ovog problema. Prvi način je
dopustiti kreiranje kruţnih veza, ali koje obavezno moraju biti ručno raskinute prije nego nestane
posljednji pametni pokazivač koji pokazuje na neki objekat unutar “začaranog kruga”. U nekim
situacijama to baš nije praktično (recimo, kod dvostruko povezanih listi postoji mnoštvo takvih
“začaranih krugova”, pri čemu su u svakom krugu samo dva čvora). Drugi način je uopće ne dozvoliti da
doĎe do uspostavljanja kruţnih veza. Pošto u stvaranju kruţnih veza učestvuju samo pametni pokazivači,
najjednostavniji način sprečavanja uspostavljanja kruţnih veza je na neko kritično mjesto unutar ciklusa
umjesto pametnog pokazivača upotrijebiti obični pokazivač. Recimo, kod kreiranja dvostruko povezanih
listi jedno od mogućih rješenja je da se za vezu unaprijed iskoristi pametni pokazivač, ali da se za vezu
unazad koriste obični (glupi) pokazivači. Oba načina zahtijevaju potpunu svjesnost programera o
problemu i odreĎenu disciplinu za njegovo rješavanje. Ovo pokazuje da se pametni pokazivači ne smiju
koristiti posve rutinski i mehanički, bez imalo razmišljanja. U tom smislu su sakupljači smeća ipak
jednostavniji za upotrebu (mada su manje efikasni i fleksibilni), ali i pametni pokazivači svakako
predstavljaju ogromnu pomoć u programiranju, a traţe samo minimum discipline i razmišljanja.

13

You might also like