CEFETES – Serra - Programação II – Mateus Costa Notas de Aula - Arquivos 1 Arquivos em C O objetivo deste capítulo e permitir a construção de programas que armazenem dados de forma permanente em dispositivos de armazenamento secundário, tais como disquetes e discos rígidos. 1.1 Definições Sistema de arquivos em C Æ é o conjunto de funções da linguagem que podem ler, escrever e manipular dados em dispositivos periféricos. Stream e Arquivos Æ Stream é uma abstração do dispositivo periférico. O dispositivo real é o arquivo. Em C o termo Stream identifica logicamente um dispositivo periférico qualquer que permita a leitura e/ou gravação de dados. No entanto neste texto usaremos o termo arquivo para nos referirmos aos arquivos gravados em memória secundária Arquivos de dados armazenados em memória Secundária Æ permitem o armazenamento permanentemente dos dados. A linguagem C provê um conjunto completo de funções para manipulação de arquivos em disco. 1.2 Tipos de arquivos Existem dois tipos básicos de arquivos, nos quais se baseiam programas que manipulam arquivos. Em C esses tipos de arquivos são chamados de arquivos texto e arquivos binários. Arquivo texto - É um arquivo cujo conteúdo é baseado em uma seqüência de caracteres que formam linhas determinadas por um caracter de nova linha ( “ \ n ”). Dentro destes arquivos podem ser gravados apenas dados em forma de texto. Ou seja, não poderemos gravar, por exemplo, um valor numérico nestes arquivos, a não ser que este valor seja transformado em seqüências de caracteres. Esta forma de organização de arquivos e a mais simples. Funções em C Funções são subprogramas chamados que quando Arquivo texto Arquivos binários – Arquivos binários tem o seu conteúdo baseado em uma estrutura ou dado que respeita um determinado tipo de dado. Este tipo de dado pode ser um tipo simples (int, float, char) ou um tipo estruturado como registro (struct). Portanto esses arquivos são construídos como uma seqüência de bytes respeitando uma determinada estrutura. Esses arquivos podem ser visualizados como um tabela onde cada linha possui um determinado conjuntos de campos (colunas). Cada linha dessa tabela possuirá também um número que identificará sua posição no arquivo. Por essas características, tais arquivos têm a vantagem de permitir o acesso aleatório a posições. Nome Telefone idade Maria 45545 21 João 87878 30 Paula 78867 22 José 565675 20 ... ... ... Visualização abstrata de um arquivo binário organizado a partir de uma struct contendo os campos nome, telefone e idade. 1.3 Ferramentas para manipulação de arquivos 1.3.1 Estrutura de controle FILE – A linguagem C utiliza esta estrutura para criarmos um ponteiro para o arquivo. Este ponteiro irá conter o endereço da estrutura FILE criada que manterá várias informações sobre o arquivo, como o seu nome, posição atual, etc. Estas informações são usadas pelas funções do sistema de arquivos em C. Para ler ou escrever arquivos, qualquer programa precisa usar ponteiros de arquivos. Declaração de um ponteiro de arquivo FILE *fp; FILE – é o nome da estrutura (tipo de dado) O * define que a variável fp é uma variável do tipo ponteiro para FILE. 1.3.2 Abrindo um arquivo abrir um arquivo significa criar uma associação entre o arquivo físico (na memória secundária) e o programa, através do ponteiro de arquivo. Para tanto usa-se a função fopen cujo protótipo é mostrado a seguir. O protótipo de uma função indica como a função foi definida, qual o tipo de dado que ela retorna e quais os parâmetros da função. Os parâmetros são indicados entre os parênteses da função. Por exemplo no lugar do parâmetro nomearq da função fopen, deve ser informado o nome do arquivo a ser aberto, através de uma variável do tipo ponteiro para char. Ainda não falamos de ponteiros para char. No entanto você já está acustumado a usá-los. Trata-se de uma string, de um variável do tipo cadeia de caracteres (ex. char str[30]) ou de uma constante do tipo string (ex. “arquivox.txt”). O valor de retorno de uma função deve ser atribuído a uma variável do mesmo tipo da função. No caso, fopen() é definida como um FILE *, ou seja, um ponteiro para FILE. Assim sendo, quando fopen é chamada deve ser feita com a atribuição do seu valor de retorna a uma variável do tipo FILE *. Ex: FILE *arq; arq = fopen(“arquivox.txt”, “w); Usar o comando de atribuição entre uma variável e uma função indica que o valor de retorno da função está sendo atribuído a variável. Parâmetro indicando o nome físico do arquivo FILE *fopen ( const char * nomearq, const char *modo) Significa que fopen retorna um ponteiro de arquivo parâmetro do tipo string que determina o modo como o arquivo será aberto nomearq é um ponteiro (nome de string) para uma cadeia de caracteres que indica um nome válido de um arquivo, podendo indicar o seu caminho. modo é um ponteiro para uma cadeia de caracteres que indica o modo como o arquivo será aberto. Esta string é construída utilizando-se os caracteres da tabela abaixo: MODO r w a rb wb ab r+ w+ a+ r+b w+b a+b SIGNIFICADO Abre um arquivo texto para leitura Cria um arquivo texto para escrita ou sobrescreve caso o arquivo exista Anexa a um arquivo texto: abre para escrita no final do arquivo texto ou caia um novo arquivo caso o mesmo não exista Abre um arquivo binário para leitura Cria um arquivo binário para escrita Abre um arquivo binário para escrita no final do arquivo ou cria um novo caso o mesmo não exista Abre um arquivo texto para leitura e escrita Cria um arquivo texto p/ leitura e escrita ou sobrescreve caso o arquivo já exista Abre um arquivo e cria um novo para leitura e escrita no final do arquivo Abre um arquivo binário para leitura/escrita Cria um arquivo binário para leitura/escrita Abre um arquivo binário para leitura e escrita no final do mesmo Exemplo: Suponha o arquivo teste.txt. O programa abaixo pode ser usado para abrí-lo para escrita no modo texto main ( ) { FILE *fp; fp = fopen (“teste.txt”, “w”) } Embora esteja correto, normalmente o código anterior é escrito da seguinte maneira: main ( ) { FILE *fp; if ((fp = fopen (“teste.txt”, “w”) = = null) { printf (“o arquivo não pode ser aberto \ n”); return 0; } } NULL é um valor nulo – inválido. Desta forma o programa irá saber se o arquivo foi corretamente aberto, detectando qualquer tipo de erro que possa ocorrer. Para evitar erros, sempre devemos conferir o sucesso da chamada de fopen. Obs. Sempre que abrirmos um arquivo para escrita, se o arquivo existir o conteúdo do mesmo será apagado. Fechando um arquivo todo arquivo aberto deve ser fechado quando não mais necessário, evitando perda de dados e outros problemas. Para tanto a função fclose ( ) deve ser usada. Seu protótipo é o seguinte: Ponteiro do arquivo int close ( FILE *fp ) fclose Retorna EOF se houver algum erro e 0 caso tenha sucesso.. 1.3.3 Escrevendo e lendo em um arquivo texto Escrevendo caracteres Função: putc ( ou fputc ( ) ). Protótipo: int putc (int ch, FILE*fp); Esta função pode ser usada para escrevermos caracteres em arquivos que foram previamente abertos por fopen (). fp é um ponteiro de arquivo retornado por fopen. ch é definido como int (2 bytes) mas apenas o byte menos significativo é levado em conta. Se putc ( ) falha ela devolve o caracter de fim de arquivo EOF Lendo caracteres Função getc ( ) (ou fgetc()) Protótipo: int getc (FILE,*fp); Lê um caracter de um arquivo previamente aberto por fopen() usando o ponteiro fp. Da mesma forma que putc, getc devolve um inteiro, mas apenas o byte menos significativo é usado. Quando chega ao final do arquivo getc ( ) devolve o caracter EOF. Ex. No código seguinte um arquivo é lido até o seu final do { ch = getc (fp); }while (ch!=EOF); Quando ocorre um erro, getc também retorna EOF. Lendo e escrevendo strings Funções: fputs e fgets As funções: fputs () e fgets() – efetuam operações de leitura e escrita de e para arquivos de forma semelhante a getc e putc. Contudo, elas lêem e escrevem strings, ao invés de caracteres. Protótipos: int fputs ( const char * str, FILE *fp); char * fgets ( char *str, int lenght, FILE *fp); fgets retorna um ponteiro para STR fputs escreve a string str no arquivo especificado. Devolve EOF se um erro ocorre. fgets lê uma string do arquivo especificado até que um caracter de nova linha seja encontrado ou que length-1 caracteres tenham sido lidos, se o caracter de nova linha for lido ele será inserido na string. A string resultante será terminada por um ‘\0’. Retorna NULL se ocorre um erro ou fim de arquivo. Exemplo1: Lê a primeira linha do arquivo “agenda.cpp” int main(int argc, char **argv) { FILE *fp; char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.cpp","r"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } fgets(buffer,100,fp); puts(buffer); getch(); return 0; } 1.3.3.1.1 Exemplo 2 – Escrevendo strings em um arquivo int main(int argc, char **argv) { FILE *fp; char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.txt","a"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } do { printf ("Digite uma string (enter para sair:\n"); gets(buffer); // gets não guarda o ‘\n‘. temos que usar o strcat para // colocar o ‘\n‘ no final da string se quisermos saltar uma linha no // arquivo. strcat (buffer,"\n"); // função de concatenação de strings fputs (buffer, fp); }while (*buffer!='\n'); fclose(fp); } exemplo 3 – Lendo um arquivo e apresentando na tela com fgets() int main(int argc, char **argv) { FILE *fp; char buffer[100]; strcpy(buffer,""); if ((fp=fopen("agenda.cpp","r"))==NULL) { puts("erro ao abrir o arquivo"); return 0; } fgets(buffer,100,fp); while (fgets(buffer,100,fp)!=NULL) { printf("%s",buffer); } fclose(fp); getch(); } Exercícios: 1. Construir um programa que leia uma string e escreva essa string em um arquivo chamado ex1.txt caractere a caractere usando a função putc. 2. Construir um programa que leia o arquivo ex1.txt e guarde os caracteres lidos em uma string. Posteriormente imprima a string na tela. 3. Construir um programa que leia palavras do teclado para um vetor de 20 strings. Posteriormente, grave as strings guardadas no vetor no arquivo ex2.txt usando a função fputs(). 4. Construir um programa que leia o arquivo ex2.txt. usando a função fgets e apresente as strings lidas na tela. 5. Criar um programa que permita a cópia de arquivos texto. O programa deve pedir o nome do arquivo que se quer copiar e o nome do arquivo que será a cópia. 6. Criar um programa que conte o número de palavras de um arquiv. Dica: Uma palavra pode ser terminada por um espaço (‘ ‘), um caractere de tabulação (‘\t’) ou um caractere de nova linha (‘\n’). 7. Construir um programa que leia uma palavra do teclado e conte o número de ocorrências da palavra lida em um arquivo texto. 8. Criar um programa que leia um arquivo e crie uma cópia “criptografada” do arquivo lido somando-se o valor 1 a cada caracter lido no arquivo original. Criar um programa para decriptografar o arquivo subtraindo 1 de cada caractere lido. Sugestão: Não criptografe o caractere de nova linha (‘\n’). 9. Construa um programa que conte o número de ocorrências de uma seqüência de caracteres dentro de um arquivo texto. A seqüência de caracteres deve ser lida do teclado e não deve conter nem espaços, nem tabs, nem caracteres de nova linha. Neste caso a seqüência pode estar dentro de um outra seqüência. Por exemplo, se procurarmos a seqüência “ana” em um arquivo que contém o texto: “Mariana é uma garota de Americana muito bacana”. Neste caso, a seqüência ana ocorre três vezes: Mariana é uma garota de Americana muito bacana. 1.4 Outras Funções de manipulação de arquivos feof() – Determina a ocorrência de fim de arquivo quando este é aberto em modo binário. Protótipo: int feof(FILE *fp) Comentário: No caso de arquivos binários, é possível que um dos dados lidos seja o valor EOF. Portanto, não é confiável utilizar este caractere para determinar o fim de arquivo. Assim, devemos usar a função feof() no lugar do teste de fim de arquivo. rewind() – Reposiciona o indicador de posição do arquivo em seu início. Protótipo: void rewind (FILE * fp) A medida que avançamos na leitura ou escrita de um arquivo, o seu indicador de posição avança junto. Assim, se quisermos voltar ao início do arquivo sem fechá-lo, devemos usar a função rewind(). ferror() – determina se uma operação com um arquivo produziu um erro. Protótipo: int ferror(FILE *fp); ferror informa retornando verdadeiro se ocorreu um erro na última operação realizada no arquivo. Ou seja, ferreor deve ser testada logo após as operações que se deseja verificar. remove() - apaga um arquivo Protótipo: int remove(const char *nomearq) Esta função apaga o arquivo indicado com pelo nome. Esta função deve ser usada cuidadosamente. É sempre bom utilizar questionar o usuário se este deseja realmente apagar o arquivo em questão. 1.5 Lendo e escrevendo em Arquivos Binários - fread() e fwrite() Até agora, usamos o sistema de arquivos de C para lermos arquivos texto. No entanto, C também permite que criemos arquivos do tipo binário. Um arquivo binário é uma forma de organização que permite o armazenamento sequencial de estruturas maiores que um byte (char). Finalidade – a finaldade deste tipo de organização é podermos Ter em armazenamento secundário uma forma de organização mais eficiente. Um aplicação imediata é no progama da agenda. Neste caso, os dados são armazenados em um vetor de registros. Da mesma forma, podemos armazenar os registros em um arquivo binário usando fread e fwrite. 1.5.1 Operador que determina o tamanho de tipos de dados - sizeof() O operador sizeof será bastante utilizado na manipulação de arquivos binários. Isso porque, para usarmos fread ou fwrite precisamos determinar o número de bytes que desejamos ler ou gravar no arquivo binário. Como saber exatamente o tamanho de uma variável? Simples: basta usar sizeof tomando como parâmetro o seu tipo. Por exemplo, o programa abaixo imprime os tamanho em bytes dos tipos char, int, float, double e long double: int main() int a,b,c,d,e; a=sizeof(char); b=sizeof(int); c=sizeof(float); d=sizeof(double); e=sizeof(long double); printf (" char: %d \n int: %d \n float: %d \n double: %d \n long double: %d \n",a,b,c,d,e); getch(); } sizeof pode ser usado para determinar o tamanho de structs. Exercício: Construa um struct para conter os dados de uma pessoa: Nome, telefone, endereco, e-mail, data de nascimento e CPF. Use sizeof para imprimir o tamanho da struct que você declarou. 1.5.2 Função fwrite() Protótipo: Size_t fwrite(const void *buffer, size_t numbytes, size_t count, FILE *fp); Parâmetros: - buffer – ponteiro para uma região da memória que contém os dados que serão escritos no arquivo - numbytes – determina o número de bytes de cada item (elemento individual) a ser escrito no arquivo. - Count determina o número de itens a serem escritos no arquivo. Os bytes serão pegos na memória a partir do endereço especificado por buffer e escritos no arquivo. - fp é um ponteiro para um arquivo previamente aberto por fopen no modo binário. 1.5.3 Função fread() Protótipo: Size_t fread(void *buffer,size_t numbytes, size_t count, FILE *fp); Parâmetros: - buffer – ponteiro para uma região da memória que irá receber os dados que serão lidos do arquivo - numbytes – determina o número de bytes de cada item (elemento individual) a ser lido do arquivo. - Count determina o número de itens a serem lidos do arquivo. Os bytes serão lidos do arquivo e armazenados na memória a partir do endereço especificado por buffer. - fp é um ponteiro para um arquivo previamente aberto por fopen no modo binário. 1.5.4 Exemplo1 - Escrevendo número inteiros em um arquivo // Programa que lê n números inteiros do teclado e armazena os números em um arquivo binário int main() { FILE *fp; int numero,n,i; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); scanf("%s",nomearq); //abre o arquivo para escrita em modo binário fp=fopen(nomearq,"wb"); if (fp==NULL){ puts("erro"); getch(); return 0; } printf("Digite o número de elementos scanf("%d",&n); a serem lidos:"); for(i=0;i<n;i++){ printf("Digite o elemento %d:",i+1); scanf("%d",&numero); // Le um número // escreve o numero no arquivo fwrite(&numero,sizeof(int),1,fp); } if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } 1.5.5 Operador de endereço & Neste exemplo n números inteiros são lidos do teclado para a memória na variável numero, e posteriormente gravados no arquivo. Observe que na função fwrite o parâmetro buffer e substituído por &numero. O operador & usado é chamado de operador de endereço. Ele é usado porque, conforme visto anteriormente, buffer deve ser um ponteiro (endereço) para a região da memória onde se encontra o dado a ser gravado por fwrite. Ou seja, a função do & junto a uma variável é obter o endereço desta variável. Este é o mesmo motivo pelo qual utilizamos o operador & nas variáveis a serem lidas por scanf. 1.5.6 Exemplo 2 – Programa que lê um arquivo binário de números inteiros e imprime na tela int main() { FILE *fp; int numero,n,i; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); scanf("%s",nomearq); //abre o arquivo para leitura em modo binário fp=fopen(nomearq,"rb"); if (fp==NULL){ puts("erro"); getch(); return 0; } fread(&numero,sizeof(int),1,fp); // tenta ler o primeiro elemento do //arquivo while(!feof(fp)){ // feof testa se não é fim de arquivo printf("%d\n",numero); // imprime o número fread(&numero,sizeof(int),1,fp); // escreve o numero no arquivo // le o próximo número } getch(); if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } Neste exemplo, o procedimento de leitura do arquivo é semelhante ao de leitura de um arquivo texto. Lê-se primeiro elemento fora do while. No entanto, ao invés do teste com o caractere EOF, a função feof() deve ser utilizada. 1.5.7 Exemplo 3 - Escrevendo um vetor de inteiros em um arquivo binário int main() { FILE *fp; int i, n, vetint[100]; char nomearq[15]; // Le o nome do arquivo printf("Digite o nome do arquivo:"); scanf("%s",nomearq); //abre o arquivo para escrita em modo binário fp=fopen(nomearq,"wb"); if (fp==NULL){ puts("erro"); getch(); return 0; } // le o numero de elementos a ser lido para o vetor printf("Digite o número de elementos do vetor:"); scanf("%d",&n); // le os números preenchendo o vetor for (i=0;i<n;i++) { printf("Digite o elemento %d",i+1); scanf("%d",&vetint[i]); } // escreve o conteudo do vetor de uma única vez fwrite(vetint,sizeof(int),n,fp); //fecha o arquivo if(fclose(fp)!=0) printf("Erro ao fechar o arquivo"); } Neste exemplo, o vetor foi previamente lido elemento a elemento do teclado. Posteriormente o vetor foi escrito de uma única vez através de fwrite. Veja que o parâmetro count de fwrite foi substituído por n que determina o número de elementos lidos para o vetor. Isso significa que o número de bytes a ser gravado no arquivo nesse caso será igual a sizeof(int)*count a partir do endereço vetint. 1.5.8 Nomes de vetores são ponteiros Observe que o no último exemplo, o parâmetro buffer de fwrite foi substituído apenas por vetint e não por &vetint. Isso ocorre porque na realidade, um nome de vetor em C já é um endereço de memória. Ou seja, o nome de um vetor possui um endereço que aponta para a região da memória onde se encontram as posições daquele vetor. Mais especificamente, um nome de vetor possui o endereço (aponta) de memória da primeira posição do vetor. Por esse motivo, o operador & é desnecessário quando usamos vetores. Por esse mesmo motivo, na função scanf, quando vamos ler uma string (vetor de char), não precisamos usar o operador de endereço.