Fişiere binare. Compilarea din mai multe surse.

În acest laborator veţi învăţa să:

  • Folosiţi corect fişiere binare.
  • Folosiţi funcţii de citire şi scriere la nivel de octet.
  • Creaţi proiecte şi să compilaţi executabile din mai multe surse (introducere).

Introducere despre fişierele binare. Comparaţie cu fişierele text.

Până în acest moment, când am folosit expresiile "scriere din fişier" sau "citire din fişier", ne-am referit în mod exclusiv la fişiere de tip text.

Practic, fişierele text conţin informaţia stocată sub forma unui şir de caractere (eventual, pe mai multe linii, dar ştim deja că sfârşitul de linie este şi el tot un caracter).

Spre deosebire de ele, fişierele binare stochează informaţia brut, fără prelucrări exact aşa cum apare ea în memorie (puteţi să va imaginaţi că se face fotografia unei porţiuni din memoria RAM, şi se scrie în fişier Byte cu Byte, astfel încât poate fi restaurată mai târziu printr-o simplă copiere înapoi în RAM).

Ca de obicei, ambele metode de stocare au avantaje şi dezavantaje care indică folosirea uneia sau a celeilalte în funcţie de aplicaţie:

  • Stocarea sub formă text are ca principal avantaj formatul human readable al informaţiei. Asta înseamnă că oricând putem să deschidem fişierul într-un editor şi putem interpreta ce scrie în el direct, fără a mai avea nevoie de o altă aplicaţie. (Exemplu: sursele de C). Dezavantajul este că informaţia text ocupă mai mult decât în formă binară, şi este greu de prelucrat de către programe.
  • Stocarea sub formă binară are ca avantaje faptul că datele ocupă în medie (nu mereu) mai puţină memorie decât cele în format text, au structură previzibilă, dar cel mai important, pot fi încărcate direct în memorie. (Exemplu: un binar executabil, care este de fapt imaginea din care este lansat un proces în execuţie). Dezavantajul constă în faptul că ele devin complet neinteligibile pentru oameni (trebuie să le interpretăm cu o altă aplicaţie pentru a le înţelege).

Pentru că o imagine valorează cât o mie de cuvinte, vom ilustra grafic cum are loc reprezentarea efectivă în memorie a informaţiilor în format text şi binar. Fie următoarea structură de date care conţine informaţii despre un elev.

Să considerăm că avem două fişiere:

  • FILE* t = fopen("text.out","w");, în care scriem conţinutul acestei structuri în format text (aşa cum am făcut până acum). Un exemplu de stocare este dat de următorul apel:

În realitate, fişierul arată în memorie astfel:

  1. În primul rând se observă că numerele nu se reprezintă pe un număr previzibil de caractere (ex: 1.1, 10.1, sau 10.333333333) care să depindă de tipul de dată.
  2. Pe de altă parte, trebuie să introducem caractere speciale de separaţie, ca să ne putem da seama unde se termină un număr şi unde începe un altul.
  3. Cel mai important însă, dacă am vrea să citim aceste date din fişier, trebuie să interpretăm din nou şirul de caractere, să îl tăiem în segmente (după spaţii) şi să transformăm bucăţile individuale de şir în numere, unde este cazul. Nu vă lăsaţi păcăliţi ca în loc de strtok() şi atoi() există scanf(). Cineva tot trebuie să facă munca asta, deci apelând scanf() de fapt în spate se desfăşoară mult efort, care costă timp!
  • FILE* b = fopen("binar.out","wb");, în care scriem conţinutul acestei structuri în format binar astfel:

În realitate, fişierul arată în memorie astfel:

  1. În acest caz, datele se reprezintă pe un număr cunoscut de Bytes (mai exact, dimensiunea tipului de dată). După cum se vede din exemplu, asta nu înseamnă mereu că se ocupă mai puţină memorie. Cu toate acestea, în mod statistic, datele binare ocupă mai puţină memorie!
  2. O problemă este că dacă am vrea să deschidem fişierul pentru a citi aceste date, nu s-ar înţelege mai nimic, pentru că orice editor ar încerca să transforme fiecare Byte într-un caracter pe care să îl afişeze pe ecran. Evident, nu se obţine ceea ce ne-am dori noi sa vedem. Avem nevoie de un program care să interpreteze fişierul şi să ne arate conţinutul din el.
  3. Din nou, cel mai important este că dacă am vrea să citim aceste date din fişier, le putem încărca direct la adresa unei structuri de tip Elev. În realitate, aceste date nu sunt cu nimic mai mult decât o "fotografie" a unei porţiuni din RAM, astfel încât citirea este de fapt o simplă copiere.

Funcţii de citire şi scriere la nivel de octet:

Primul lucru pe care trebuie să îl facem pentru a putea folosi un fişier este să îl deschidem. În acest sens, lucrurile stau foarte simplu: trebuie doar să adaugăm "b" la şirul care specifică modul de deschidere al unui fişier în funcţia fopen().

SemnificaţieFişiere binareFişiere text
citire"rb""r"
scriere"wb""w"
adăugare"ab""a"

Pentru citire la nivel de octet se foloseşte funcţia fread() definită în headerul <stdio.h>, care are următoarea sintaxă:

Semnificaţia argumentelor este următoarea:

  • void ptr* este un pointer către o zonă de memorie unde se va face citirea
  • size_t size reprezintă dimensiunea în octeţi a unui element citit
  • size_t count reprezintă numărul de elemente citite. Elementele vor fi depuse în locaţii consecutive începând de la adresa ptr din memorie
  • FILE* stream reprezintă fluxul (fişierul) din care se face citirea

Pentru scriere la nivel de octet se foloseşte funcţia fwrite() definită în headerul <stdio.h>, care are următoarea sintaxă:

Semnificaţia argumentelor este următoarea:

  • void* ptr este un pointer către o zonă de memorie unde se vor prelua datele ce trebuie scrise
  • size_t size reprezintă dimensiunea în octeţi a unui element scris
  • size_t count reprezintă numărul de elemente scrise. Elementele vor fi preluate pentru scriere din locaţii consecutive începând de la adresa ptr din memorie
  • FILE* stream reprezintă fluxul (fişierul) în care se face scrierea

Nu în ultimul rând, fişierele binare au o proprietate interesantă: Am spus că spaţiul ocupat de diverse articole depinde exclusiv de tipul lor de dată. Cu alte cuvinte, dacă am vrea să citim al 101-lea număr întreg dintr-un fişier binar care conţine doar numere întregi, ştim sigur că acest număr ocupă Bytes-ii cu numerele 400, 401, 402 şi 403 din fişier. (Presupunând că sizeof(int)==4. Numerele se schimbă dacă avem alte arhitecturi cu alte tipuri de date.)

Ar fi foarte convenabil dacă am putea sări peste restul fişierului direct la acea locaţie de memorie. În realitate, acest lucru este posibil. Pentru a rezolva această problemă, C-ul pune la dispoziţie două funcţii:

  • long int ftell ( FILE * fisier ), întoarce o valoare care reprezintă poziţia curentă în fişier.
  • int fseek ( FILE * fisier, long int deplasament, long int fata_de_acest_punct ), sare în fişierul fisier la poziţia obţinută prin suma celor doi parametri întregi. Deplasamentul reprezintă un număr de Bytes peste care se sare, iar punctul faţa de care se sare poate fi una din constantele:
    • SEEK_SET, începutul fişierului
    • SEEK_CUR, poziţia curentă în fişier (salt autorelativ)
    • SEEK_END, poziţia finală din fişier (nu adunaţi valori pozitive)

Includerea fişierelor. Gărzi de includere multiplă.

În aplicaţiile mari, în mod normal modulele diferite de program se implementează în fişiere separate, urmând a fi necesară compilarea executabilului final din mai multe surse. Includerea unui fişier sursă în alt fişier sursă se face cu ajutorul directivei de preprocesare # include care este urmată de numele fişierului ce trebuie inclus. Distingem două cazuri:

  • <nume_header> specifică un fişier header standard. Compilatorul se aşteaptă să găsească un astfel de fişier într-un director anume care conţine biblioteci standard
  • "nume_header" specifică un fişier header definit de utilizator. Compilatorul se aşteaptă să găasească un astfel de fişier în directorul curent al proiectului.

Trebuie să mai specificăm aici următoarea problemă. Este posibil să implementăm de exemplu definiţia unei structuri de date într-un fişier header, şi apoi să scriem în fişiere separate funcţii ce operează pe acea structură de date. Evident, funcţiile definite vor trebui să includă la rândul lor fişierul de definire al structurii de date. Dar fişierul care conţine funcţia main(), de exemplu, trebuie să includă toate fişierele care implementează funcţii, ceea ce ar însemna că fişierul de definire al structurii de date este inclus de mai multe ori. Acest lucru trebuie întotdeauna evitat prin protejarea clauzelor de includere astfel:

Înainte de a se include pentru prima dată <stdlib.h>, numele __STDLIB__ nu este definit, ceea ce permite includerea headerului. Încercările ulterioare de a include fişierul header vor eşua (ne dorim acest lucru deoarece per ansamblu nu dorim să includem headerul decât o singură dată în program).

Exerciţii laborator

Problema 1

Să se definească o structură Produs cu următoarele date despre un produs:

  • nume_produs - un şir de maxim 20 caractere
  • cantitate - un număr întreg
  • pret_produs - un număr real

Să se implementeze definiţia structurii într-un fişier numit Produs.h şi o funcţie main(), într-un fişier numit main.c.

Funcţia main va primi argumente din linia de comandă şi va trebui să importe tipul de dată Produs din fişierul extern (folosiţi directiva #include).

ATENŢIE! Definiţia tipului de date, includerea bibliotecilor precum şi antetele funcţiilor care se vor defini ulterior vor fi grupate în fişierul header Produs.h, iar implementarea efectiva a functiilor in fisierul main.c!

Problema 2. (9-11 linii)

Scrieţi o funcţie pentru crearea unui fişier binar care să conţină 100 de produse cu date generate aleator astfel:

  • Numele produsului este de forma ProdusX unde X este un număr generat aleator (convertit în şir de caractere)
  • Cantitatea produsului este X % 100
  • Preţul produsului se obţine cu relaţia X <= 100 ? X : X % 100

În fişier se va scrie câte un articol de tip Produs cu ajutorul funcţiei fwrite() (nu pe componente).

Hints:

  • Folosiţi funcţia rand() pentru a genera un număr aleator între 0 şi RAND_MAX
  • Puteţi transforma uşor un întreg într-un şir folosind:

trimite ieşirea formatată în şirul str.

Funcţia poate avea următorul antet:

Problema 3. (7-10 linii)

Scrieţi o funcţie pentru afişarea pe ecran a fişierului creat anterior, câte un articol pe o linie.

Funcţia poate avea următorul antent:

Problema 4. (10-12 linii)

Scrieţi o funcţie pentru sortarea articolelor din fişier crescător după nume.

Hint-uri:

  • Aduceţi fişierul în RAM (cititi datele intr-un vector), aplicaţi sortarea şi apoi suprascrieţi fostul conţinut.
  • Apelaţi funcţiile scrise anterior
  • Pentru sortare puteţi folosi BubbleSort sau altă metodă de sortare!

Problema 5.

Scrieţi o funcţie pentru citirea unui nume de produs de la tastatură, căutarea produsului cu acel nume in fisier şi afişarea datelor despre produsul găsit. Dacă nu există un produs cu acel nume atunci se afişează un mesaj corespunzător.

Problema 6.

Scrieţi o funcţie pentru citirea unui nume de produs de la tastatură, căutarea produsului cu acel nume in fisier şi afişarea datelor despre produsul găsit. După afişare se introduce o linie cu alte valori pentru cele 3 câmpuri, care vor înlocui in fişier valorile corespunzatoare produsului respectiv.
Dacă nu există un produs cu acel nume atunci se afişează un mesaj corespunzător.
Atenţie! Nu se va suprascrie tot fişierul!

Hint:

  • Pentru căutare citiţi rând pe rând produsele, iar în momentul în care ajungeţi la produsul dorit îl modificaţi direct în fişier!

Problema 7.

Completaţi următorul schelet de cod astfel încât să realizeze salvarea şi apoi restaurarea a 4 structuri care contin siruri alocate dinamic. În fişierul binar NU aveţi voie să scrieţi şi terminatorul de şir (caracterul \0). În fişier va trebui sa precedeţi fiecare şir cu 4 Bytes în care să se regăsească dimensiunea şirului stocat de la acel punct încolo.

Procesul de salvare al unui obiect într-un fişier este uneori mai complicat decât o simplă copiere. Dacă obiectul conţine membrii alocaţi dinamic, atunci nu mai este suficientă o scriere de tip "shallow" (simplă copiere), deoarece se pierde informaţia din obiectele alocate dinamic.

ATENTIE! NU scrieţi niciodată, sub nici o formă pointeri în fişiere! Aceasta este o greşeală de logică foarte gravă!!!

De ce? Un pointer reprezintă o adresă către o zonă din memoria RAM pe care sistemul de operare o oferă procesului pe timpul rulării acestuia. Fişierele supravieţuiesc de la o rulare la alta a programului.

Astfel, la următoarea rulare, nu numai ca probabil că zona respectivă de memorie ajunge între timp în posesia altui proces, dar nici măcar datele nu mai exista fizic acolo, deoarece sunt şanse foarte mari să fi fost suprascrise între timp (memoria RAM este extrem de intens utilizată). Este foarte important să cereţi informaţii suplimentare dacă nu vă este foarte clar de ce nu ar funcţiona!

În concluzie, în astfel de cazuri, trebuie să "aplatizăm" structura, adică să scriem în fişier absolut toate datele referenţiate de aceasta prin pointeri, iar la restaurare să realocăm toată memoria şi să refacem pointerii. Procesul de salvare al unei structuri în formă binară se numeşte serializare, iar reconstruirea acestuia din formă binară se numeşte deserializare.