Breviar 12 - Introducere fişiere 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.

typedef struct Elev{
        short int nota1;
        short int nota2;
        char nume[10];
        float medie;
} Elev;
 
[ ... main() şi alte funcţii ...]
 
Elev elev;
elev.nota1 = 7;
elev.nota2 = 10;
elev.medie = (elev.nota1 + elev.nota2) / 2.0;
strcpy(elev.nume, "Cartman");

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:
     fprintf(t, "%d %d %f %s\n", elev.nota1, elev.nota2, elev.medie, elev.nume); 

Î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: <code c> fwrite(&elev, sizeof(Elev), 1, b); </code>

Î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ţie Fişiere binare Fişiere text
citire
scriere
adaugare
“rb”
“wb”
“ab”
“r”
“w”
“a”

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

 size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); 

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ă:

 size_t fwrite ( void * ptr, size_t size, size_t count, FILE * stream ); 

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:

#ifndef __STDLIB__
#define __STDLIB__
 
#include <stdlib.h>
 
#endif /* __STDLIB__ */

Î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).

programare-cc/breviar-12.txt · Last modified: 2023/01/12 21:47 by carmen.odubasteanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0