sexta-feira, 28 de novembro de 2008

Trabalhando com Arquivos

Vamos falar agora propriamente dita da manipulação dos arquivos em C, leitura e escrita.

Escrevendo em um arquivo

Para escrevermos em um arquivo em C, utilizamos as funções fputc e putc. Eles escrevem na stream, um caracter por vez. O protótipo é idêntico:

int fputc(int c, FILE *stream);

As duas funções fazem exatamente a mesma coisa, só que uma, a putc(), deve ser implementada em termos de macro, para manter compatibilidade. As versões antigas utilizavam o putc(), e para manter tudo funcionando, em novas implementações se fez isso.

Lendo um caracter de arquivo

Igualmente, definido pelo padrão C ansi, temos as funções getc e fgetc. O protótipo é:

int fgetc(FILE *stream);

Igualmente a dupla fputc e putc, a putc é implementada como macro. Elas servem para ler caracteres da stream passada, que será o retorno da função. Caso chegue ao final do arquivo retornará EOF (End of File) ou erro.

Também vale relembrar, que a função getchar() é equivalente a getc(stdin), ou seja, lê caracteres da entrada padrão. Já putchar(), é equivalente a putc(caracter, stdout), ou seja, escreve no dispositivo padrão de saída.

As funções citadas fazem parte da stdio.h, e podem ser utilizadas de forma complementar, ou até com outras funções de streams. Essa é uma das características que tornam o C uma das linguagens mais versáteis e poderosas do mundo .

Bem, agora falaremos de leitura de arquivos de forma mais geral, através de strings.

Escrevendo em um arquivo linha a linha

A função fputs() é igual a função putc, só que em vez de trabalhar caracter a caracter, ela trabalha linha a linha, geralmente uma situação de conforto para o programador. O protótipo dela segue:

int fputs(const char *s, FILE *stream);

Repare que passamos um ponteiro para char, onde passaremos a string (a linha) e especificamos qual stream estamos utilizando. Note que é idêntica a função puts, só que deixa especificar a stream, ao contrário da puts que utiliza stdin. protótipo: int puts(const char *s);

Lendo arquivo linha a linha

Você também, analogamente, pode ler arquivos linha a linha. Repare o protótipo da função:

char *fgets(char *s, int size, FILE *stream);

Ela retorna um ponteiro para string recebida, ela para até achar o fim da linha ou tiver alcançado o tamanho de size-1. Stream especifica de onde está sendo lido, e o retorno vai sempre para o ponteiro que está recebendo. Caso tenha sido encontrado o final de linha ou EOF, o \0 é armazenado depois do último caracter lido.

Voltando no arquivo..

Bem, sabemos que existe um descritor de arquivo, a tal estrutura do tipo FILE, definida no cabeçalho stdio.h. Bem, nesta descrição, quando é montada a estrutura em memória, existe um de seus campos que indica a posição onde o ponteiro está posicionado para leitura/escrita. É assim que fazemos para ler caracteres e escreve-los. Existe a função chamada fseek, que existe para reposicionar o indicador de posição do descritor de string. Isso mesmo, ela só muda para onde o ponteiro está direcionado. Você pode correr ele para o início do arquivo, final, ou uma posição definida por sua pessoa mesmo.

O protótipo da função é: int fseek( FILE *stream, long offset, int whence);

O que isso quer dizer? Bem, você passa qual stream está trabalhando, o descritor logicamente, passa quantos bytes deseja percorrer a partir de uma posição predeterminada. Ou seja, o campo whence, você passa onde que é a posição base para início de calculo, por exemplo, existem a possibilidade de passar as macros: SEEK_SET, SEEK_CUR e SEEK_END, que você posiciona no início do arquivo, posição corrente ou no final do arquivo. Aí você passa quantos bytes deseja trabalhar a partir dessa posição. O retorno da função é 0 caso tenha obtido sucesso e diferente caso não o tenha.

Existem as funções ftell() (protótipo: long ftell( FILE *stream);), que retorna justamente onde está posicionado o indicador de posição da stream e a função rewind() (protótipo: void rewind( FILE *stream);) que volta ao início do arquivo, é como um (void)fseek(stream, 0L, SEEK_SET).

Mantendo tudo sob controle

Vamos relembrar: um arquivo em C é uma stream, um "fluxo" de dados. Como tudo em computação, uma stream possui um tamanho finito. Se efetuarmos diversas leituras nela, indo da primeira posição em frente, alcançaremos um ponto chamado EOF, que é o fim do arquivo (em inglês, End Of File). Deste ponto, só podemos retroceder; nem pense em ler mais alguma coisa do arquivo! E como sabemos que chegamos ao fim do arquivo? Simplesmente perguntando. Na biblioteca padrão temos a função feof que nos retorna se o arquivo já terminou. O protótipo dela é:

int feof(FILE *stream)

Ela retorna 0 se o arquivo não chegou ao fim ou 1 caso contrário (lembre que em C o valor inteiro que corresponde a uma afirmação falsa é 0 e uma afirmação verdadeira corresponde a um inteiro diferente de zero).

Por exemplo, para determinar se chegamos ao final da stream apontada pela variável Cadastro (que já deverá ter sido aberta com fopen), podemos fazer o seguinte teste:

if (feof(Cadastro))
printf ("Fim do arquivo");

Aqueles que são bons observadores, já devem ter notado que os exemplos dados na última aula já fazem uso desta função (propositalmente). Sugiro a todos que dêem mais uma olhada neles.

Uma outra função que é útil para verificar como está uma stream é a ferror. Novamente, como tudo em computação, um acesso a uma stream pode ser efetuado com sucesso, ou pode falhar (pessimistas acham que a probabilidade de falhar é maior do que a de acertar). Para ter certeza de que eu não tenha resultados imprevistos na hora em que um acesso der errado, eu testo o indicador de erro da stream logo após tê-la acessado. Se for 0, não há erro; se for diferente de zero, "Houston, temos um problema". O protótipo de ferror é:

int ferror(FILE *stream)

Vamos dar uma olhada em um pedacinho de código só para fixarmos esta nova função. Neste exemplo, a stream apontada por Lista já deverá estar aberta, e Item é uma string:

fgets(Item, 40, Lista);
if (ferror(Lista)) {
printf ("Apertem os cintos: ocorreu um erro no último acesso!");
exit(1);
}

Note bem: o indicador de erro poderá mudar se for feito uma nova leitura ou escrita antes de testá-lo. Se quiser testar, teste logo após executar um acesso.

Escreveu, não leu...

Já notou que as funções fgets e fputs se parecem muito com as suas primas gets e puts, sendo que estas lêem do teclado e escrevem na tela e aquelas fazem o mesmo, só que relativamente a streams? Seria bom se nós tivéssemos algo do tipo printf e scanf para arquivos; e nós temos! Usando fprintf e fscanf nós fazemos o mesmo que já fazíamos na tela, mas agora faremos em streams. Veja como elas estão definidas nas bibliotecas padrão:

fprintf (FILE *stream, char *formato, ...)

fscanf (FILE *stream, char *formato, ...)

Na prática, podemos usar estas duas funções que atuam em streams do mesmo modo como usamos suas primas, já velhas conhecidas nossas, mas acrescentando antes o ponteiro para a stream. Assim, se eu tinha um programa que lia do teclado dados digitados pelo usuário, posso fazer uma mudança para ler de um arquivo. De modo similar, isso também vale para a tela. Por exemplo, sejam dois arquivos apontados pelas variáveis Saída (aberto para escrita) e Dados (aberto para leitura):

se eu tinha: scanf("%d",&numero);
posso fazer: fscanf(Dados,"%d",&numero);

se eu tinha: printf("Tenho %d unidades do produto: %s\n",quant,prod);
posso fazer: fscanf(Saida,"Tenho %d unidades do produto: %s\n",quant,prod);

Experimente usar isto nos programas que você já fez, só para praticar: ao invés de usar de scanf e printf, leia dados com fscanf de um arquivo e escreva-os em um outro arquivo com fprintf. O arquivo gerado poderá ser aberto inclusive em editores de texto, e você poderá ver o resultado. Mas ATENÇÃO: fprintf e fscanf devem ser aplicados a streams texto.

Até agora, todas as funções que lêem e escrevem em um arquivo se referem a streams texto. É uma boa hora para se questionar a utilidade das tais streams binárias.

Podemos querer ler e/ou escrever uma stream tendo controle total dos bytes dela, para, por exemplo, copiar fielmente um arquivo, sem alterar nada nele. Assim, abriremos o arquivo usando o já mencionado qualificador b. E para acessar os dados, teremos duas novas funções, fread e fwrite, definidas da seguinte forma:

size_t fread(void *Buffer, size_t TamItem, size_t Cont, FILE *Fp)
size_t fwrite(void *Buffer, size_t TamItem, size_t Cont, FILE *Fp)

O tipo size_t é definido na biblioteca STDIO.H e, para simplificar, podemos dizer que eqüivale a um inteiro sem sinal (unsigned int). Lembre-se de que void* é um ponteiro qualquer, ou seja, um ponteiro sem tipo. Assim, eu posso usar um int*, char*, ...

A função fread opera do seguinte modo: lê da stream apontada pela variável Fp tantos itens quantos forem determinados pela variável Cont (cada item tem o tamanho em bytes descrito em TamItem) e coloca os bytes lidos na região de memória apontada por Buffer. A função fwrite funciona de modo similar, porém gravando o que está na região de memória apontada por Buffer na stream Fp.

As duas funções retornam o número de bytes efetivamente lidos ou escritos do arquivo, respectivamente.

Para determinar o tamanho em bytes de um determinado tipo de dado que queremos ler ou gravar, é possível usar o operador sizeof. Como exemplo, podemos querer gravar na stream binária apontada pela variável Dados o valor da variável inteira X:

fwrite(&X,sizeof(int),1,Dados);

O tamanho de um inteiro é determinado por sizeof(int). Note que o número de bytes que foram efetivamente gravados, retornado pela função, pode ser desprezado (e geralmente o é).

Seja a struct Pessoa definida por (lembra-se das aulas sobre struct?):

struct Pessoa {
char nome[40];
int idade;
};

Definamos a variável Aluno do tipo struct Pessoa. Podemos ler um registro de um aluno de um arquivo com o comando:

fread(&Aluno,sizeof(struct Pessoa),1,Dados);

Nenhum comentário: