Programarea Calculatoarelor, seria CC

Laborator 13

Fişiere binare. Compilarea din mai multe surse

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



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:

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");

Sa considerăm că avem două fișiere:


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

Câteva exemple:
Semnificație Fișiere binare Fiș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ă:

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


Problema1.

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

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 într-un fişier header!


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:

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

Hints:


Problema 3. (7-10 linii)

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


Problema 4. (10-12 linii)

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

Hint-uri:


Problema 5.

Scrieți o funcție pentru citirea unui număr întreg X de la tastatură, căutarea produsului cu nume ProdusX şi afişarea datelor despre produsul găsit. După afişare se poate introduce o linie cu alte valori pentru cele 3 câmpuri, care vor înlocui in fişier valorile afişate (o linie goală nu modifică nimic în fişier).

Dacă nu există un produs cu acel nume atunci se afişează un mesaj corespunzător.

Hint:


Problema 6.

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.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define N 4

typedef struct Persoana{
        char* nume;
        char* prenume;
} Persoana;

void serializeaza(Persoana v[], int nrPersoane, char* fileName)
{
        FILE* f = fopen(fileName, "wb");
        
        //TODO! Adauga codul de serializare: ~10 linii

        fclose(f);
}

void deserializeaza(Persoana v[], int nrPersoane, char* fileName)
{
        FILE* f = fopen(fileName, "rb");

        //TODO! Adauga codul de deserializare: ~10 linii

        fclose(f);
}

int main()
{
        Persoana v[N],w[N];
        char* prenume[N] = { "Eric", "Kyle", "Stan", "Kenny" };
        char* nume[N] = { "Cartman", "Broflovski", "Marsh", "McCormick" };
        int i;
        for (i = 0; i < N; i++){
                v[i].nume = nume[i];
                v[i].prenume = prenume[i];
        }
        
        // Serializam vectorul intr-un fisier
        serializeaza(v, N, "persoane.bin");
        // Deserializam in alt vector, din acelasi fisier. Ar trebui sa obtinem aceleasi informatii.
        deserializeaza(w, N, "persoane.bin");

        for (i = 0; i < N; i++){
                printf("%s %s\n",w[i].prenume,w[i].nume);
        }

        return 0;
}