Table of Contents

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:

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:

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

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:

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:

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:

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