Enrere Mòdul 8
Fonaments de Programació. Llenguatge C/C++---
Pràctica  Exercicis
Pràctica d'ampliació

 
Resum Teòric

Introducció

El sistema d'entrada i sortida de C/C++ proporciona un sistema independent del dispositiu físic en el qual, o des del qual, es realitzin les operacions. Aquest sistema independent és una abstracció de dispositiu que rep el nom de flux o corrent (en anglès stream). De fet, només necessitem considerar un arxiu com un flux de dades que circulen en un o altre sentit. 

Tècnicament, quan s'obre un arxiu per utilitzar funcions d'entrada i sortida, aquest arxiu s'associa amb una estructura del tipus FILE (definit a stdio.h). Aquesta estructura conté tota la informació bàsica sobre l'arxiu. 

Declaració d'un arxiu

Com tota variable, una variable arxiu ha de ser declarada abans d'utilitzar-la. Així, la sentència

FILE *disc;

declara una variable d’arxius anomenada disc. Aquesta variable, de fet, és un punter a l'objecte FILE, que és una estructura definida a l'arxiu capçalera stdio.h. Per tant, és necessari incloure la directiva #include <stdio.h> en qualsevol programa en què s’utilitzi arxius.

Obertura d'un arxiu

Declarar una variable d'arxiu no és suficient per poder treballar amb ell. És necessari precisar dues coses:

  • D'una banda, és imprescindible assenyalar de quin arxiu físic es tracta. És necessari donar un nom a l'arxiu que serà el que restarà al suport magnètic.

  • D'altra banda, farà falta precisar el tipus de treball que volem fer: llegir, escriure,...

Aquestes característiques es donen amb l'ajuda de la funció fopen() que té la següent sintaxi:

FILE *fopen(char *nom_arxiu, char *mode)

La funció fopen() retornarà el valor NULL si no s'ha pogut obrir l'arxiu, per exemple, si el disc està protegit o ple.

El primer argument de la funció fopen() és el nom físic de l'arxiu i permet l'associació entre el nom físic sobre el suport (nom_arxiu) i el nom lògic de l'arxiu en el programa.

El segon argument està format per una cadena de caràcters que precisa el mode de treball amb l'arxiu. Són possibles les diferents possibilitats:

r

(read). Obre un arxiu només per llegir. L'arxiu ha d'existir. El cursor se situarà damunt del primer octet de l'arxiu.

w

(write). Obre un arxiu només per escriure. El cursor se situarà damunt del primer octet de l'arxiu. Si l'arxiu ja existeix es sobreescriu.

a

(append). Obre un arxiu per escriure. El cursor se situarà damunt de l'últim octet de l'arxiu si aquest ja existeix, per tant, serveix per afegir dades a un arxiu. En el cas que l'arxiu no existeixi funciona igual que w.

r+

Obre un arxiu per llegir i escriure alhora. L'arxiu ha d'existir prèviament. El cursor se situa al començament de l'arxiu.

w+

Obre un arxiu per llegir i escriure alhora. Si l'arxiu existeix prèviament, s'esborra el seu contingut.

a+

Obre un arxiu per afegir dades i per llegir. Si l'arxiu no existeix el crearà. Aquest mode, a diferència del mode a, esborra la marca EOF de l'arxiu abans d'afegir dades, d'aquesta forma el comandament type de l'MS-DOS podrà mostrar tot el contingut de l'arxiu.

Als modes anteriors se li pot afegir els caràcters t o b per tal d'indicar si es tracta d'un arxiu de text o un arxiu binari. L'arxiu en mode text tradueix en l'entrada les seqüències de retorn de carro/alimentació de línia en caràcters de nova línia i a la sortida fa la traducció a l'inrevés

La raó de l'existència d'aquests dos modes -text i binari- és històrica. En els sistemes UNIX es va utilitzar un d'ells i el conegut MSDOS va utilitzar l'altre.

Tancar un arxiu

Al final del treball sobre l'arxiu, o si més no abans de sortir del programa, és necessari tancar l'arxiu. Aquest tancament té diferents objectius. El primer consisteix en alliberar el canal atribuït a l'arxiu i permetre obriu un altre arxiu sense superar el màxim nombre possible d'arxius oberts al mateix temps. El segon, i gairebé sempre el més important, és buidar el buffer (zona de memòria) associat a l'arxiu. L'escriptura d'un arxiu no es fa directament al suport físic sinó que les diferents escriptures són enviades a un buffer  per tal d'estalviar accessos al disc que són massa lents. En cas d'acabament normal del programa, el sistema operatiu s'encarrega de tancar els arxius, però això no sempre és possible, per exemple quan marxa el llum, si es bloqueja l'ordinador, etc. Per tant és recomanable cada cert interval de temps tancar i tornar-lo a obrir per evitar pèrdua de dades.

Per tancar ho farem amb la funció fclose() que té la següent sintaxi:

int fclose (FILE *nom_arxiu);

Aquesta funció torna el valor enter 0 si l'arxiu s'ha pogut tancar correctament.

 

Escriure i llegir en un arxiu amb les funcions getc() i putc()

Les funcions getc() i putc() s'utilitzen per llegir i escriure caràcters en un arxiu obert prèviament amb la funció fopen(). La sintaxi d'aquestes dues funcions són:

int putc(int ch, FILE *fp)

int getc(FILE *fp)

on fp és un punter d'arxiu retornat per la funció fopen i ch una variable caràcter. De fet ch pot ser una variable entera, però només els 8 bits més baixos dels 32 d'una variable entera són utilitzats.

Si tot funciona correctament, les dues funcions retornen EOF, una constant entera definida a l'arxiu de capçalera stdio.h i que significa el final de l'arxiu. Per tant, si volem llegir un arxiu fins al final es pot fer servir el següent codi:

while((ch=getc(fp))!=EOF) putchar(ch);

Lectura i escriptura de cadenes de caràcters. Les funcions  fgets() i fputs()

Les funcions fgets() i fputs() permeten llegir i escriure cadenes de caràcters en els arxius prèviament oberts amb la funció fopen(). La sintaxi d'aquestes funcions és:

char *fgets( char *cadena, int n, FILE *fp);

int fputs( const char *cadena, FILE *fp );

La funció fgets() llegeix de l'arxiu apuntat per *fp una cadena de caràcters i la guarda a la variable cadena. Si la mida de la cadena llegida supera els n-1 caràcters, només es llegeixen els n-1 primers caràcters. Al resultat guardat en cadena  se li afegeix el caràcter de fi de cadena ('\0'). Si s'ha llegit el caràcter de nova línia també és emmagatzemat a la variable. Això últim és una diferència important entre la funció fgets() i la funció gets().

Aquesta funció retorna un punter a la cadena cadena o bé un punter NULL si hi ha hagut algun error.

La funció fputs() escriu el contingut de la variable cadena a l'arxiu apuntat per *fp. No escriu el caràcter fi de cadena. Aquesta funció torna la constant entera EOF si es troba algun error. 


Lectura i escriptura formatada. Les funcions  fprintf() i fscanf()

Les funcions fprintf() i fscanf() funcionen exactament igual que printf()scanf() excepte pel fet que necessiten un argument addicional per apuntar a l'arxiu corresponent. La sintaxi d'aquestes funcions és:

int fprintf(punter_arxiu,"cadena_de_control",llista arguments);

int scanf (punter_arxiu,"cadena_de_control",llista arguments);

La funció fprintf() retorna el nombre de caràcters enviats a l'arxiu. Si es produeix un error retorna un número negatiu.

La funció fscanf() retorna el nombre d'elements que ha pogut aparellar. Quan arriba al final de l'arxiu retorna la constant EOF.

Lectura i escriptura de registres. Les funcions  fread() i fwrite()

També hi ha la possibilitat d’escriure tot un bloc de dades en un arxiu binari. Vectors, estructures, vectors d’estructures i altres dades poden ser gravades de cop amb la funció fwrite().

El prototipus de la funció és:

int fwrite(void *punter, int grandaria_del_element , int nombre_de_elements, FILE *arxiu)

Aquesta funció retorna el nombre d'elements escrits. Si aquesta dada no és l'esperada tenim algun error d’escriptura.

Farem una breu descripció de cada argument:

  • Primer argument. void *punter. És un punter al bloc de dades que volem escriure a l'arxiu. Si l’element a gravar és una estructura hem d’enviar l’adreça de l’estructura.

  • Segon argument. int grandària. Aquesta variable és la mida en octets de l’element a gravar. Per assegurar la portabilitat és convenient utilitzar l’operador sizeof().

  • Tercer argument. int nombre_de_elements. Aquest és el nombre d’elements a gravar. El nombre total d'octets serà el producte d'aquesta variable i l'anterior.

  • Quart argument. FILE *arxiu. És el punter d'arxiu corresponent.

 

De la mateixa forma, per llegir tot un bloc es fa servir la funció fread(), el prototipus de la qual és:

int fread(void *punter, int grandaria_del_element , int nombre_de_elements, FILE *arxiu)

Aquesta funció retorna el total d'octets llegits. Si aquest nombre és inferior a nombre_de_elements és que s'ha produït un error (aquest error pot ser simplement que ha trobat el final de l'arxiu).

El significat dels arguments és similar als de la funció fwrite(), només puntualitzar que  void *punter  és ara un punter al lloc de la memòria que s'omplirà amb les dades llegides.

La funció ferror() de detecció d'errors.

La funció ferror() determina si s'ha produït un error en una operació sobre un arxiu. El prototipus de la funció és:

int ferror( FILE *fp);

on fp és un punter a un arxiu. La funció ferror() retorna veritat si s'ha produït un error en l'última operació sobre l'arxiu, en cas contrari retorna fals. Per tant, en cada operació sobre un arxiu és convenient cridar a la funció ferror().

 

La funció remove().

La funció remove() té per protocol:

int remove( char *nom_arxiu);

Aquesta funció esborra l'arxiu que especifiquem en el seu argument. Si tot funciona correctament retornarà zero, en cas contrari un número diferent de zero.

La funció feof().

La funció feof() retorna un valor diferent de zero si el punter es troba al final d’arxiu. Mentre sigui zero aquest punter no es troba al final de l'arxiu.

fseek(), ftell() i rewind()

Quan s'obre un arxiu amb la funció fopen() es crea un marcador o punter que indica la posició exacta en el flux de dades que es llegirà o escriurà en la pròxima funció d'entrada o sortida. La funció fopen() fa que aquest marcador comenci apuntant al principi del flux o al final, segons el mode establert. Les funcions abans tractades, modifiquen automàticament aquest marcador per tal que la següent funció d'entrada o sortida llegeixi o escrigui a continuació.

No obstant això, C/C++ incorporen funcions que permeten alterar aquest marcador. 

La funció fseek() permet modificar la posició d'aquest marcador. La seva sintaxi és:

int fseek( FILE *fp, long desplaçament, int origen );

El punter fp és un punter del tipus FILE que es refereix a un arxiu obert. La variable desplaçament és el nombre d'octets que es desplaçarà el marcador des de la posició indicada per la variable origen. La variable origen només pot tenir tres valors (0,1 o 2). El significat d'aquests tres valors és:

  • 0 es calcula el desplaçament des del principi de l'arxiu.
  • 1 es calcula el desplaçament des de la posició actual del marcador.
  • 2 es calcula el desplaçament des de la posició final de l'arxiu.

Hi ha definides tres constants per referir-se a cadascun d'aquests valors: SEEK_SET, SEEK_CUR i SEEK_END.

La funció fseek() torna el valor 0 si el desplaçament s'ha produït amb èxit o un valor diferent de 0 si no s'ha pogut produir.

La funció ftell() torna la posició actual del marcador comptant des de la posició inicial. La seva sintaxi és:

long ftell( FILE *fp);

 

La funció rewind() només fa que el marcador apunti al començament de l'arxiu. La seva sintaxis és:

void rewind( FILE *fp );

Aquestes funcions es poden fer servir per crear arxius d'accés directe.

stdin, stdout i stderr

En el moment de començar l'execució d'una aplicació, automàticament s'obren tres fluxos: stdin, stdout i stderr, el primer està associat amb el teclat i els dos últims amb la pantalla. Es poden fer servir aquests fluxos en qualsevol de les funcions d'entrada i sortida tractats amb anterioritat. Per exemple:

fgets( cadena,10,stdin);

llegeix del teclat 10 caràcters i els emmagatzema a la variable cadena i

fputs( cadena, stdout);

escriu el contingut de la variable cadena a la pantalla.

Introducció a les entrades i sortides de C++

Encara que totes les funcions d'entrada i sortida de C i declarades a l'arxiu de capçalera stdio.h també estan disponibles en C++, aquest últim incorpora la seva forma pròpia de tractar amb arxius més relacionada amb la filosofia de la programació orientada a objectes (POO). Les classes que s'encarreguen de manipular les entrades i sortides es troben a l'arxiu de capçalera iostream.h.

S'ha d'advertir que no es pot entendre amb profunditat el funcionament de les entrades i sortides de C++ sense endinsar-se en la POO i, per tant, no és objectiu d'aquestes línies fer una descripció detallada d'aquest aspecte de C++. Ens limitarem a presentar un petit nombre d'operacions que es pot fer amb aquestes classes i alguns exemples.

cin, cout i cerr i els operadors d'extracció (>>) i inserció (<<)

L'equivalent dels fluxos de C: stdin, stdout i stderr són cin, cout i cerr. Aquests últims es descriuen a l'arxiu iostream.h i s'obren automàticament en el moment de començar l'execució del programa.

Les accions d'intercanvi de dades entre variables i fluxos es fa a través dels operadors d'extracció (>>) i inserció (<<). Aquests operadors estan sobrecarregats. Això, que és una característica pròpia de C++,  significa que estan associats a funcions diferents segons el tipus d'operadors que els hi acompanyen. Això ja s'ha fet servir des del mòdul 1 en el qual es va parlar de les entrades i sortides estàndards.

 

Arxius d'entrada i sortida

Si en lloc d'escriure i llegir de les sortides i entrades estàndards es vol fer aquestes accions amb arxius de disc és necessari incorporar l'arxiu de capçalera fstream.h. Aquest arxiu ja incorpora a iostream.h. L'arxiu fstream.h conté les classes: fstream, ifstream i ofstream

Un objecte de la classe fstream estarà associat amb un arxiu d'entrada i sortida.

Un objecte de la classe ifstream estarà associat amb una arxiu d'entrada.

Un objecte de la classe ofstream estarà associat amb un arxiu de sortida.

Per obrir un arxiu d'entrada s'ha de declarar un objecte de la classe ifstream (també pot ser fstream). En la declaració de la classe es pot assignar el nom d'arxiu i el mode. Si l'objecte és de la classe ifstream, normalment assignarem només el nom, ja que el mode s'assigna automàticament.

Per exemple:

ifstream arxiu("a:/arxiu.in");

Aquesta línia crea un objecte anomenat arxiu associat a l'arxiu físic a:\arxiu.in. Mencionarem també que aquesta associació es pot fer en dos passos:

 

ifstream arxiu;
arxiu.open("a:/arxiu.in");

Es pot comprovar si hi ha hagut un error amb el nom de la classe de la següent forma:

 

ifstream arxiu;
arxiu.open("a:/arxiu.in");
if(!arxiu) cerr << "no es pot obrir l'arxiu";;

De la mateixa forma, per obrir un arxiu de sortida s'ha de declarar un objecte de la classe ofstream (també pot ser fstream). En la declaració de la classe es pot assignar el nom de fitxer i el mode. Si l'objecte és de la classe ofstream, normalment assignarem només el nom, ja que el mode s'assigna automàticament.

Per exemple:

 

ofstream arxiu("a:/arxiu.out");

Aquesta línia crea un objecte anomenat arxiu associat a l'arxiu físic a:\arxiu.in. Mencionarem també que aquesta associació es pot fer igualment en dos passos:

 

ofstream arxiu;
arxiu.open("a:/arxiu.out");

Es pot comprovar si hi ha hagut un error amb el nom de la classe de la següent forma:

 

ofstream arxiu;
arxiu.open("a:/arxiu.out");
if(!arxiu) cerr << "no es pot obrir l'arxiu";;

Una vegada obert un objecte d'aquestes classes es pot fer servir amb els operadors << i >> per escriure i llegir tot tipus de dades.

De la mateixa forma que en C, els arxius oberts s'han de tancar per assegurar-se que tot el contingut del buffer es grava al disc. Això s'aconsegueix amb la funció membre close. Per exemple:

 

arxiu.close();