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 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 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 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 << else { fout << fout << fout <<
"Ne mogu otvoriti datoteku!\n"; rad.get_placa() << " "; rad.get_ime() << " "; 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: "; cout << "Prezime: "; cout << "Placa: ";
cin >> ime; cin >> prezime; 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 << else if (x cout << else { fstream
"Nema toliko radnika u bazi!\n"; < 1) "Pogresan unos!\n"; 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