ARQUIVOS DE ACESSO SEQÜENCIAL

Até agora, toda vez que iniciávamos nossos programas, tínhamos que fornecer os dados necessários. Contudo, algumas vezes gostaríamos de poder guardar os dados que já fornecemos para podermos continuar seu processamento mais tarde.

Por exemplo, ao digitarmos um texto, podemos querer guardá-lo em algum lugar, para então desligarmos o editor de texto. Mais tarde, deveríamos ser capazes de abrir o editor novamente, recuperar o que já digitamos, e então continuar a digitar de onde paramos.

E como fazemos isso? Com arquivos. Arquivos tratam da memória secundária do computador, ou seja, da memória de longo prazo, que pode inclusive ser passada de um computador para outro. E como os arquivos funcionam em C?

Vamos supor que criamos um arquivo e demos um nome a ele. Com isso nosso programa manda uma ordem ao Sistema Operacional para que este crie um arquivo na unidade de armazenagem desejada (HD, disquete etc). Além disso, nosso programa guarda informação sobre em que unidade de armazenagem o arquivo está, seu nome, seu estado (se podemos escrever nele, somente ler etc), além do indicador de posição no arquivo. O indicador de posição diz em que parte do arquivo estamos no momento. Sendo assim, após criarmos o arquivo, temos algo assim:

Suponha que escrevamos "Oba" em nosso arquivo. Nesse caso, o programa deve verificar o indicador de posição e escrever "Oba" naquela posição do arquivo, tomando o cuidado de atualizar o indicador após a operação. Após a escrita nosso arquivo estará assim:

Toda operação de escrita é feita assim, a partir do indicador de posição. Por exemplo, após escrever "Eba" em nosso arquivo, ele ficará:

Mas não é somente escrita que é feita a partir do indicador de posição, também a leitura é feita assim. Ou seja, quando lemos algo de um arquivo, lemos a partir do indicador de posição. Agora note um probleminha: nosso indicador de posição já está no final do arquivo (ao fim da última coisa que escrevemos), como então podemos ler algo? A resposta é: não podemos, a menos que reposicionemos o indicador de posição, levando-o ao começo do arquivo, por exemplo:

Agora sim, podemos ler algo. Suponha que lemos as 3 primeiras letras do arquivo ("Oba", no caso). Após a operação de leitura, o programa deve atualizar novamente o indicador de posição, e nosso arquivo fica:

Note que ler de um arquivo não implica apagar a informação lida.

Agora, isso tudo não é tão simples, não é mesmo? Controlar tudo isso requer uma boa dose de trabalho e atenção. A boa notícia é que o C esconde isso de você. Ou seja, quando você for lidar com arquivos não precisa se preocupar com detalhes como reposicionar o indicador de posição - o C faz isso para você. Contudo, é importante ter em mente como as coisas funcionam, até para entender melhor o comportamento das funções do C que lidam com arquivos.

Arquivos-Texto

Então mãos à massa! Suponha que queiramos fazer um programa de banco de dados simples, um que guarde o nome e a data de aniversário de um grupo de pessoas. Como fazemos? Antes de mais nada, teremos que decidir como ficará nosso banco de dados, ou seja, nosso arquivo. Um modelo bem simples pode ser escrever um nome em uma linha e o aniversário em outra, assim:

Na prática, não há como fazermos o arquivo ficar "ajeitado" como acima. No disco, ele será algo assim:

Mas por questões de clareza, vamos representá-lo como se ele fosse mostrado na tela (com os \n significando nova linha).

Criando o Arquivo

Agora que já definimos o modelo do nosso arquivo, como fazemos para criá-lo em C?

#include <stdio.h>

int main() {
	FILE *arq;

	/* tento criar o arquivo */
	arq = fopen("niver.bd", "w");

	/* vejo se houve problemas */
	if (!arq) {
		printf("Não foi possível criar o arquivo\n");
		exit(1);
	}

	/* fecho o arquivo */
	fclose(arq);
}
		

E o que isso faz? Vamos por partes. A primeira coisa que fizemos foi declarar um ponteiro para arquivo. Este é um ponteiro para as informações que o programa precisa saber sobre o arquivo (como seu nome, o valor do indicador de posição etc). Todas as funções que lidam com arquivos em C usam este ponteiro para saber, dentre outras coisa, em qual arquivo guardar ou buscar a informação, e em que lugar do arquivo fazer isso.

Com "fopen("niver.bd", "w");", tentamos abrir o arquivo de nome "niver.bd" para escrita. O formato geral de fopen é:

fopen(caminho_e_nome_do_arquivo, modo_de_abertura);
		

Note que o nome do arquivo pode vir acompanhado de seu caminho no disco. Se o arquivo for armazenado no mesmo diretório do programa (o que é o nosso caso), podemos omitir o caminho, dando apenas o nome do arquivo (string).

Mas espere! Como podemos abrir o arquivo se ele não existe? Não podemos. O que acontece é que quando usamos fopen com o modo "w", ele cria o arquivo se este não existir. Se já existir um arquivo com o mesmo nome do que queremos criar (no caso, "niver.bd"), este terá seu conteúdo apagado. Ou seja, fopen, com "w", cria o arquivo se ele não existir, e se existir o apaga, substituindo por outro arquivo, vazio, de mesmo nome. Cuidado com isso!

Outra particularidade de fopen é que ela retorna um ponteiro para o arquivo criado, ou NULL em caso de erro. É exatamente por isso que testamos o ponteiro de arquivo e, no caso dele ser NULL, damos a mensagem de erro. Também vale mencionar que, ao abrirmos um arquivo (ou criarmos, como no nosso caso), o indicador de posição do arquivo apontará para seu início.

Por fim, usamos a função fclose para fechar o arquivo. Mas é mesmo necessário usar esta função? No exemplo acima, não, dado que quando o programa termina, todos os arquivos por ele abertos são fechados. Contudo, é uma boa prática sempre fechar os arquivos que abrimos tão logo não precisemos deles. Uma das razões para isso é que temos um número limitado de arquivos que podem permanecer abertos ao mesmo tempo. Este número é dado por FOPEN_MAX, da stdio. Para verificá-lo faça:

	printf("%d\n", FOPEN_MAX);
		

No meu caso, este número é 16. Mas isso varia de compilador para compilador.

É sempre bom lembrar que, ao fecharmos um arquivo, também liberamos seu ponteiro, para que seja usado em outro arquivo, por exemplo (tal qual um ponteiro para uma região alocada com malloc, após fazermos um free). Vale também mencionar que fclose retorna 0 em caso de sucesso, e outro valor em caso de erro. Mais tarde, quando vermos ferror, voltaremos a fclose, verificando qual erro ocorreu.

Também vale lembrar que o nome do arquivo é um string e, como tal, pode estar em uma variável. Por exemplo, o programa abaixo modifica o exemplo acima, pedindo que o usuário forneça um nome para o arquivo:

#include <stdio.h>

int main() {
	FILE *arq;
	char nome[256];

	/* peço o nome do arquivo */
	printf("Entre o nome do arquivo: ");
	gets(nome);

	/* tento criar o arquivo */
	arq = fopen(nome, "w");

	/* vejo se houve problemas */
	if (!arq) {
		printf("Não foi possível criar o arquivo\n");
		exit(1);
	}

	/* fecho o arquivo */
	fclose(arq);
}
		

Escrevendo no Arquivo Recém Criado

Até agora somente criamos o arquivo (pode verificar que ele estará lá, no seu HD, disquete, onde quer que você o tenha criado). E como escrevemos algo no arquivo? Como escrevemos o nome e o aniversário das pessoas lá? Assim:

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

int main() {
	FILE *arq;     /* o arquivo de aniversários */
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */
	char resp;     /* auxiliar do while */

	/* tento criar o arquivo */
	if (!(arq = fopen("niver.bd", "w"))) {
		printf("Não foi possível criar o arquivo\n");
		exit(1);
	}

	do {
		/* peço os dados a serem armazenados */
		printf("Nome: ");
		gets(nome);
		printf("Data (dd/mm): ");
		gets(data);

		/* adiciono nova linha após cada dado (o formato no arquivo) */
		strcat(nome,"\n");
		strcat(data,"\n");

		/* gravo no arquivo */
		if (fputs(nome, arq) == EOF) {
			printf("Erro ao abastecer o arquivo\n");
			exit(1);
		}
		if (fputs(data, arq) == EOF) {
			printf("Erro ao abastecer o arquivo\n");
			exit(1);
		}

		/* vejo se há mais nomes */
		printf("Continua? (s/n) ");
		resp = getchar();
		/* retiro o \n da entrada */
		getchar();
	} while (resp == 's');

	/* fecho o arquivo */
	fclose(arq);
}
		

Uma interação com este programa é:

Nome: Adolfo Dias
Data (dd/mm): 10/12
Continua? (s/n) s
Nome: Caio da Rocha
Data (dd/mm): 15/10
Continua? (s/n) s
Nome: Omar Mota
Data (dd/mm): 03/05
Continua? (s/n) n
		

E ao final, teremos no mesmo diretório do programa, um arquivo chamado "niver.bd", contendo:

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
		

Agora vamos analisar o programa por partes. Já vimos como tudo funciona até a criação do arquivo. Após isso, pedimos ao usuário que nos forneça o nome e a data de aniversário de uma pessoa. Em seguida, adicionamos um \n a cada dado fornecido. Fizemos isso porque nosso modelo exigia que cada dado fosse posto em uma linha separada. Justamente por isso reservamos espaço para 32 caracteres no nome: 30 do nome + '\n' + '\0'.

O próximo passo foi usar a função fputs para guardar os dados no arquivo. A forma geral desta função é:

puts(string_a_ser_armazenado, ponteiro_para_o_arquivo);
		

Note que passamos o ponteiro para o arquivo, ou seja, após abrirmos o arquivo, toda e qualquer operação com ele é feita por meio de seu ponteiro. Fputs tem uma peculiaridade: retorna EOF (definido em stdio.h) caso haja algum erro durante a gravação do string (como um disco protegido, cheio etc). Por isso testamos o valor de retorno de fputs.

Ao final do programa perguntamos ao usuário se ele tem outro nome para nos fornecer. Apenas um parênteses aqui: quando digitamos a letra e batemos enter, na verdade digitamos 2 caracteres - a letra e um '\n'. Quando lemos a letra, o \n fica lá, atrapalhando nossa vida na próxima leitura. Isso acontece devido ao modo como o getchar (e o scanf) funcionam. A estratégia que usamos para evitar isso é usar outro getchar para ler o \n.

Lendo do Arquivo

Agora que já criamos um arquivo, vamos ler seu conteúdo. Da mesma forma que usamos fputs para guardar no arquivo, usamos fgets para ler:

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

int main() {
	FILE *arq;     /* o arquivo de aniversários */
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */

	/* tento abrir o arquivo */
	if (!(arq = fopen("niver.bd", "r"))) {
		printf("Não foi possível abrir o arquivo\n");
		exit(1);
	}

	while (!feof(arq)) {
		/* leio o nome do arquivo */
		if (!fgets(nome, 32, arq) && !feof(arq)) {
			printf("Erro ao ler nome do arquivo\n");
			exit(1);
		}

		/* leio o aniversário do arquivo */
		if (!fgets(data, 7, arq) && !feof(arq)) {
			printf("Erro ao ler data do arquivo\n");
			exit(1);
		}

		/* imprimo na tela */
		if (!feof(arq)) {
			printf("Nome: %s", nome);
			printf("data: %s",data);
		}
	}

	/* fecho o arquivo */
	fclose(arq);
}
		

A saída do programa é:

Nome: Adolfo Dias
data: 10/12
Nome: Caio da Rocha
data: 15/10
Nome: Omar Mota
data: 03/05
		

E como isso funciona? Antes de mais nada, devemos abrir o arquivo para leitura. Daí o uso do "r" no fopen. Em seguida, usamos feof para verificar se o arquivo chegou ao fim.

A forma geral de feof é:

feof(ponteiro_para_arquivo);
		

sendo que ela retorna verdadeiro (não zero) se o final do arquivo foi atingido, ou zero se não. Para descobrir isso, feof verifica nosso amigo indicador de posição do arquivo. O grande problema de feof é que ela espera uma leitura no arquivo para verificar algo. Por isso estamos testando a toda hora se o final do arquivo foi atingido após cada leitura. Apesar disso, feof é extremamente útil com arquivos binários, coisa que não veremos neste curso.

Bom, mas e como lemos do arquivo? Com o fprintf. A forma geral do fprintf é:

fprintf(string_de_armazenagem, tamanho, ponteiro_de_arquivo);
		

Basicamente, fgets lê um string do arquivo até que um \n seja lido, ou que tamanho-1 caracteres sejam lidos. Ela então guarda o string lido no string de armazenagem. Naturalmente, se um EOF for lido, indicando o final do arquivo, scanf parará de ler.

Um detalhe importante sobre fgets é que se ela ler um \n ele fará parte do string. Além disso, ela inclui o \0 final para nós. Justamente por isso definimos 32 caracteres para um nome de apenas 30. Os outros 2 guardarão um possível \n, vindo da fscanf, e o \0 final.

A função fscanf retorna um ponteiro para o string lido, ou NULL se um erro ocorrer. O grande problema é que NULL também é retornado caso o fim do arquivo tenha sido atingido. Ou seja, temos que fazer testes usando feof (ou ferror, que veremos mais adiante) para verificar se o NULL se deve ao final do arquivo ou a um erro de leitura.

Agora podemos juntar os dois algoritmos em um só programa:

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

/*
	Carrega o arquivo com os nomes dados pelo usuário. Retorna ponteiro
	para o arquivo ou NULL em caso de erro.

	Param:
		nomeArq - o nome do arquivo a ser criado (e caminho, opcional)
*/
FILE *criaArq(char nomeArq[]) {
	FILE *arq;     /* o arquivo de aniversários */
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */
	char resp;     /* auxiliar do while */

	/* tento criar o arquivo */
	if (!(arq = fopen("niver.bd", "w"))) {
		return(NULL);
	}

	do {
		/* peço os dados a serem armazenados */
		printf("Nome: ");
		gets(nome);
		printf("Data (dd/mm): ");
		gets(data);

		/* adiciono nova linha após cada dado (o formato no arquivo) */
		strcat(nome,"\n");
		strcat(data,"\n");

		/* gravo no arquivo */
		if (fputs(nome, arq) == EOF) {
			return(NULL);
		}
		if (fputs(data, arq) == EOF) {
			return(NULL);
		}

		/* vejo se há mais nomes */
		printf("Continua? (s/n) ");
		resp = getchar();
		/* retiro o \n da entrada */
		getchar();
	} while (resp == 's');

	/* fecho o arquivo */
	fclose(arq);

	/* retorno o ponteiro */
	return(arq);
}


/*
	Lê o conteúdo do arquivo, imprimindo na tela. Retorna 1 se tudo correr
	bem, e 0 em caso de erro.

	Param:
		arq - o arquivo a ser lido
*/
int leArq(FILE *arq) {
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */

	/* tento abrir o arquivo */
	if (!(arq = fopen("niver.bd", "r"))) {
		return(0);
	}

	while (!feof(arq)) {
		/* leio o nome do arquivo */
		if (!fgets(nome, 32, arq) && !feof(arq)) {
			return(0);
		}

		/* leio o aniversário do arquivo */
		if (!fgets(data, 7, arq) && !feof(arq)) {
			return(0);
		}

		/* imprimo na tela */
		if (!feof(arq)) {
			printf("Nome: %s", nome);
			printf("data: %s",data);
		}
	}

	/* fecho o arquivo */
	fclose(arq);

	/* tudo correu bem */
	return(1);
}

int main() {
	FILE *arq;

	/* crio o arquivo */
	if (!(arq = criaArq("niver.bd"))) {
		printf("Erro ao criar o arquivo\n");
		exit(1);
	}

	/* leio seu conteúdo */
	if (!leArq(arq))
		printf("Erro ao ler o arquivo\n");
}
		

Escrevendo ao Final de Arquivo Já Existente

Ótimo! Agora já sabemos criar um arquivo, abastecê-lo de dados, e ler seu conteúdo. Contudo, como faremos para, após ele ter sido criado (possivelmente em outro dia ou outro computador), colocar mais dados nele? Já vimos que "r" serve somente para leitura, e que "w" vai apagar qualquer arquivo existente. O que fazer então? Usar "a". "a" faz com que fopen abra o arquivo para inclusão em seu final. Veja como:

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

int main() {
	FILE *arq;     /* o arquivo de aniversários */
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */
	char resp;     /* auxiliar do while */

	/* tento abrir o arquivo */
	if (!(arq = fopen("niver.bd", "a"))) {
		printf("Não foi possível abrir o arquivo\n");
		exit(1);
	}

	do {
		/* peço os dados a serem armazenados */
		printf("Nome: ");
		gets(nome);
		printf("Data (dd/mm): ");
		gets(data);

		/* adiciono nova linha após cada dado (o formato no arquivo) */
		strcat(nome,"\n");
		strcat(data,"\n");

		/* gravo no arquivo */
		if (fputs(nome, arq) == EOF) {
			printf("Erro ao abastecer o arquivo\n");
			exit(1);
		}
		if (fputs(data, arq) == EOF) {
			printf("Erro ao abastecer o arquivo\n");
			exit(1);
		}

		/* vejo se há mais nomes */
		printf("Continua? (s/n) ");
		resp = getchar();
		/* retiro o \n da entrada */
		getchar();
	} while (resp == 's');

	/* fecho o arquivo */
	fclose(arq);
}
		

Uma possível interação com o programa acima é:

Nome: Jacinto Dores
Data (dd/mm): 04/11
Continua? (s/n) s
Nome: Armando Pinto
Data (dd/mm): 01/01
Continua? (s/n) n
		

Se nosso arquivo continha:

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
		

Agora conterá:

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Jacinto Dores
04/11
Armando Pinto
01/01
		

Vendo isso você já deve estar imaginando o que o "a" faz: ele simplesmente abre o arquivo para escrita, posicionando o indicador de posição no final do arquivo. Também vale lembrar que, se o arquivo não existir, ele será criado. Sendo assim, a grande diferença entre "a" e "w" é que o "w" elimina qualquer arquivo já existente, enquanto que o "a" adiciona dados ao seu final, mantendo os dados que lá estavam.

Apagando Informação do Meio do Arquivo

Apagar uma informação que está no meio de um arquivo é uma operação não muito fácil. O modo mais direto de fazer isso é criar um outro arquivo, que conterá todo o conteúdo do arquivo original, exceto a informação a ser apagada. Ao final disso, substituímos o arquivo original pelo novo arquivo, sem a informação.

Vamos então fazer um programa para retirar "Jacinto Dores" do arquivo acima:

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

int main() {
	FILE *arqAnt;
	FILE *arqNovo;
	char nome[32]; /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];  /* data do aniversário (5 caracteres) */
	char resp;     /* auxiliar do while */

	/* abro o arquivo original para leitura */
	if (!(arqAnt = fopen("niver.bd", "r"))) {
		printf("Não foi possível abrir o arquivo niver.bd\n");
		exit(1);
	}

	/* crio o arquivo auxiliar */
	if (!(arqNovo = fopen("aux.bd", "w"))) {
		printf("Não foi possível criar o arquivo auxiliar\n");
		exit(1);
	}

	/* corro o arquivo original */
	while (!feof(arqAnt)) {
		/* leio o nome do arquivo */
		if (!fgets(nome, 32, arqAnt) && !feof(arqAnt)) {
			return(0);
		}

		/* leio o aniversário do arquivo */
		if (!fgets(data, 7, arqAnt) && !feof(arqAnt)) {
			return(0);
		}

		/* verifico os dados */
		if (!feof(arqAnt)) {
			if (strcmp(nome, "Jacinto Dores\n")) {
				/* não é o nome a ser excluido, escrevo no novo arquivo */
				if (fputs(nome, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
				if (fputs(data, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
			}
			/* se for Jacinto Dores, não copio para o novo */
		}
	}

	/* fecho os arquivos */
	fclose(arqAnt);
	fclose(arqNovo);

	/* apago o arquivo original */
	if (remove("niver.bd")) {
		printf("Erro ao remover niver.bd\n");
		exit(1);
	}

	/* troco o nome do novo */
	if (rename("aux.bd", "niver.bd")) {
		printf("Erro ao renomear aux.bd\n");
		exit(1);
	}
}
		

Suponha que tínhamos o arquivo:

Arquivo: niver.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Jacinto Dores
04/11
Armando Pinto
01/01
		

Após o while, temos dois arquivos:

Arquivo: niver.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Jacinto Dores
04/11
Armando Pinto
01/01


Arquivo: aux.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Armando Pinto
01/01
		

E após o fim do programa, temos somente um:

Arquivo: niver.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Armando Pinto
01/01
		

A parte nova nisso tudo são as funções remove e rename (ambas da stdio). Remove tenta apagar o arquivo cujo nome lhe é passado como parâmetro, retornando 0 em caso de sucesso e diferente de zero em caso de erro.

Rename possui a seguinte forma geral:

rename(nome_antigo, nome_novo);
		

Rename tenta mudar o nome do arquivo (de nome_antigo para nome_novo), retornando 0 em caso de sucesso e diferente de zero em caso de erro. Um dos erros que esta função pode acusar é quando já existir um arquivo de nome igual a nome_novo, nesse caso ela não apaga o arquivo existente, simplesmente retornando um erro.

Substituindo Informação do Meio do Arquivo

A substituição da informação contida no meio do arquivo se dá da mesma forma que a exclusão de uma informação. Só que agora, em vez de ignorarmos a informação na hora de copiar de um arquivo para outro, simplesmente trocamos a informação pela nova:

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

int main() {
	FILE *arqAnt;
	FILE *arqNovo;
	char nome[32];  /* nome do aniversariante (máximo = 30 caracteres) */
	char data[7];   /* data do aniversário (5 caracteres) */
	char resp;      /* auxiliar do while */
	char nomeN[32]; /* novo nome */
	char dataN[7];  /* nova data */

	/* pego o novo nome e data */
	printf("Nome: ");
	gets(nomeN);
	printf("Data: ");
	gets(dataN);

	/* incluo a nova linha */
	strcat(nomeN, "\n");
	strcat(dataN, "\n");

	/* abro o arquivo original para leitura */
	if (!(arqAnt = fopen("niver.bd", "r"))) {
		printf("Não foi possível abrir o arquivo niver.bd\n");
		exit(1);
	}

	/* crio o arquivo auxiliar */
	if (!(arqNovo = fopen("aux.bd", "w"))) {
		printf("Não foi possível criar o arquivo auxiliar\n");
		exit(1);
	}

	/* corro o arquivo original */
	while (!feof(arqAnt)) {
		/* leio o nome do arquivo */
		if (!fgets(nome, 32, arqAnt) && !feof(arqAnt)) {
			return(0);
		}

		/* leio o aniversário do arquivo */
		if (!fgets(data, 7, arqAnt) && !feof(arqAnt)) {
			return(0);
		}

		/* verifico os dados */
		if (!feof(arqAnt)) {
			if (strcmp(nome, "Jacinto Dores\n")) {
				/* não é o nome a ser excluido, escrevo no novo arquivo */
				if (fputs(nome, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
				if (fputs(data, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
			}
			else {
				/* é Jacinto Dores, copio os novos dados */
				if (fputs(nomeN, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
				if (fputs(dataN, arqNovo) == EOF) {
					printf("Erro ao abastecer o arquivo\n");
					exit(1);
				}
			}
		}
	}

	/* fecho os arquivos */
	fclose(arqAnt);
	fclose(arqNovo);

	/* apago o arquivo original */
	if (remove("niver.bd")) {
		printf("Erro ao remover niver.bd\n");
		exit(1);
	}

	/* troco o nome do novo */
	if (rename("aux.bd", "niver.bd")) {
		printf("Erro ao renomear aux.bd\n");
		exit(1);
	}
}
		

Aqui substituímos "Jacinto Dores" pela nova pessoa dada pelo usuário. Suponha que tínhamos o arquivo:

Arquivo: niver.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Jacinto Dores
04/11
Armando Pinto
01/01
		

Após o while, temos dois arquivos:

Arquivo: niver.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Jacinto Dores
04/11
Armando Pinto
01/01


Arquivo: aux.bd

Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Rolando da Rocha
05/12
Armando Pinto
01/01
		

E após o fim do programa, temos somente um:

Arquivo: niver.bd
Adolfo Dias
10/12
Caio da Rocha
15/10
Omar Mota
03/05
Rolando da Rocha
05/12
Armando Pinto
01/01
		

Outro Exemplo - Jogos

Outro uso muito comum para arquivos é em jogos. Nesse caso, o arquivo guarda o estado atual de um jogo. Por exemplo, considere o jogo da vida. Podemos programá-lo tal que o usuário possa salvar o estado atual do jogo, e depois carregá-lo.

E qual formato usaremos? Uma possibilidade é, em um arquivo, guardarmos as dimensões da matriz, bem como as coordenadas de cada célula viva, que usaríamos para preencher a matriz. Ou seja, o arquivo:

10 5
5 2
3 3
9 4
		

indicaria um "mundo" matricial de 10 linhas × 5 colunas, com células vivas nas coordenadas (5,2), (3,3) e (9,4).

A questão agora é: como lemos estes dados? E a resposta é: como líamos tudo até agora: como string. Veja:

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

int maxL; /* número máximo de linhas */
int maxC; /* número máximo de colunas */
char *geracao1; /* primeira geração */
char *geracao2; /* segunda geração */
char nomeArq[] = "dados.jv"; /* arquivo do jogo */


/*
	Aloca memória para as matrizes geracao1 e geracao2.
	Retorna 1 se tudo ok e 0 em caso de erro.
*/
int criaMundo() {
	if (!(geracao1 = (char *)malloc(maxL*maxC*sizeof(char))))
		return(0);

	if (!(geracao2 = (char *)malloc(maxL*maxC*sizeof(char))))
		return(0);
}


/*
	Mostra o tabuleiro.
*/
void mostra() {
	int i,j; /* contadores */

	printf("   ");
	for (j=0; j<maxC; j++) {
		printf(" %2d ",j);
	}
	printf("\n   +");
	for (j=0; j<maxC; j++)
		printf("---+");
	printf("\n");

	for (i=0; i<maxL; i++) {
		printf("%2d |",i);
		for (j=0; j<maxC; j++)
			printf(" %c |",*(geracao1 + (i*maxC) +j));
		printf("\n   +");
		for (j=0; j<maxC; j++)
			printf("---+");
		printf("\n");
	}
}


/*
	Esvazio a matriz.
*/
void esvazia() {
	int i,j; /* contadores */

	for (i=0; i<maxL; i++)
		for (j=0; j<maxC; j++)
			*(geracao1 + (i*maxC) +j) = ' ';
}


/*
	Crio o mundo conforme o que há no arquivo. Retorna 1 se ok e 0 em caso de
	erro.
*/
int leMundo() {
	FILE *arq;       /* o arquivo */
	char linha[101]; /* linha no arquivo */
	int l;           /* linha de uma coordenada */
	int c;           /* coluna de uma coordenada */

	/* abro o arquivo para leitura */
	if (!(arq = fopen(nomeArq, "r"))) {
		return(0);
	}

	/* leio o tamanho da matriz do arquivo */
	if (!fgets(linha, 100, arq)) {
		return(0);
	}

	/* retiro da linha os tamanhos */
	sscanf(linha, "%d %d", &maxL, &maxC);

	/* crio a matriz do mundo */
	if (!criaMundo()) return(0);

	/* zero a matriz */
	esvazia();

	/* leio as posições, abastecendo na matriz */
	while (!feof(arq)) {
		/* leio a linha */
		if (!fgets(linha, 100, arq) && !feof(arq)) {
			return(0);
		}

		/* abasteço a matriz */
		if (!feof(arq)) {
			/* retiro as posições */
			sscanf(linha, "%d %d", &l, &c);

			/* atualizo a matriz */
			*(geracao1 + (l*maxC) + c) = 'c';
		}
	}

	/* fecho o arquivo */
	fclose(arq);

	/* deu tudo certo */
	return(1);
}

int main() {
	/* leio o tabuleiro */
	if (!leMundo()) {
		printf("Erro ao ler o tabuleiro\n");
		exit(1);
	}

	/* imprimo o tabuleiro */
	mostra();
}
		

O grande truque aqui foi ler do string os valores inteiros das coordenadas. Isso foi feito pela função sscanf. Sscanf funciona exatamente como a scanf, exceto que, em vez de pegar os valores do teclado, ela pega do string que é dado a ela. Sua forma geral é:

sscanf(string_fonte, string_formatador, lista_de_variáveis);
		

A grande vantagem do uso de arquivos-texto é que podemos abrir o arquivo "dados.jv" com qualquer editor de textos e editar seu conteúdo, incluindo ou excluindo células, ou ainda mudando o tamanho do mundo.

Por falar nisso, como fazemos para salvar o estado atual do jogo?

/*
	Salva o estado atual do jogo. Retorna 1 se tudo ok e 0 em caso de falha.
*/
int salva() {
	FILE *arq;       /* o arquivo de aniversários */
	char linha[101]; /* linha no arquivo */
	int i;           /* auxiliar */
	int j;           /* auxiliar */

	/* tento criar o arquivo */
	if (!(arq = fopen(nomeArq, "w"))) {
		return(0);
	}

	/* guardo as dimensões da matriz */
	sprintf(linha,"%d %d\n", maxL, maxC);
	if (fputs(linha, arq) == EOF) return(0);

	/* corro a matriz, salvando as posições das células */
	for (i=0; i

		

Aqui usamos o parente de sscanf: sprintf. Sprintf é igual a printf, exceto que, em vez de escrever na tela, ela escreve na string. Sua forma geral é:

sprintf(string_alvo, string_formatador, lista de variáveis);
		

Pronto! Agora temos um modo de escrevermos valores numéricos nos arquivos, com a vantagem de tratar tudo como string.

Porém arquivos não terminam aqui. Há muito mais para ser visto. É aconselhada a leitura da bibliografia, especialmente o livro "C Completo e Total", do Herbert Scildt.

Stdin, stdout

Toda vez que executamos um programa, 3 ponteiros de arquivos são automaticamente criados e abertos: stdin, stdout e stderr. Aqui veremos apenas stdin e stdout.

stdout é a saida padrão (geralmente a tela). Ela se comporta como um arquivo (que foi aberto ao início do programa), sendo que toda vez que escrevemos algo nela, ela escreve na tela (e não em algum arquivo).

Por exemplo, como excrevíamos "Oba\n" na tela? Com printf:

printf("Oba\n");
		

Contudo, existe um outro meio de escrevermos um string (e somente string) na tela:

puts("Oba");
		

puts escreve o string (tanto constante string, como "Oba", quanto uma variável string) passado a ela como parâmetro na tela, dando um '\n' automático após isso. Uma terceira maneira de escrever este string é usando fputs:

fputs("Oba\n",stdout);
		

Note que fputs coloca o string no arquivo, porém, como o ponteiro de arquivo passado é stdout, o string será escrito na tela.

Ou seja, as 3 formas abaixo fazem exatamente a mesma coisa:

	printf("Oba\n");
	puts("Oba");
	fputs("Oba\n", stdout);
		

stdin, por outro lado, é a entrada padrão (geralmente o teclado). Como líamos um string do teclado? Com gets:

char str[10];

gets(str);
		

E usando fgets?

char str[10];

fgets(str, 10, stdin);
		

Porém elas não fazem a mesma coisa. Há 2 diferenças básicas entre gets e fgets. A primeira é que gets retorna o string lido até o \n, mas sem incluir o \n no string. fgets, por outro lado, inclui o \n. Por exemplo, os dois fragmentos abaixo fazem a mesma coisa (note que no primeiro printf tivemos que por o \n, enquanto que no segundo não):

	char str[10];

	gets(str);
	printf("Você digitou: %s\n",str);

	fgets(str, 10, stdin);
	printf("Você digitou: %s",str);
		

A segunda, e maior diferença, aparece quando, no nosso caso, digitamos um string de tamanho 21, por exemplo. Usando o código acima, o que acontece se digitarmos um string de tamanho 21?

string grande demais
Você digitou: string grande demais
string grande demais
Você digitou: string gr
		

No caso do gets ele colocou um string de tamanho 21 onde só cabia um de tamanho 10 - ele escreveu em memória não alocada, o que poderia ter sobrescrito outros dados do programa. O fgets, por outro lado, parou de ler ao 9º caracter, armazenando corretamente apenas 10 caracteres no string. Isso evita escrevermos além do limite do string, ou seja, além do limite da memória alocada (o que é conhecido como buffer overflow).




Copyright© 2005 Norton Trevisan Roman - Direitos Autorais Reservados.