Até agora, tudo que nossos programas produziram morreu com eles: feche o programa e os dados somem. Isso muda hoje. Vamos aprender a gravar informações em arquivos no disco e a lê-las de volta — dar aos nossos dados uma vida que sobrevive ao encerramento do programa. É o que transforma um exercício num software de verdade: um que lembra. E, como sempre no C, isso vem com responsabilidades — abrir, usar e fechar cada arquivo com disciplina.
O conceito de fluxo e o tipo FILE
Em C, a comunicação com um arquivo acontece através de um fluxo (stream), representado por um ponteiro para um tipo especial chamado FILE, definido em <stdio.h>. Você não manipula o FILE diretamente; você guarda um ponteiro para ele (FILE *) e o passa às funções de arquivo. Esse ponteiro é como uma "alça" que representa sua conexão com o arquivo aberto.
Na verdade, você já usou fluxos sem saber: stdin (entrada padrão, o teclado), stdout (saída padrão, a tela) e stderr (saída de erro) são três fluxos FILE * que o C abre automaticamente para todo programa. Trabalhar com arquivos é, essencialmente, criar seus próprios fluxos e usá-los com as mesmas ideias.
O ciclo de vida de um arquivo: abrir, usar, fechar
Todo trabalho com arquivo segue três passos invioláveis. Você abre o arquivo com fopen, que devolve um FILE *. Você usa esse ponteiro para ler ou escrever. E você fecha o arquivo com fclose, liberando os recursos. Esse ciclo espelha o malloc/free da memória: assim como toda alocação pede uma liberação, todo fopen pede um fclose.
#include <stdio.h>
int main(void) {
// 1. ABRIR: modo "w" (write) cria/sobrescreve para escrita
FILE *arquivo = fopen("saida.txt", "w");
// fopen pode falhar (disco cheio, permissão negada...) e retorna NULL
if (arquivo == NULL) {
printf("Erro ao abrir o arquivo.\n");
return 1;
}
// 2. USAR: fprintf é como printf, mas escreve no arquivo
fprintf(arquivo, "Primeira linha\n");
fprintf(arquivo, "Número: %d\n", 42);
// 3. FECHAR: garante que tudo foi gravado e libera o recurso
fclose(arquivo);
printf("Arquivo gravado com sucesso.\n");
return 0;
}
Três lições essenciais aqui. fopen pode falhar — o arquivo pode não existir, faltar permissão, o disco estar cheio — e nesses casos retorna NULL; verificar isso antes de usar o ponteiro é obrigatório, exatamente como verificamos o retorno de malloc. fprintf é o gêmeo de printf que escreve num fluxo em vez da tela — mesma sintaxe de formato, só com o FILE * como primeiro argumento. E fclose não é opcional: além de liberar o recurso, ele garante que dados ainda em buffer sejam efetivamente escritos no disco.
Os modos de abertura
O segundo argumento de fopen é o modo, uma string que diz o que você pretende fazer com o arquivo. Os três fundamentais:
O modo "w" (write) abre para escrita, criando o arquivo se não existir e apagando todo o conteúdo se existir. Cuidado: ele destrói o que estava lá. O modo "r" (read) abre para leitura; se o arquivo não existir, fopen retorna NULL. E o modo "a" (append) abre para escrita acrescentando ao final, preservando o conteúdo existente e criando o arquivo se preciso. Há variações com + (como "r+" para leitura e escrita) e o sufixo "b" para modo binário, que é o tema da próxima aula. Por ora, fixe estes três: "w" sobrescreve, "a" acrescenta, "r" lê.
Lendo um arquivo linha a linha
Para ler texto, a ferramenta mais segura e idiomática é o fgets que conhecemos na aula anterior — só que agora, em vez de stdin, passamos o nosso FILE *. Lemos linha a linha até o arquivo acabar:
#include <stdio.h>
int main(void) {
FILE *arquivo = fopen("saida.txt", "r"); // abre para leitura
if (arquivo == NULL) {
printf("Não foi possível abrir o arquivo.\n");
return 1;
}
char linha[256];
// fgets retorna NULL quando chega ao fim do arquivo
while (fgets(linha, sizeof(linha), arquivo) != NULL) {
printf("Lido: %s", linha); // a linha já inclui o '\n'
}
fclose(arquivo);
return 0;
}
O padrão é elegante e seguro: fgets lê uma linha por vez (respeitando o limite do buffer, sem risco de estouro) e devolve NULL ao chegar ao fim do arquivo (EOF). Assim, o while (fgets(...) != NULL) percorre o arquivo inteiro, linha após linha, parando naturalmente no fim. Note que a linha lida inclui o \n do final — por isso o printf("Lido: %s", ...) não adiciona outro. Este é o jeito recomendado de ler arquivos de texto em C.
Lendo caractere a caractere
Para controle mais fino, ou para processar o arquivo byte a byte, existe fgetc, que lê um único caractere por vez. Ele devolve o caractere lido ou o valor especial EOF ao terminar:
#include <stdio.h>
int main(void) {
FILE *arquivo = fopen("saida.txt", "r");
if (arquivo == NULL) return 1;
int c; // note: int, não char! EOF não caberia num char
int contador = 0;
while ((c = fgetc(arquivo)) != EOF) {
putchar(c); // reimprime o caractere na tela
contador++;
}
fclose(arquivo);
printf("\nTotal de caracteres: %d\n", contador);
return 0;
}
Um detalhe sutil, mas importante: a variável que recebe o retorno de fgetc deve ser int, não char. Isso porque EOF é um valor especial (tipicamente -1) que precisa ser distinguível de qualquer caractere válido, e um char poderia não representá-lo corretamente. Usar int aqui é uma daquelas convenções do C que parecem estranhas até você entender o motivo — e cuja violação causa bugs sutis de fim de arquivo.
Um programa completo: gravar e reler
Vamos juntar tudo num programa que grava uma lista de tarefas e depois a relê:
#include <stdio.h>
int main(void) {
// --- Fase de escrita ---
FILE *escrita = fopen("tarefas.txt", "w");
if (escrita == NULL) {
fprintf(stderr, "Erro ao criar arquivo.\n"); // erros vão pro stderr
return 1;
}
const char *tarefas[] = {"Estudar ponteiros", "Praticar arquivos", "Revisar structs"};
for (int i = 0; i < 3; i++) {
fprintf(escrita, "%d. %s\n", i + 1, tarefas[i]);
}
fclose(escrita);
// --- Fase de leitura ---
FILE *leitura = fopen("tarefas.txt", "r");
if (leitura == NULL) {
fprintf(stderr, "Erro ao ler arquivo.\n");
return 1;
}
printf("=== Lista de Tarefas ===\n");
char linha[128];
while (fgets(linha, sizeof(linha), leitura) != NULL) {
printf("%s", linha);
}
fclose(leitura);
return 0;
}
Repare no uso de fprintf(stderr, ...) para as mensagens de erro. Enquanto stdout é para a saída normal do programa, stderr é o canal apropriado para erros e diagnósticos — uma convenção importante que permite, por exemplo, separar a saída útil das mensagens de erro ao redirecionar a saída de um programa. Note também que cada fopen tem seu fclose correspondente, e que verificamos o retorno de ambas as aberturas. Essa disciplina é o que torna o código robusto.
O padrão de robustez: sempre feche o que abriu
A regra de ouro dos arquivos ecoa a da memória: todo fopen bem-sucedido precisa de um fclose. Esquecer o fclose causa um "vazamento de arquivo" — o sistema operacional mantém o arquivo aberto e reserva recursos para ele. Em programas pequenos, isso é recuperado no encerramento; em servidores de longa duração, esgotar o limite de arquivos abertos derruba a aplicação. Pior: sem o fclose, dados que ficaram no buffer podem nunca chegar ao disco, e você perde informação silenciosamente. A mesma disciplina de par (abrir/fechar, como alocar/liberar) que vem nos guiando desde a Fase 2 se aplica aqui integralmente.
O que vem a seguir
Hoje demos aos nossos programas memória duradoura: aprendemos a abrir arquivos com fopen (e seus modos "w", "r", "a"), a escrever com fprintf, a ler com fgets e fgetc, e a sempre fechar com fclose, verificando falhas em cada passo. Vimos também os fluxos padrão stdin, stdout e stderr. Mas até aqui gravamos apenas texto — caracteres legíveis. Na próxima aula, exploraremos os arquivos binários, que armazenam os bytes exatos dos seus dados (structs inteiras, por exemplo) de forma compacta e eficiente, e o posicionamento livre dentro de um arquivo com fseek — abrindo caminho para formas mais poderosas de persistência.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 7.5, File Access
- cppreference —
fopene os modos de abertura — https://en.cppreference.com/w/c/io/fopen - cppreference —
fgets,fgetc,fprintfpara arquivos — https://en.cppreference.com/w/c/io - Modern C, Jens Gustedt — capítulo sobre entrada e saída com arquivos — https://gustedt.gitlabpages.inria.fr/modern-c/
- CERT C — FIO série (FIO30-C a FIO47-C), sobre uso seguro de arquivos — https://wiki.sei.cmu.edu/confluence/display/c
Exercícios
Exercício 1
Escreva um programa que crie um arquivo nomes.txt no modo "w" e grave nele três nomes, um por linha, usando fprintf. Verifique se o fopen teve sucesso antes de escrever.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
FILE *f = fopen("nomes.txt", "w");
if (f == NULL) {
fprintf(stderr, "Erro ao abrir.\n");
return 1;
}
fprintf(f, "Ana\n");
fprintf(f, "Bruno\n");
fprintf(f, "Carla\n");
fclose(f);
return 0;
}
Exercício 2
Escreva um programa que abra o nomes.txt do exercício 1 no modo "r" e imprima cada linha na tela, usando fgets num laço while. Confirme que a saída bate com o que foi gravado.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
FILE *f = fopen("nomes.txt", "r");
if (f == NULL) {
fprintf(stderr, "Erro ao abrir.\n");
return 1;
}
char linha[100];
while (fgets(linha, sizeof(linha), f) != NULL) {
printf("%s", linha);
}
fclose(f);
return 0;
}
A saída reproduz os três nomes gravados, um por linha.
Exercício 3
Modifique o programa do exercício 1 para usar o modo "a" em vez de "w". Rode-o duas vezes e observe o conteúdo do arquivo. Explique a diferença de comportamento entre "w" e "a".
Ver resposta
✓ Resposta: Trocando "w" por "a" no exercício 1 e rodando duas vezes, o arquivo conterá os três nomes duas vezes (seis linhas no total). A diferença: o modo "w" apaga todo o conteúdo existente ao abrir, começando do zero a cada execução — por isso, rodá-lo várias vezes sempre deixa só três nomes. O modo "a" preserva o conteúdo e acrescenta ao final, então cada execução adiciona mais três linhas às que já estavam lá. Use "w" para recomeçar e "a" para ir acumulando (como num arquivo de log).
Exercício 4
Escreva um programa que conte quantas linhas e quantos caracteres um arquivo de texto possui. Use fgetc num laço, contando os caracteres e incrementando o contador de linhas a cada \n encontrado.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
FILE *f = fopen("nomes.txt", "r");
if (f == NULL) return 1;
int c;
int caracteres = 0, linhas = 0;
while ((c = fgetc(f)) != EOF) {
caracteres++;
if (c == '\n') {
linhas++;
}
}
fclose(f);
printf("Linhas: %d, Caracteres: %d\n", linhas, caracteres);
return 0;
}
Contamos cada caractere lido e incrementamos o contador de linhas sempre que encontramos um \n. (Nota: se o arquivo não terminar com \n, a última linha não seria contada por este método — um detalhe que ferramentas como wc também consideram.)
Exercício 5
Explique por que a variável que recebe o retorno de fgetc deve ser int e não char. O que poderia dar errado se fosse char?
Ver resposta
✓ Resposta: A variável deve ser int porque fgetc precisa devolver, além de todos os 256 valores possíveis de um byte, um valor extra — EOF — para sinalizar o fim do arquivo (ou um erro). EOF é tipicamente -1. Se você usasse char, dois problemas surgiriam: primeiro, num sistema onde char é unsigned, o valor -1 de EOF nunca seria representado corretamente, e o laço while (c != EOF) poderia nunca terminar; segundo, num char signed, um byte válido com valor 0xFF (que é -1 como char) seria confundido com EOF, encerrando a leitura no meio do arquivo. Usar int garante espaço para distinguir todos os bytes válidos do marcador EOF, evitando ambos os bugs.