You are on page 1of 23

DATOTEKE

Dosadašnji programi, sve su svoje podatke držali u varijablama. Varijable su smještene u


radnoj memoriji. Program dakle za vrijeme izvršavanja zauzima određeni dio radne memorije,
koji se nakon završetka rada programa oslobađa. Drugim riječima, nakon što program završi
s radom on "zaboravi" sve podatke sa kojima je raspolagao. Ovo je osnovni problem rada sa
varijablama. Također, pri radu sa varijablama može se dogoditi da program ne raspolaže sa
dovoljno radne memorije. U tom slučaju program se ne može niti izvršavati (ili će se tijekom
rada srušiti). Prostor radne memorije je relativno mali (u usporedbi sa npr. tipičnim veličinama
tvrdih diskova), te je nedostatak radne memorije moguće očekivati u programima koji barataju
sa većim brojem podataka. Ova dva problema, mogu se riješiti upotrebom datoteka. Glavna
svrha datoteka je trajno "pamćenje" podataka. Program sprema podatke koji se moraju trajno
pamtiti u datoteku, te ih može pri ponovnom pokretanju učitati.

OSNOVE RADA S DATOTEKAMA

Svaki korisnik računala već se susreo sa datotekama. Datoteke su organizirane po mapama


(folderi, direktoriji), na različitim diskovima. Datoteku jedinstveno određuje njen naziv. U
jednom folderu se ne mogu nalaziti dvije datoteke sa istim nazivom.

Kao što je već spomenuto, datoteke se primarno koriste za trajno pamćenje podataka.
Osnovne operacije koje se obavljaju pomoću datoteka su spremanje i učitavanje podataka.
Spremanje podataka je postupak pri kojem program podatke iz svojih varijabli (tj. iz radne
memorije) zapisuje u datoteku. Stoga se spremanje podataka još zove i zapisivanje podataka.
Pri učitavanju (tj. čitanju) podataka, program podatke prebacuje iz datoteke u svoje varijable.
Kaže se da program učitava (ili čita) iz datoteke.

Važno je napomenuti da se načelni princip rada ostatka programa ne mijenja. Osnovne


operacije (kao što su aritmetičke operacije, operacije usporedbe, ispis na zaslon i sl.) program
može obavljati samo pomoću varijabli.

Sada se mogu navesti osnovni koraci pri upotrebi datoteka:

1. Pri radu sa datotekom, datoteku je potrebno otvoriti. Otvaranje datoteke, znači


pridruživanje neke konkretne datoteke (koja je određena svojim imenom i mapom u kojoj
se nalazi), sa odgovarajućim objektom. Sve ostale operacije se obavljaju preko tog
objekta.

2. Nakon uspješnog otvaranja datoteke, slijedi obavljanje operacija nad njom. Program
zapisuje podatke iz memorije u datoteku ili učitava podatke iz datoteke u memoriju. Pri
zapisivanju i učitavanju, ostavljena je velika sloboda na koji način se to izvodi. Drugim
riječima, programer sam odlučuje koje će podatke program zapisivati (ili učitavati).
Posebno, pri učitavanju, nije nužno učitati cjelokupni sadržaj datoteke u radnu memoriju
(tj. u varijable programa).

3. Nakon što su svi podaci zapisani ili učitani potrebno je datoteku zatvoriti.

Ovo su bili osnovni koraci pri upotrebi datoteka. Datoteke se mogu upotrebljavati na razne
načine, ali osnovni princip rada ostaje isti: otvaranje datoteke, zapisivanje ili učitavanje
podataka, zatvaranje datoteke. Datoteka služi samo kao trajno "skladište" podataka. Da bi
program izveo bilo koje operacije nad podacima koji se nalaze u datoteci, on mora prethodno
te podatke učitati u varijable u radnu memoriju.
OTVARANJE DATOTEKE

Kao što je već spomenuto, otvaranje datoteke je pridruživanje konkretne datoteke objektu
određene klase. Sve ostale operacije nad datotekom se obavljaju preko te datoteke. Klasa čiji
se objekt pridružuje datoteci ovisi o tome za što će se datoteka upotrebljavati. Razlikujemo
dva slučaja: otvaranje datoteke za pisanje (tzv. izlazne datoteke) te otvaranje datoteke za
čitanje (tzv. ulazne datoteke). Datoteku otvaramo za pisanje kada želimo u nju nešto zapisati.
Slično, datoteka se otvara za čitanje ako želimo iz nje nešto pročitati. Bez obzira na način
upotrebe, ukoliko želimo koristiti datoteke u programu, potrebno je navesti slijedeću liniju na
početak programa:

#include <fstream.h>

DATOTEKE ZA PISANJE (IZLAZNE DATOTEKE)

Objekt koji se povezuje sa datotekama za pisanje je klase ofstream. Konstruktor klase


ofstream prima jedan argument - naziv datoteke koja se otvara za pisanje. Slijedeća naredba:

ofstream fout("IZLAZ.TXT");

povezuje datoteku IZLAZ.TXT sa objektom fout. Ovime je zapravo datoteka IZLAZ.TXT


otvorena za pisanje. Nakon ovoga je moguće u tu datoteku zapisati podatke iz varijabli
programa koristeći se objektom fout. Pri otvaranju datoteke za zapisivanje događa se
slijedeće:

1. Datoteka se stvara u mapi u kojoj se nalazi i program. Ukoliko datoteka sa takvim


imenom ne postoji, stvoriti će se nova datoteka. Ukoliko već postoji datoteka sa
takvim imenom, njen sadržaj će se izbrisati. Nakon otvaranja za pisanje, datoteka je
prazna.

2. Upotrebom pripadnog objekta, moguće je pisati u datoteku, ali ne i čitati podatke iz


nje.

DATOTEKE ZA ČITANJE (ULAZNE DATOTEKE)

Način otvaranja datoteke za čitanje je sličan prethodnom načinu. Osnovna razlika je u tome
što se upotrebljava klasa ifstream:

ifstream fin("ULAZ.TXT")

Ovom naredbom je datoteka IZLAZ.TXT otvorena za čitanje i povezana sa objektom fin. Pri
otvaranju datoteke za čitanje, vrijede malo drugačija pravila:

1. Otvara se datoteka koja ima zadani naziv i nalazi se u mapi u kojoj se nalazi i
program. Ukoliko datoteka sa zadanim nazivom ne postoji, datoteka se neće otvoriti.

2. Upotrebom pripadnog objekta moguće je čitati podatke iz datoteke, ali ne i zapisivati


podatke u nju.

ZATVARANJE DATOTEKE

Datoteka se zatvara pozivom metode close objekta koji je povezan sa datotekom. Metoda ne
prima argumente. Npr. prethodno otvorene datoteke moguće je zatvoriti na slijedeći način:

fin.close();
fout.close();
Nakon obavljanja potrebnih operacija nad datotekama (čitanje ili pisanje), datoteke je uvijek
potrebno zatvoriti.

PROVJERA JE LI DATOTEKA USPJEŠNO OTVORENA

Moguće su situacije kada nakon gornjih naredbi datoteka neće biti uspješno otvorena. Pri
otvaranju datoteke za čitanje, to se može dogoditi ukoliko datoteka sa takvim nazivom ne
postoji. Pri otvaranju datoteke za pisanje mogući razlozi su pokušaj stvaranja datoteke na
zaštićenom disku (ili u zaštićenoj mapi), pokušaj stvaranja datoteke na disku na kojem nema
slobodnog prostora itd. Vrlo je važno provjeriti prije same upotrebe datoteke je li ona uspješno
otvorena. To se može napraviti na slijedeći način:

ifstream fin("ULAZ.TXT");

if (!fin) // greška pri otvaranju?


cout << "Datoteka nije otvorena!!\n";
else
{
// ovdje se obavljaju operacije nad datotekom
// upotrebom objekta fin

// datoteka se zatvara samo ako je uspješno otvorena


fin.close();
}

Važno je uočiti da se unutar uvjeta if naredbe nalazi samo !fin, pri čemu je fin naziv objekta
koji je pridružen datoteci ULAZ.TXT. Ovo je moguće zato što klasa ifstream (isto vrijedi i za
klasu ofstream) ima ugrađen operator konverzije u cijeli broj. Taj operator se brine da vrati
vrijednost takvu da ovako napisan if uvjet bude ispunjen samo ako datoteka nije uspješno
otvorena. Provjera se obavlja na isti način za izlazne datoteke.

OBAVLJANJE OSNOVNIH OPERACIJA NAD DATOTEKAMA

Kao što je već ranije spomenuto, osnovne operacije (praktički i jedine) koje se mogu izvoditi
nad datotekama su čitanje i pisanje. U nastavku će biti opisano izvođenje tih operacija.

JEDNOSTAVNO PISANJE U DATOTEKU

Kada program zapisuje u datoteku, on u stvari zapisuje podatke iz svoje memorije.


Zapisivanje podataka u datoteku je vrlo slično zapisivanju podataka na zaslon. Jedina razlika
je što se umjesto cout objekta upotrebljava objekt tipa ofstream koji je pridružen izlaznoj
datoteci. Slijedeći primjer zapisuje sadržaj cjelobrojne varijable u datoteku BROJ.TXT

#include <fstream.h>

main()
{
cout << "Unesite broj: ";

int x;
cin >> x;

ofstream fout("BROJ.TXT");

if (!fout)
cout << "Datoteka nije uspjesno otvorena!\n";
else
{
fout << x << endl; // pisanje u datoteku

fout.close();
}

return 0;
}

Sadržaj datoteke je moguće provjeriti pomoću bilo kojeg tekstualnog editora (npr. Notepad).

Upotrebom operatora << nad objektom koji je pridružen datoteci, moguće je pisati u datoteku
na identičan način kao što se zapisuje na zaslon. Čak će i sam sadržaj "izgledati" kao što bi
izgledao da se na isti način zapisivao na zaslon.

Važno je napomenuti da se ovakvo zapisivanje može obaviti samo pomoću objekta tipa
ofstream (tip za izlazne datoteke). Sam objekt može imati bilo koji naziv (naravno, potrebno je
poštivati pravila o nazivu varijabli u jeziku C++). Posebno je zgodno nazvati objekt fout zbog
očite sličnosti sa objektom cout.

JEDNOSTAVNO ČITANJE IZ DATOTEKE

Da bi program mogao manipulirati sa podacima u datoteci, on ih mora prethodno pročitati iz


datoteke u svoje varijable. Kao i kod pisanja, čitanje iz datoteke je slično učitavanju podataka
sa tipkovnice. Jedina razlika je što se upotrebljava objekt tipa ifstream. Slijedeći program čita
broj koji je prethodni program zapisao u datoteku BROJ.TXT.

#include <fstream.h>

main()
{
ifstream fin("BROJ.TXT");
int x;

if (!fin)
cout << "Datoteka ne postoji!\n";
else
{
fin >> x; // učitavanje iz datoteke u varijablu

fin.close(); // datoteka se može zatvoriti jer smo


// pročitali sve što smo htjeli

cout << "Pročitan je broj: " << x << endl;


}

return 0;
}

Ovakvo učitavanje moguće je jedino sa objektima tipa ifstream. Kod ovakvih datoteka je zbog
sličnosti sa cin objektom zgodno odgovarajući objekt nazvati fin. Važno je uočiti da u ovom
programu korisnik ništa ne unosi: podatak se čita iz datoteke!

Rad prethodna dva programa možete provjeriti tako da prvo pokrenete program koji zapisuje
u datoteku, a zatim program koji učitava iz (te iste) datoteke.
UPOTREBA DATOTEKA U KONKRETNIM PROGRAMIMA

Prethodna dva programa ilustriraju najjednostavniju moguću upotrebu datoteka. Međutim,


podrška datotekama u složenijim programima ne zahtijeva puno više posla. Osnovna ideja je
sažeta u ova dva programa. Već je ranije spomenuto da se datoteke koriste kada program
mora trajno "zapamtiti" podatke koje drži u svojim varijablama. Tipično se u programima ova
mogućnost podržava kroz opciju Save (tj. pohranjivanje, spremanje podataka na disk) te
opciju Load (tj. učitavanje podataka sa diska). Tipični postupak koji je potrebno napraviti pri
izvođenju Save operacije je spremiti sve bitne podatke (recimo sve elemente jednog polja) u
datoteku. Pri izvođenju operacije Load potrebno je sve podatke iz datoteke prebaciti u
odgovarajuće polje. Pri tome je važno obratiti pažnju na način na koji se podaci pohranjuju.
Naime, podaci se moraju učitati na isti onaj način na koji su i pohranjeni. U nastavku će biti
prikazani najjednostavniji načini pohranjivanja elemenata jednog polja:

POSEBNO ZAPISIVANJE BROJA ELEMENATA POLJA

Ranije je spomenute da je pri izvođenju operacije učitavanja potrebno pročitati cjelokupni


sadržaj datoteke u odgovarajuće polje. Postavlja se pitanje kako će program znati koliko
točno podataka ima u datoteci. Ovaj problem se može jednostavno riješiti tako da se prije
zapisivanja samih elemenata polja zapiše broj elemenata. Slijedeći primjer prikazuje kako se
može na taj način zapisati polje. Pretpostavimo da postoje slijedeće globalne varijable:

float ocjene[MAX];
int nocjena;

Također se pretpostavlja da je u main funkciji već napisan dio koda koji popunjava to polje
(ugrađene su opcije dodaj, promijeni i sl.). Slijedi funkcija koja sprema sadržaj u datoteku
OCJENE.TXT:

void spremi()
{
ofstream fout("OCJENE.TXT");

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
fout << nocjena << endl; // zapisivanje broja elemenata

// zapisivanje svakog elementa


for (int i = 0;i < nocjena;i++)
fout << ocjene[i] << endl;

fout.close();
}
}

Pretpostavimo da je funkcija spremi pozvana u trenutku kada se u polju nalaze tri ocjene: 1,2
i 3. Izgled datoteke OCJENE.TXT bi bio slijedeći:

3
1
2
3

Prvo se nalazi broj 3 (čime se označava da slijede tri broja). Nakon toga slijede sami elementi
polja (tj. ocjene 1, 2 i 3).
Uočite da nije slučajno pređeno u novi red svaki put nakon što se zapiše neki podatak. Kada
ne bi bilo tako izgled datoteke za prethodni primjer bi bio:

3123

Program bi pri učitavanju pogrešno pročitao ovakav podatak kao 3123 (umjesto 3, 1, 2, 3).

Učitavanje ovakve datoteke je sada lagano obaviti. Prvo je potrebno učitati broj ocjena n, te
zatim n ocjena:

void ucitaj()
{
ifstream fin("OCJENE.TXT");

if (!fin)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
fin >> nocjena;

// ucitavanje n elemenata
for (int i = 0;i < nocjena;i++)
fin >> ocjene[i];

fin.close();
}
}

Prilikom rada sa datotekama (posebno prilikom čitanja), potrebno je znati da postoji nešto što
se zove trenutna pozicija. Prilikom čitanja, trenutna pozicija (u datoteci) je zapravo mjesto
otkuda će slijedeća naredba čitanja krenuti sa čitanjem podatka. Podatak se čita sve dok se
ne dođe do znaka razmaka, tabulatora ili prelaska u novi red. Pri otvaranju datoteke, trenutna
pozicija je na samom početku datoteke. Nakon svakog čitanja, ona se pomiče prema kraju
datoteke. Zahvaljujući tome, funkcija spremi radi točno onako kako bi i trebala. Nakon što se
pročita broj nocjena, trenutna pozicija u datoteci se pomiče na slijedeći podatak (tj. prvu
ocjenu), nakon toga na drugi podatak itd.

Nakon ovakvog učitavanja, prethodni sadržaj polja je izgubljen tj. podaci iz radne memorije su
zamijenjeni podacima iz datoteke. Naravno, da bi ovakvo učitavanje radilo datoteka
OCJENE.TXT mora postojati, a njen sadržaj mora biti zapisan na način na koji radi funkcija
spremi. Još jednom je važno istaknuti da programer sam određuje na koji način će podaci biti
zapisani pri čemu mora voditi računa o tome da na taj način i učitava podatke iz datoteke.
Posebno je važno pri spremanju podataka paziti da se iza svakog podatka zapiše jedan od
znakova koji označavaju kraj podatka (novi red, razmak ili tabulator tj. "\n", " " ili "\t").

ZAPISIVANJE POLJA BEZ ZAPISIVANJA BROJA ELEMENATA

Pokušaj učitavanja iza kraja datoteke (tj. nakon što su pročitani svi podaci) je moguć. Takav
pokušaj će učitati podata ("učitani" podatak će naravno biti neispravan jer se ne nalazi u
datoteci) i program "neće shvatiti" da nešto nije u redu. Za provjeru je li učitani podatak zaista
u datoteci, može se upotrijebiti metoda eof koja se nalazi u klasi ifstream. Metoda eof vraća
vrijednost različitu od nule (tj. logička istina u jezicima C i C++) ukoliko je pokušano čitanje iza
kraja datoteke. Inače je povratna vrijednost jednaka nuli. Upotrebom te metode, moguće je
prepraviti način zapisivanja podataka u datoteku. Sada više nije potrebno zapisivati broj
elemenata polja, već je dovoljno zapisati samo elemente. Funkcija koja učitava će čitati jedan
po jedan podatak sve dok ne dođe do kraja datoteke (provjeru će obaviti pozivom metode
eof). Slijede funkcije koje na ovaj način zapisuju i učitavaju polje ocjena:
void spremi()
{
ofstream fout("OCJENE.TXT");

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
// zapisivanje svakog elementa
for (int i = 0;i < nocjena;i++)
fout << ocjene[i] << endl;

fout.close();
}
}

void ucitaj()
{
ifstream fin("OCJENE.TXT");

if (!fin)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
nocjena = 0; // početni broj ocjena

// ucitavanje svih elemenata u datoteci


while(1)
{
fin >> ocjene[nocjena];

// je li pokušano čitanje iza kraja datoteke?


if (fin.eof()!= 0)
break; // ako je prekini učitavanje
else
nocjena++;
}

fin.close();
}
}

Funkcija spremi je doživjela minimalne promjene - izbačeno je zapisivanje broja elemenata


(ocjena). Nova verzija funkcije ucitaj je gotovo u cijelosti prepravljena. Prvo, više se ne može
učitati broj koji označava koliko podataka slijedi iza njega (jer taj broj jednostavno više nije
zapisan). Stoga funkcija ucitaj početno postavlja varijablu nocjena na nula. Za svaki uspješno
učitani podatak, ona će povećati iznos u toj varijabli za jedan. Učitavanje se vrši u
beskonačnoj petlji. Nakon svakog učitanog podatka se provjerava je li učitani podatak
ispravan. Ukoliko nije (tj. ukoliko je pokušano čitanje iza kraja datoteke), slijedi izlazak iz
petlje. Ovo možda na prvi pogled izgleda neispravno, ali zaista radi. Naime, kada se prvi put
obavlja učitavanje iza kraja datoteke, neispravan podatak će biti spremljen u polje. Međutim,
taj podatak postaje "stvarni" element polja tek u onom trenutku kada se izvede naredba
nocjena++. Ako nije učitan ispravan podatak, tada se ta naredba neće izvesti i podatak kao
da nije element polja (jer ga niti jedna petlja koja prolazi po svim elementima polja neće uzeti
u obzir).

Nova verzija učitavanja će ispravno učitati podatke iz datoteke samo onda kada su oni
zapisani na način kako je to napravljeno u novoj verziji funkcije spremi.
SPREMANJE I UČITAVANJE OBJEKATA

Spremanje i učitavanje upotrebom operatora << i >> radi za jednostavne tipove podataka (int,
float, stringovi, ...). Ukoliko želimo spremiti ili učitati cijeli objekt tada je potrebno svaki član
objekta zapisati ili učitati posebno. Slijedeći primjer ilustrira spremanje i učitavanje jednog
objekta. Pretpostavimo da je deklariran slijedeći objekt:

class Radnik
{
private:
float placa;
char ime[20], prezime[20];

public:
float get_placa() const;
void set_placa(float n_placa);

const char *get_ime() const;


void set_ime(const char *n_ime);

const char *get_prezime() const;


void set_prezime(const char *n_prezime);
};

Također se pretpostavlja da su deklarirane metode i napisane. Slijede funkcije koje zapisuju i


učitavaju objekt klase Radnik:

void spremi(Radnik rad)


{
ofstream fout("RADNIK.TXT");

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
fout << rad.get_placa() << " ";
fout << rad.get_ime() << " ";
fout << rad.get_prezime() << endl;

fout.close();
}
}

void ucitaj(Radnik &rad)


{
ifstream fin("RADNIK.TXT");

if (!fin)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
char ime[20],prezime[20];
float placa;

fin >> placa;


fin >> ime;
fin >> prezime;

fin.close();
rad.set_placa(placa);
rad.set_ime(ime);
rad.set_prezime(prezime);
}
}

Funkcija spremi zapisuje podatke u datoteku član po član. Pri tome se iza prva dva člana
zapisuje znak razmaka, a iza trećeg znak za prelazak u novi red. Ovo je napravljeno iz čisto
"estetskih" razloga (radi lakšeg čitanja datoteke u tekstualnom editoru). Ne bi bilo razlike (za
funkciju ucitaj) kada bi se iza svakog podatka (tj. člana) zapisao prelazak u novi red.

Funkcija ucitaj mora imati tri privremene lokalne varijable u koje će učitati podatke iz datoteke.
Nakon toga se pozivom pripadnih set metoda, podaci premještaju u objekt. Ovdje je argument
funkcije deklariran kao referenca, kako bi se promjena u objektu (tj. smještanje učitanih
podataka u objekt) "vidjele" i u pozivnoj funkciji.

SPREMANJE I UČITAVANJE POLJA OBJEKATA

Sada je vrlo jednostavno napraviti i spremanje čitavog polja objekata. Funkcije će raditi sa
poljem objekata ranije deklarirane klase Radnik. Pretpostavlja se da su deklarirane globalne
varijable:

Radnik radnici[MAX];
int nradnik;

Također se pretpostavlja da su već ugrađene standardne opcije za rad sa poljima (dodaj,


briši, izmjeni, ... ). Slijede funkcije za spremanje i učitavanje polja radnika:

void spremi()
{
ofstream fout("RADNICI.TXT");

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
// zapisivanje svakog objekta
for (int i = 0;i < nradnik;i++)
{
fout << radnici[i].get_placa() << " ";
fout << radnici[i].get_ime() << " ";
fout << radnici[i].get_prezime() << endl;
}

fout.close();
}
}

void ucitaj()
{
ifstream fin("RADNICI.TXT");

if (!fin)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
nradnik = 0; // početni broj radnika
// učitavanje svih elemenata iz datoteke
while(1)
{
char ime[20],prezime[20];
float placa;

fin >> placa;


fin >> ime;
fin >> prezime;

// je li pokušano čitanje iza kraja datoteke?


if (fin.eof()!= 0)
break; // ako je prekini učitavanje
else // inače popuni objekt i ažuriraj broj radnika
{
radnici[nocjena].set_placa(placa);
radnici[nocjena]set_ime(ime);
radnici[nocjena]set_prezime(prezime);

nradnik++;
}
}

fin.close();
}
}

DODAVANJE NA KRAJ DATOTEKE

Dosadašnji programi koji su zapisivali podatke u datoteku su prilikom pisanja uvijek prebrisali
prethodni sadržaj datoteke. Podsjetimo se, prilikom otvaranja datoteke za pisanje, prethodni
sadržaj datoteke se automatski prebriše. Prilikom otvaranja datoteke za pisanje, moguće je
posebno navesti da se prethodni sadržaj datoteke ne prebriše. Ovo se radi na slijedeći način:

ofstream fout("IZLAZ.TXT",ios::app);

Konstruktoru objekta se šalje još jedan argument (ios::app), čime se određuje da prethodni
sadržaj datoteke neće biti obrisan. Kaže se da se datoteka otvara u tzv. append (dodaj)
modu. Pri tome vrijede slijedeća pravila:

1. Ukoliko datoteka sa navedenim imenom ne postoji, stvoriti će se nova datoteka.

2. Ukoliko datoteka sa navedenim imenom postoji, otvoriti će se postojeća datoteka.

3. Moguće je obavljati samo pisanje u datoteku. Svako zapisivanje će se obaviti na kraju


datoteke (iza svih podataka koji su do tog trenutka zapisani).

BINARNE DATOTEKE

Dosada opisane datoteke su tzv. tekstualne datoteke. Najčešće se koriste kada je u njih
potrebno zapisati podatke na način razumljiv čovjeku. Nedostatak je u tome što veličina
zapisanog podatka ovisi o samom podatku koji se zapisuje. Pretpostavimo da se u neku
tekstualnu datoteku zapisuju samo cijeli brojevi. Recimo da su zapisana 3 broja: 5, 10 i 123.
Izgled datoteke je slijedeći:
5
10
123

Očito je da veličina jednog zapisa (broja) ovisi o samom podatku. Ovo se može pokazati kao
veliki nedostatak. Recimo da se u datoteci nalazi puno brojeva i mi prilikom čitanja želimo
pročitati samo jedan, točno određen broj (npr. 100. po redu u datoteci). Nas dakle zanima
samo 100. broj u datoteci. Da bi smo pročitali taj podatak, moramo prvo pročitati prethodnih
99 brojeva, što je ilustrirano u slijedećoj funkciji:

int procitaj_100() // funkcija koja čita 100. broj iz datoteke


{
ifstream fin("BROJ.TXT");

if (!fin) // ako je pogreška


return -1;
else
{
int x;

for (int i = 0;i < 100;i++)


fin >> x;

return x;
}
}

Ovo često može predstavljati problem. Ukoliko program u datoteci čuva veću količinu
podataka, ovo rješenje se pokazuje sporim. Zbog ovakvog načina pristupa, tekstualne
datoteke se često nazivaju slijedne ili sekvencijalne datoteke.

Ovaj nedostatak je moguće riješiti upotrebom tzv. binarnih datoteka. U binarnu datoteku,
podatak se doslovno zapisuje onako kako ga računalo "vidi" u radnoj memoriji. Npr. tip
unsigned short int zauzima u memoriji dva byte-a. Prilikom zapisivanja jednog broja tipa
unsigned short int, zapisati će se uvijek točno dva byte-a. Drugim riječima, veličina podatka u
datoteci ovisi o tipu tog podatka, a ne o njegovom sadržaju. Sadržaj binarne datoteke više
nije razumljiv čovjeku (tj. ne može se provjeriti pomoću tekstualnog editora), a podaci su
unutra zapisani točno onako kako "izgledaju" u radnoj memoriji.

JEDNOSTAVNO ZAPISIVANJE U BINARNU DATOTEKU

Slijedeći program ilustrira osnovni način kako se može zapisati jedan podatak u binarnu
datoteku:

main()
{
ofstream fout("BROJ.DAT",ios::binary + ios::out);

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
int x = 256;

// zapiši podatak koji je u varijabli x


fout.write((const char *) &x,sizeof(int));

fout.close();
}
return 0;
}

Načelni princip rada ostaje isti: otvaranje, operacije nad datotekom, zatvaranje. Prva razlika je
pri samom otvaranju. Potrebno je poslati dodatni argument konstruktoru čime se navodi da se
datoteka otvara kao binarna i to za pisanje (ios::binary + ios::out). Uočite da se zastavica
ios::out može proslijediti kao argument samo ako je riječ o ofstream klasi.

Osnovna razlika je ipak u samom zapisivanju. Naime, kod binarnih datoteka zapisivanje se
obavlja pomoću metode write. Prvi argument je adresa podatka (tj. varijable) koja se
zapisuje. U našem primjeru adresa podatka je naravno &x. Prije same adrese se uvijek
navodi operator konverzije (const char *). Drugi argument je veličina podatka u byte-ovima.
Taj podatak se može dobiti pomoću operatora sizeof. Naime, sizeof prima kao argument tip
podatka, a vraća veličinu tog tipa u byte-ovima tj. koliko varijabla tog tipa zauzima byte-ova u
memoriji.

Ovdje je važno primjetiti da prvi argument uvijek mora biti adresa varijable. Ukoliko želimo
zapisati neki točno određen broj, moramo ga prvo zapisati u privremenu varijablu, a zatim tu
varijablu zapisati u datoteku (gornji program radi točno tako).

Osnovna pravila ostaju ista: ukoliko datoteka s tim nazivom ne postoji, stvoriti će se nova
datoteka, u suprotnom, sadržaj postojoće datoteke će biti prebrisan.

JEDNOSTAVNO ČITANJE IZ BINARNE DATOTEKE

Čitanje iz binarne datoteke je slično pisanju u binarnu datoteku:

main()
{
ifstream fin("BROJ.DAT",ios::binary + ios::in);

if (!fin)
cout << "Ne mogu otvoriti datoteku!";
else
{
int x;

fin.read((char *) &x,sizeof(int));
fin.close();

cout << x << endl;


}

return 0;
}

Prva razlika (u odnosu na pisanje u binarnu datoteku) je što se konstruktoru proslijeđuje


ios::binary + ios::in (umjesto ios::binary + ios::out). Naravno, zato što je riječ o čitanju, ovdje
se koristi klasa ifstream (umjesto klase ofstream). Samo čitanje je vrlo slično pisanju, uz tu
razliku što se koristi metoda read. Sintaksa je ista. Prvi argument je adresa varijable u koju će
se pročitani podatak smjestiti. Prije same adrese također slijedi operator konverzije. Drugi
argument je veličina podatka u byte-ovima.

Rad prethodna dva programa možete provjeriti tako da prvo pokrenete program koji piše u
datoteku, a zatim program koji čita iz datoteke.
ZAPISIVANJE POLJA U BINARNU DATOTEKU

Način zapisivanja polja se jednostavno dobije iz prethodnih programa. Potrebno je u petlji


zapisivati jedan po jedan element polja. Slijedeći primjer pretpostavlja da su deklarirane
globalne varijable:

float ocjene[MAX];
int nocjena;

Također se pretpostavlja da je u main funkciji već napisan dio koda koji popunjava to polje
(ugrađene su opcije dodaj, promijeni i sl.). Funkcija koja obavlja zapisivanje ima slijedeći
oblik:

void pisi()
{
ofstream fout("OCJENE.DAT",ios::binary + ios::out);

if (!fout)
cout << "Ne mogu otvoriti datoteku!\n";
else
{
for (int i = 0;i < nocjena;i++)
fout.write((const char *) &ocjene[i], sizeof(float));

fout.close();
}
}

Važno je uočiti prilikom pisanja, da se šalje adresa i-tog elementa polja (&ocjene[i]). Drugi
argument je sizeof(float) jer su elementi polja tipa float.

ČITANJE POLJA IZ BINARNE DATOTEKE

Na sličan način se može napraviti i čitanje polja iz binarne datoteke. Potrebno je u petlji čitati
podatak po podatak sve dok se ne dođe do kraja datoteke. Metoda eof radi isto kao i kod
tekstualnih datoteka:

void citaj()
{
ifstream fin("OCJENE.DAT",ios::binary + ios::in);

if (!fin)
cout << "Ne mogu otvoriti datoteku!";
else
{
nocjena = 0;

while(1)
{
fin.read((char *) &ocjene[nocjena],sizeof(float));

if (fin.eof() != 0)
break;
else
nocjena++;

}
fin.close();
}
}

Princip je očito isti kao i kod čitanja polja iz tekstualne datoteke. Čita se jedan po jedan
podatak, pri čemu se nakon svakog podatka provjerava je li ispravno pročitan (tj. jesmo li
došli do kraja datoteke).

PISANJE I ČITANJE OBJEKATA U BINARNE DATOTEKE

Pisanje i čitanje objekata u binarne datoteke je puno jednostavnije u odnosu na tekstualne


datoteke. Budući da metode za pisanje i čitanje traže adresu elementa i njegovu veličinu u
byte-ovima, objekt se doslovno može zapisati (ili pročitati) jednim pozivom metode write (ili
read). U prethodnoj funkciji za pisanje, dovoljno je napraviti slijedeću promjenu da bi se
mogao zapisati objekt klase Radnik:

for (int i = 0;i < nradnik;i++)


fout.write((const char *) &radnici[i], sizeof(Radnik));

Uočite da je prvi argument adresa objekta klase Radnik, dok je drugi argument veličina
jednog objekta te klase.

Slično se može obaviti i čitanje polja objekata klase Radnik iz ovako zapisane datoteke:

nradnik = 0;

while(1)
{
fin.read((char *) &radnici[nracnik],sizeof(Radnik));

if (fin.eof() != 0)
break;
else
nradnik++;

Važno je napomenuti da je ovakvo pisanje i čitanje objekta moguće samo ako objekti ne
sadrže pokazivačke članove ili reference.

DIREKTAN PRISTUP PODACIMA

Sada se možemo pozabaviti ključnom prednosti binarnih datoteka. Već je ranije spomenuto
da je nedostatak tekstualnih datoteka u tome što ne omogućava direktan pristup podatku. U
binarnim datotekama je to moguće jer su svi podaci jednake veličine. Npr. ukoliko u datoteku
upisujemo objekte klase Radnik, tada je svaki podataka veličine sizeof(Radnik). Drugim
riječima moguće je odrediti za određeni podatak koliko byte-ova je u datoteci zapisano prije
njega. Npr. prije 100. objekta klase Radnik je u datoteku zapisano 99 objekata što je zapravo
99 * sizeof(Radnik) byteova. Općenito se može reći da je prije i-tog podatka zapisano (i - 1) *
sizeof(tip) byteova. Pri tome tip označava tip podatka koji se zapisuje u datoteku. Ovdje je
pretpostavljeno brojanje od jedan (podatak na početku datoteke je 1. element a ne 0.).
Zahvaljujući tom strogo matematičkom odnosu, moguće je lako direktno pristupiti točno
određenom podatku.
SEEKG METODA

Ovom metodom moguće je promijeniti trenutnu poziciju datoteke. Podsjetimo se, trenutna
pozicija je ona pozicija otkuda će krenuti slijedeća naredba čitanja. Metoda prima jedan
argument - novu poziciju (broj byte-ova od početka datoteke). Slijedeća funkcija učitava 100.
objekt iz datoteke RADNICI.DAT u koju su zapisani objekti klase Radnik:

void citaj_100()
{
ifstream fin("RADNICI.DAT",ios::binary + ios::in);

if (!fin)
cout << "Ne mogu otvoriti datoteku!";
else
{
Radnik x;

// pozicioniraj se na 99. objekt


fin.seekg(99 * sizeof(Radnik));

fin.read((char *) &x,sizeof(Radnik));
fin.close();

// sada je moguće ispisati članove objekta


}
}

Uočite kako se do 100. objekta dolazi u dvije naredbe. Prva naredba je "skok" na 100.
poziciju, nakon čega slijedi učitavanje samo jednog (100.) objekta.

ODREĐIVANJE VELIČINE DATOTEKE

Što se događa ukoliko pokušamo postaviti trenutnu poziciju iza kraja datoteke? Pozicija se
neće promijeniti, ali nećemo dobiti dojavu o pogrešci. Ovo je moguće zaobići ako se uvede
kontrola pozicije. Prije same promjene pozicije, potrebno je vidjeti je li nova pozicija ispravna
(tj. nalazi li se nova pozicija u datoteci). Npr. za datoteku RADNICI.DAT možemo odrediti
koliko objekata je zapisano u njoj, te paziti da nikad ne mijenjamo poziciju iza zadnjeg
objekta. Da bi odredili broj objekata u datoteci, moramo odrediti veličinu datoteke te taj broj
podijeliti sa veličinom jednog podatka (tj. sa veličinom jednog objekta).

Veličina datoteke se određuje u dva koraka. Prvo, potrebno je postaviti trenutnu poziciju na
sam kraj datoteke, što se radi posebnim pozivom metode seekg:

fin.seekg(0,ios::end);

Nakon ove naredbe, trenutna pozicija je na samom kraju datoteke. Sada se može pozvati
tellg metoda. Ta metoda vraća trenutnu poziciju (broj byte-ova od početka datoteke):

long velicina = fin.tellg();

Ovime smo dobili veličinu datoteke u byte-ovima. Još je jedino potrebno izračunati koliko je
objekata zapisano u datoteci:

long nradnik = velicina / sizeof(Radnik);


Naravno, ovaj primjer radi za datoteku u kojoj su smješteni objekti klase Radnik. Slijedi
cjelovita funkcija koja računa koliko je objekata klase Radnik smješteno u datoteku:

long br_radnika()
{
ifstream fin("RADNICI.DAT",ios::binary + ios::in);

if (!fin) // ako je pogreška


return -1;
else
{
fin.seekg(0,ios::end);
long rez = fin.tellg() / sizeof(Radnik);
fin.close();

return rez;
}
}

ČUVANJE PODATAKA UZ MINIMALNI UTROŠAK RADNE MEMORIJE

Upotrebom binarnih datoteka, posebno tehnike direktnog pristupa, moguće je smanjiti utrošak
radne memorije. Pretpostavimo da radimo program za evidenciju zaposlenih u nekoj firmi.
Ukoliko je riječ o velikoj firmi, broj radnika može biti prilično velik te je moguće da jednostavno
nemamo dovoljno prostora u radnoj memoriji za tako veliko polje (podsjetimo se da je prostor
radne memorije relativno mali). Moguće rješenje je upotreba datoteke za čuvanje svih
podataka. Svi objekti (u ovom slučaju radnici) se nalaze u datoteci, a u radnoj memoriji
čuvamo samo podatke o radniku čije podatke trenutno obrađujemo (npr. pri ispisu, promjeni ili
sl.). U slijedećim koracima će biti objašnjeno kako se može realizirati ovakav program.

ORGANIZACIJA PODATAKA

Podaci će biti organizirani u objektima klase Radnik. Svi podaci će biti pohranjeni u datoteci
RADNICI.DAT, a u radnoj memoriji će se uvijek nalaziti najviše jedan objekt (onaj koji se
trenutno obrađuje). Slijedi deklaracija klase radnik:

class Radnik
{
private:
float placa;
char ime[20], prezime[20];
public:
float get_placa() const {return placa;};
void set_placa(float n_pl) {placa = n_pl;};

const char *get_ime() const {return ime;};


void set_ime(const char *n_ime) {strcpy(ime,n_ime);};

const char *get_prezime() const {return prezime;};


void set_prezime(const char *n_prez)
{strcpy(prezime,n_prez);};

void Ispisi() // metoda koja ispisuje podatke na zaslon


{
cout << ime << " " << prezime << " " << placa << endl;
};
};

Ovo je pojednostavljeni oblik klase radnik. Pri tome je u klasu radi jednostavnosti ugrađena
metoda ispisi koja na zaslon ispisuje sve podatkovne članove.

DODAVANJE NOVOG ELEMENTA

Dodavanje novog člana je vrlo jednostavno za realizirati. Potrebno je prvo učitati podatke sa
tipkovnice te popuniti jedan objekt klase Radnik. Nakon toga taj objekt je potrebno dodati na
kraj datoteke. Zadnji korak se vrlo jednostavno može napraviti ukoliko se datoteka otvori za
dodavanje. Sjetimo se, pri otvaranju datoteke za dodavanje, ukoliko ta datoteka ne postoji,
stvoriti će se nova datoteka. Ukoliko ta datoteka već postoji, njen sadržaj će ostati očuvan, a
svaki novi upis u datoteku će biti dodan na kraj datoteke:

void dodaj_novi()
{
char ime[20],prezime[20];
float pl;

cout << "\n\n\n";

cout << "Ime: "; cin >> ime;


cout << "Prezime: "; cin >> prezime;
cout << "Placa: "; cin >> pl;

Radnik pom;

pom.set_placa(pl);
pom.set_ime(ime);
pom.set_prezime(prezime);

// otvori datoteku za dodavanje


ofstream fout("RADNICI.DAT",ios::binary + ios::out +
ios::app);

if (!fout)
cout << "Greska! Ne mogu otvoriti datoteku!\n";
else
{
// zapisi novi objekt na kraj datoteke
fout.write((const char *) &pom, sizeof(Radnik));

fout.close();

cout << "Radnik je uspjesno dodan!\n";


}
}

Prilikom otvaranja, drugi argument je ios::binary + ios::out + ios::app. Ovo se čita na slijedeći
način: otvori datoteku za pisanje kao binarnu, novi podaci će se dodati na kraj datoteke (klasa
naravno mora biti ofstream).

ISPIS SVIH ELEMENATA U DATOTECI

Ovo je već ranije obrađeno. Dovoljno je čitati jedan po jedan objekt iz datoteke sve dok se ne
dođe do samog kraja:
void ispisi_sve()
{
cout << "\n\n\n";

ifstream fin("RADNICI.DAT", ios::binary + ios::in);

if (!fin)
cout << "Greska! Ne mogu otvoriti datoteku!\n";
else
{
while(1)
{
Radnik pom;

fin.read((char *) &pom, sizeof(Radnik));

if (fin.eof() != 0)
break;
else
pom.Ispisi();
}

fin.close();
}
}

RAČUNANJE BROJA OBJEKATA U DATOTECI

Broj objekata u datoteci se računa prema ranije objašnjenom principu. Prvo je potrebno
"skočiti" na kraj datoteke, pročitati trenutnu poziciju (tj. veličinu datoteke u byte-ovima) te
podijeliti sa veličinom jednog zapisa (objekta klase Radnik):

unsigned long br_obj()


{
ifstream fin("RADNICI.DAT", ios::binary + ios::in);

if (!fin)
return 0;
else
{
// skoci na kraj
fin.seekg(0,ios::end);

// racunanje broja objekata


unsigned long rez = fin.tellg() / sizeof(Radnik);

fin.close();

return rez;
}
}
ISPIS ODREĐENOG ELEMENTA

Ovime se omogućuje korisniku da unese redni broj radnika te da se na zaslonu pojave


njegovi podaci. Nakon što korisnik unese redni broj, potrebno je provjeriti je li uneseni broj
ispravan. Broj ne smije biti manji od 1 niti veći od broja upisanih radnika u datoteci. Ukoliko je
broj ispravan, potrebno je na ranije objašnjeni način "skočiti" na traženi zapis, pročitati dotični
objekt te ispisati podatke na zaslon:

void ispisi_jednog()
{
unsigned long n;

n = br_obj(); // broj objekata u datoteci

unsigned long x;

cout << "\n\n\n";

cout << "Unesite redni broj radnika: ";


cin >> x;

if (x > n)
cout << "Nema toliko radnika u bazi!\n";
else if (x < 1)
cout << "Pogresan unos!\n";
else
{
ifstream fin("RADNICI.DAT", ios::binary + ios::in);

if (!fin)
cout << "Greska! Ne mogu otvoriti datoteku!\n";
else
{
Radnik pom;

// "skoci" na trazenu poziciju


fin.seekg((x - 1) * sizeof(Radnik));

// procitaj podatak
fin.read((char *) &pom,sizeof(Radnik));

cout << "\n";


pom.Ispisi();

fin.close();
}
}
}

PROMJENA PODATAKA ZA JEDAN ELEMENT

Prije obavljanja promjene, potrebno je usvojiti još par novih tehnika rada sa datotekama. Ideja
same promjene je da se prvo pročita podatak koji se mijenja, nakon toga se ispišu trenutne
vrijednosti za taj podatak (u ovom primjeru se ispisuju trenutni podaci radnika), korisnik unosi
nove podatke te se novi podaci prepisuju preko starih. Datoteku je potrebno istovremeno
otvoriti za čitanje i za pisanje. U tu svrhu se koristi klasa fstream:

fstream f("RADNICI.DAT", ios::binary + ios::in + ios::out +


ios::nocreate);

Drugi argument se čita na slijedeći način: datoteka se otvara kao binarna, za čitanje i pisanje,
ukoliko datoteka ne postoji neće se stvoriti nova datoteka, a ukoliko datoteka postoji njen
sadržaj neće biti prebrisan. Sada je moguće obavljati i operacije čitanja i operacije pisanja
nad datotekom. Načelno se postupak promjene podataka svodi na slijedeće korake:

1. Učitaj redni broj traženog radnika.


2. "Skoči" na traženu poziciju, pročitaj podatke u radnu memoriju, ispiši podatke na
zaslon.
3. Učitaj nove podatke te popuni objekt sa njima.
4. Skoči na traženu poziciju i zapiši nove podatke preko starih.

U koraku 4. potrebno je promijeniti trenutnu poziciju na koju se zapisuje podatak. To se


postiže pozivom metode seekp koja radi na isti način kao i metoda seekg. Konkretno se ovaj
postupak može napraviti na slijedeći način:

void promjena_radnika()
{
unsigned long n;

n = br_obj();

unsigned long x;

cout << "\n\n\n";


cout << "Unesite redni broj radnika: ";

cin >> x;

if (x > n)
cout << "Nema toliko radnika u bazi!\n";
else if (x < 1)
cout << "Pogresan unos!\n";
else
{
fstream f("RADNICI.DAT", ios::binary + ios::in +
ios::out + ios::nocreate);

if (!f)
cout << "Greska! Ne mogu otvoriti datoteku!\n";
else
{
// citanje podataka
Radnik pom;

f.seekg((x - 1) * sizeof(Radnik));
f.read((char *) &pom,sizeof(Radnik));

cout << "\nPodaci o radniku:\n\n";


pom.Ispisi();

// unos novih podataka


char ime[20],prezime[20];
float pl;
cout << "Unesite ime: ";
cin >> ime;

cout << "Unesite prezime: ";


cin >> prezime;

cout << "Unesite placu: ";


cin >> pl;

pom.set_placa(pl);
pom.set_ime(ime);
pom.set_prezime(prezime);

// na mjesto starih podataka dolaze novi podaci


f.seekp((x - 1) * sizeof(Radnik));
f.write((const char *) &pom,sizeof(Radnik));

f.close();
}
}
}

GLAVNI PROGRAM

Sada je moguće napraviti jednostavni glavni program koji povezuje ove funkcije u cjelinu:

void nacrtaj_izbornik()
{
cout << "\n\n\n";
cout << "1. Dodavanje novog elementa\n";
cout << "2. Ispis svih elemenata\n";
cout << "3. Ispis tocno jednog radnika\n";
cout << "4. Promjena podataka radnika\n";
cout << "X. Kraj rada\n\n";
cout << "Odabir: ";
}

main()
{
while(1)
{
nacrtaj_izbornik();

char ch;
cin >> ch;
if (ch == 'X' || ch == 'x')
break;

switch(ch)
{
case '1':
dodaj_novi();
break;

case '2':
ispisi_sve();
break;

case '3':
ispisi_jednog();
break;

case '4':
promjena_radnika();
break;
}
}
return 0;
}
DATOTEKE.............................................................................................................................................1
OSNOVE RADA S DATOTEKAMA............................................................................................1
OTVARANJE DATOTEKE..................................................................................................2
DATOTEKE ZA PISANJE (IZLAZNE DATOTEKE)............................................2
DATOTEKE ZA ČITANJE (ULAZNE DATOTEKE)............................................2
ZATVARANJE DATOTEKE...................................................................................2
PROVJERA JE LI DATOTEKA USPJEŠNO OTVORENA..................................3
OBAVLJANJE OSNOVNIH OPERACIJA NAD DATOTEKAMA.............................................3
JEDNOSTAVNO PISANJE U DATOTEKU........................................................................3
JEDNOSTAVNO ČITANJE IZ DATOTEKE........................................................................4
UPOTREBA DATOTEKA U KONKRETNIM PROGRAMIMA.................................................5
POSEBNO ZAPISIVANJE BROJA ELEMENATA POLJA..................................................5
ZAPISIVANJE POLJA BEZ ZAPISIVANJA BROJA ELEMENATA....................................6
SPREMANJE I UČITAVANJE OBJEKATA..................................................................................8
SPREMANJE I UČITAVANJE POLJA OBJEKATA............................................................9
DODAVANJE NA KRAJ DATOTEKE........................................................................................10
BINARNE DATOTEKE...............................................................................................................10
JEDNOSTAVNO ZAPISIVANJE U BINARNU DATOTEKU.............................................11
JEDNOSTAVNO ČITANJE IZ BINARNE DATOTEKE....................................................12
ZAPISIVANJE POLJA U BINARNU DATOTEKU...........................................................13
ČITANJE POLJA IZ BINARNE DATOTEKE....................................................................13
PISANJE I ČITANJE OBJEKATA U BINARNE DATOTEKE...........................................14
DIREKTAN PRISTUP PODACIMA...........................................................................................14
SEEKG METODA.............................................................................................................15
ODREĐIVANJE VELIČINE DATOTEKE.........................................................................15
ČUVANJE PODATAKA UZ MINIMALNI UTROŠAK RADNE MEMORIJE.........................16
ORGANIZACIJA PODATAKA..........................................................................................16
DODAVANJE NOVOG ELEMENTA.................................................................................17
ISPIS SVIH ELEMENATA U DATOTECI.........................................................................17
RAČUNANJE BROJA OBJEKATA U DATOTECI............................................................18
ISPIS ODREĐENOG ELEMENTA...................................................................................19
PROMJENA PODATAKA ZA JEDAN ELEMENT............................................................19
GLAVNI PROGRAM.........................................................................................................21

You might also like