Reforço de Algoritmos e Programação - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



Aula 11 - Arquivos Texto e Binários

Até agora, todos os dados dos nossos programas existiam apenas enquanto o programa estava em execução: ao fechar o programa, tudo se perdia. Nesta aula, veremos como gravar e recuperar dados em arquivos, permitindo que informações sobrevivam entre execuções.


1. Por que usar arquivos?

Um arquivo é um conjunto de dados armazenado em memória secundária (disco rígido, SSD, etc.). Diferentemente da memória principal (RAM), que vimos desde a Aula 1 e é volátil e rápida, a memória secundária é persistente: os dados sobrevivem mesmo após desligar o computador.

Memória Principal (RAM) Memória Secundária (Disco)
Velocidade Rápida Lenta (em comparação)
Persistência Volátil — dados são perdidos ao desligar Persistente — dados sobrevivem
Capacidade Limitada (gigabytes) Grande (terabytes)
Uso típico Executar programas, dados temporários Armazenar dados permanentemente

Arquivos são, no fundo, apenas sequências de bytes. Não importa se chamamos um arquivo de "texto" ou "binário": internamente, tudo são bytes (números de 0 a 255). A diferença está inteiramente em como interpretamos esses bytes:

  • Arquivos de texto: os bytes representam caracteres (ASCII ou UTF-8), e por isso são legíveis diretamente por humanos, em qualquer editor de texto. Exemplos: .txt, .csv, .html.
  • Arquivos binários: os bytes representam dados em um formato específico (números em sua representação interna, structs completas, dados compactados), sem relação direta com caracteres legíveis. Exemplos: .jpg, .mp3, .exe.
Por que essa distinção importa na prática? Um número inteiro como 12345, guardado em um arquivo de texto, ocupa 5 bytes (um por dígito, como caracteres '1', '2', ...). O mesmo número, guardado em um arquivo binário como int, ocupa sempre 4 bytes (o tamanho de um int em memória), independente de quantos dígitos ele tenha. Arquivos binários são mais compactos e mais rápidos de ler/escrever (não é preciso converter texto para número e vice-versa), mas exigem que quem for ler o arquivo saiba exatamente o formato usado — não dá para simplesmente abrir e ler visualmente.

2. Abrindo e fechando arquivos

Para trabalhar com arquivos em C, usamos um ponteiro do tipo FILE (um tipo definido em <stdio.h>, que representa um arquivo aberto). A função fopen() abre um arquivo e retorna um ponteiro para ele — sempre verifique se essa abertura foi bem-sucedida, checando se o retorno é NULL.

#include <stdio.h>

int main() {
    // Abrindo um arquivo em modo leitura
    FILE *arquivo = fopen("dados.txt", "r");

    // Verificando se abriu corretamente
    if (arquivo == NULL) {
        printf("Erro: nao foi possivel abrir o arquivo\n");
        return 1;
    }

    // ... operações com o arquivo ...

    // Fechando sempre ao final
    fclose(arquivo);

    return 0;
}

O segundo argumento de fopen() é o modo de abertura, que diz o que pretendemos fazer com o arquivo:

Modo Significado
"r" Leitura (o arquivo precisa já existir).
"w" Escrita (cria um novo arquivo, ou apaga o conteúdo de um já existente).
"a" Anexação — escreve sempre ao final do arquivo, sem apagar o que já existia.
"r+" Leitura e escrita (o arquivo precisa já existir).
"w+" Leitura e escrita (cria um novo arquivo, ou apaga o conteúdo de um já existente).
"rb", "wb", "ab" Os mesmos modos acima, mas em formato binário (acrescentando um b).

3. Operações gerais com arquivos

As funções desta seção funcionam com qualquer tipo de arquivo, texto ou binário. Todas elas giram em torno de um conceito central: o cursor do arquivo.

A struct FILE. Quando chamamos fopen(), o que recebemos de volta é um ponteiro para uma struct chamada FILE — definida internamente pela biblioteca padrão de C, e não por nós. Diferente das structs que criamos nas Aulas 10 e 11 (Aluno, Ponto, Funcionario), não temos acesso direto aos membros de FILE: é uma struct opaca — sabemos que ela existe e o que ela representa, mas seus detalhes internos são escondidos de propósito, e só manipulamos esse arquivo através das funções prontas que a biblioteca oferece (fread, fwrite, fseek, etc.), nunca acessando seus membros diretamente com . ou ->.

Ainda assim, é útil imaginar como uma FILE poderia ser implementada por dentro, para entender o que está de fato acontecendo quando chamamos essas funções. Uma versão simplificada (e só didática — a implementação real varia entre bibliotecas, e é bem mais complexa) poderia se parecer com isto:

// Versão didática e simplificada — NÃO é o código real da biblioteca!
typedef struct {
    int descritor;              // identifica o arquivo aberto para o sistema operacional
    long cursor;                // posição atual (o "current file position")
    unsigned char buffer[4096];  // bytes lidos/a escrever, guardados temporariamente
    int modo;                   // leitura, escrita, binário, etc. (definido no fopen)
    int eof;                    // virou verdadeiro quando o cursor passa do fim do arquivo
} FILE;

O nome cursor, aqui, é a nossa forma didática de nos referirmos ao que a documentação oficial chama de current file position — "posição atual no arquivo" — o único membro desta lista que realmente importa para o que vamos discutir nesta aula. É esse número que:

  • Começa em 0 ao abrir um arquivo em modo "r" ou "w".
  • Avança automaticamente, byte a byte, a cada leitura ou escrita — sem que precisemos fazer nada manualmente.
  • É apenas consultado (sem ser alterado) por ftell(arquivo).
  • É alterado diretamente por fseek(arquivo, ...) e rewind(arquivo), "pulando" para qualquer posição, sem precisar ler ou escrever nada no caminho.

Com isso em mente, as principais funções gerais são:

void rewind(FILE *arquivo); — volta o cursor para o início do arquivo (posição 0). Equivale a fseek(arquivo, 0, SEEK_SET).

long ftell(FILE *arquivo); — retorna a posição atual do cursor, em bytes a partir do início do arquivo. Retorna -1 se houver erro.

int fseek(FILE *arquivo, long deslocamento, int origem); — move o cursor para uma posição específica. O parâmetro origem pode ser:

  • SEEK_SET — a partir do início do arquivo (deslocamento é a posição absoluta).
  • SEEK_CUR — a partir da posição atual do cursor (deslocamento é relativo).
  • SEEK_END — a partir do fim do arquivo (deslocamento geralmente negativo).

fseek retorna 0 se bem-sucedido, e um valor diferente de zero se houver erro.

int fflush(FILE *arquivo); — força a gravação imediata de tudo o que ainda estiver no buffer interno, sem esperar o momento "natural" em que isso aconteceria. Útil quando é preciso garantir que os dados já foram fisicamente escritos antes de continuar.

int feof(FILE *arquivo); — retorna verdadeiro (diferente de zero) se o cursor já passou do fim do arquivo, e falso (zero) caso contrário. É comum (mas, como veremos, arriscado) usá-la em laços como while (!feof(arquivo)) para ler até o final.

Além dessas, existem outras funções úteis que atuam sobre o arquivo como um todo (e não sobre o seu conteúdo), como remove(), que apaga um arquivo do disco, e rename(), que renomeia (ou move) um arquivo — ambas recebem apenas o(s) nome(s) do arquivo, sem precisar de um FILE * aberto. Não entraremos em detalhes sobre elas aqui, mas vale saber que existem, caso você precise gerenciar arquivos além de apenas ler e escrever seu conteúdo.

#include <stdio.h>

int main() {
    char buffer[256];

    FILE *arquivo = fopen("dados.txt", "r");
    if (arquivo == NULL) {
        return 1;
    }

    // rewind: garante que o cursor comeca no inicio
    rewind(arquivo);
    printf("Posicao apos rewind: %ld\n", ftell(arquivo)); // 0

    // fseek: posiciona em um lugar especifico
    fseek(arquivo, 10, SEEK_SET); // vai para o byte 10, a partir do inicio
    printf("Posicao agora: %ld\n", ftell(arquivo)); // 10

    fseek(arquivo, 5, SEEK_CUR); // avanca 5 bytes a partir da posicao atual
    printf("Posicao apos avancar 5: %ld\n", ftell(arquivo)); // 15

    // feof com laco de leitura
    rewind(arquivo);
    while (!feof(arquivo)) {
        if (fgets(buffer, 256, arquivo) != NULL) {
            printf("%s", buffer);
        }
    }

    fclose(arquivo);
    return 0;
}

Repare, nesse exemplo, exatamente como cursor muda a cada chamada: começa em 0 (após o rewind), pula para 10 (após o fseek absoluto), e depois para 15 (após o fseek relativo, 10 + 5) — e é sempre o valor de cursor, nunca outra coisa, que ftell nos devolve a cada chamada.

O que acontece se eu escrever no meio do arquivo? Uma dúvida comum: se eu usar fseek para levar cursor para o meio de um arquivo já existente e escrever algo ali, o conteúdo que já estava depois é "empurrado" para frente, para abrir espaço? Não. Escrever em uma posição já existente sobrescreve os bytes que estavam ali, byte a byte, exatamente como acontece ao escrever em uma posição de um vetor — não existe, em C, uma operação de "inserir no meio" de um arquivo. Arquivos não têm a capacidade de "abrir espaço" automaticamente; eles só crescem quando escrevemos além do seu tamanho atual (ou quando usamos o modo "a", que sempre escreve no fim).
#include <stdio.h>

int main() {
    FILE *arquivo = fopen("dados.txt", "w");
    if (arquivo == NULL) {
        return 1;
    }

    fprintf(arquivo, "AAAAAAAAAA"); // 10 bytes: "AAAAAAAAAA"
    fclose(arquivo);

    // Reabrindo para escrever no MEIO do arquivo
    arquivo = fopen("dados.txt", "r+"); // leitura + escrita, sem apagar o conteudo
    if (arquivo == NULL) {
        return 1;
    }

    fseek(arquivo, 3, SEEK_SET); // cursor = 3 (o quarto byte)
    fprintf(arquivo, "BBB");   // sobrescreve 3 bytes, a partir da posicao 3

    fclose(arquivo);
    // Conteudo final de "dados.txt": "AAABBBAAAA" (10 bytes, mesmo tamanho de antes!)

    return 0;
}

O arquivo, que tinha 10 bytes ("AAAAAAAAAA"), continua com exatamente 10 bytes depois da segunda escrita — apenas os 3 bytes a partir da posição 3 foram substituídos por "BBB", resultando em "AAABBBAAAA". Nada foi empurrado; os bytes que vinham depois ("AAAA", no final) simplesmente foram sobrescritos onde o cursor passou por cima deles. Se quiséssemos, de fato, inserir conteúdo no meio sem perder nada, seria necessário reescrever o arquivo inteiro "na mão": ler tudo, montar o novo conteúdo (com a parte inserida) em memória ou em um arquivo temporário, e gravar esse resultado por completo.


4. Leitura e escrita em arquivos binários

Para arquivos binários, usamos duas funções específicas: fread() (leitura) e fwrite() (escrita). Diferente das funções de texto que veremos na próxima seção, elas não convertem nada para caracteres — trabalham diretamente com blocos de bytes brutos, copiando exatamente o conteúdo da memória para o arquivo (ou vice-versa).

fwrite: escrevendo blocos de bytes

Sintaxe genérica:

size_t fwrite(const void *buffer, size_t tamanho, size_t quantidade, FILE *arquivo);
Parâmetro Significado
buffer Endereço de onde os dados serão lidos na memória, para serem gravados no arquivo (por exemplo, &variavel, ou o próprio nome de um vetor).
tamanho Tamanho, em bytes, de cada elemento a ser escrito — quase sempre obtido com sizeof(tipo).
quantidade Quantos elementos (cada um com tamanho bytes) devem ser escritos.
arquivo O FILE * já aberto (em um modo binário de escrita, como "wb") para onde os dados vão.

fwrite retorna a quantidade de elementos efetivamente escritos (que deveria ser igual a quantidade, se tudo correu bem).

fread: lendo blocos de bytes

Sintaxe genérica:

size_t fread(void *buffer, size_t tamanho, size_t quantidade, FILE *arquivo);

Os parâmetros são análogos aos de fwrite, mas com o sentido invertido:

Parâmetro Significado
buffer Endereço na memória para onde os dados lidos do arquivo serão copiados (o destino da leitura) — por exemplo, &variavel, ou o próprio nome de um vetor.
tamanho Tamanho, em bytes, de cada elemento a ser lido — novamente, quase sempre sizeof(tipo).
quantidade Quantos elementos devem ser lidos.
arquivo O FILE * já aberto (em um modo binário de leitura, como "rb") de onde os dados vêm.

fread retorna a quantidade de elementos efetivamente lidos — se esse valor for menor do que o pedido em quantidade, significa que o arquivo acabou (ou ocorreu algum erro) antes de conseguirmos ler tudo o que queríamos. É sempre importante checar esse retorno, em vez de assumir que a leitura sempre dá certo.

Vejamos as duas funções em ação, escrevendo e depois lendo uma struct inteira de uma só vez:

#include <stdio.h>

typedef struct {
    int id;
    char nome[100];
    float salario;
} Funcionario;

int main() {
    Funcionario func = {1, "Joao", 5000.0};
    Funcionario funcLido;
    size_t elementosLidos;

    // Escrevendo a struct inteira, em uma unica chamada
    FILE *arquivo = fopen("dados.bin", "wb");
    if (arquivo == NULL) {
        return 1;
    }

    // fwrite: escreve 1 bloco de sizeof(Funcionario) bytes
    fwrite(&func, sizeof(Funcionario), 1, arquivo);
    fclose(arquivo);

    // Lendo a struct de volta
    arquivo = fopen("dados.bin", "rb");
    if (arquivo == NULL) {
        return 1;
    }

    // fread: le 1 bloco de sizeof(Funcionario) bytes
    elementosLidos = fread(&funcLido, sizeof(Funcionario), 1, arquivo);

    if (elementosLidos == 1) {
        printf("ID: %d, Nome: %s, Salario: %.2f\n",
               funcLido.id, funcLido.nome, funcLido.salario);
    }

    fclose(arquivo);
    return 0;
}
Por que gravar a struct inteira de uma vez? Como fwrite/fread trabalham com blocos de bytes brutos, é possível gravar (ou ler) uma struct inteira — com todos os seus membros — em uma única chamada, informando sizeof(Funcionario) como o tamanho de cada "elemento". Isso é muito mais direto do que escrever membro a membro com funções de texto, e garante que a struct volte exatamente como estava, sem risco de erro de formatação na conversão.

5. Leitura e escrita em arquivos de texto

Para arquivos de texto, existem quatro funções principais, cada uma pensada para um tipo de leitura ou escrita diferente: fprintf, fscanf, fgets e fgetc.

fprintf: escrita formatada

Funciona exatamente como o printf que já usamos desde a Aula 1, com um parâmetro a mais no início: o arquivo de destino.

Sintaxe genérica:

int fprintf(FILE *arquivo, const char *formato, ...);

arquivo é o FILE * de destino (aberto em modo de escrita, como "w"); formato e os argumentos seguintes funcionam exatamente como no printf (especificadores %d, %s, %f, etc.). A função retorna a quantidade de caracteres escritos (ou um valor negativo, se houver erro).

Na prática, fprintf sozinho já resolve qualquer escrita em arquivo de texto que você precisar nesta disciplina — de um único caractere a uma linha inteira formatada. A biblioteca padrão também oferece fputc(caractere, arquivo) (escreve um único caractere) e fputs(string, arquivo) (escreve uma string, sem formatação), mas não vamos usá-las aqui: qualquer coisa que elas fazem, fprintf também faz, então basta se lembrar dela.

fscanf: leitura formatada

É o equivalente do scanf para arquivos: em vez de ler do teclado, lê do arquivo indicado.

Sintaxe genérica:

int fscanf(FILE *arquivo, const char *formato, ...);

Os parâmetros seguem a mesma lógica do scanf (variáveis passadas por endereço com &, exceto vetores de char). A função retorna a quantidade de itens lidos com sucesso — se esse número for menor do que o esperado, é sinal de que algo deu errado (ou o arquivo acabou) antes de todos os valores serem lidos.

Cuidado com fscanf em char[]. Assim como o scanf (Aula 5), o especificador %s em fscanf não verifica o tamanho do vetor de destino — um arquivo com uma palavra maior do que o vetor reservado causa o mesmo problema de buffer overflow já discutido anteriormente. Prefira fgets quando o tamanho da entrada não for controlado.

fgets: lendo uma linha inteira

Sintaxe genérica:

char *fgets(char *destino, int tamanho, FILE *arquivo);
Parâmetro Significado
destino Vetor de char onde a linha lida será armazenada.
tamanho Tamanho máximo a ser lido — fgets lê, no máximo, tamanho - 1 caracteres, sempre deixando espaço para o '\0' final. É por isso que ela é segura: nunca escreve além do vetor informado.
arquivo O FILE * de onde a linha será lida.

fgets lê até encontrar uma quebra de linha ('\n') ou atingir o limite de tamanho - 1 caracteres, o que vier primeiro — e, diferente do que fizemos manualmente na Aula 5, inclui o '\n' na string resultante, caso ele tenha sido encontrado. A função retorna o próprio destino em caso de sucesso, ou NULL se não conseguir ler nada (por exemplo, se o arquivo já tiver acabado).

fgetc: lendo um único caractere

Sintaxe genérica:

int fgetc(FILE *arquivo);

Lê exatamente um caractere do arquivo e o devolve como int (não como char) — isso é proposital: assim, além de qualquer caractere válido, a função também consegue devolver o valor especial EOF ("end of file", fim de arquivo) para sinalizar que não há mais nada para ler, algo que não caberia em um char comum sem ambiguidade.

Juntando as quatro funções em um único exemplo:

#include <stdio.h>

int main() {
    FILE *arquivo;
    char linha[256];
    int caractere;
    char nome[50];
    int idade;

    // fprintf: escrita formatada em arquivo
    arquivo = fopen("output.txt", "w");
    if (arquivo == NULL) {
        return 1;
    }

    fprintf(arquivo, "%s %d\n", "Maria", 30);
    fclose(arquivo);

    // fgets: le a linha inteira
    arquivo = fopen("output.txt", "r");
    if (arquivo == NULL) {
        return 1;
    }

    if (fgets(linha, 256, arquivo) != NULL) {
        printf("Linha lida com fgets: %s", linha); // ja contem o '\n'
    }
    fclose(arquivo);

    // fscanf: le dados formatados (nome e idade separados)
    arquivo = fopen("output.txt", "r");
    fscanf(arquivo, "%s %d", nome, &idade);
    printf("Lido com fscanf -> nome: %s, idade: %d\n", nome, idade);
    fclose(arquivo);

    // fgetc: le caractere por caractere, ate o fim do arquivo
    arquivo = fopen("output.txt", "r");
    printf("Caracteres: ");
    caractere = fgetc(arquivo);
    while (caractere != EOF) {
        printf("[%c]", caractere);
        caractere = fgetc(arquivo);
    }
    printf("\n");
    fclose(arquivo);

    return 0;
}
Resumo: boas práticas com arquivos.
  • Sempre verifique se fopen retornou NULL: o arquivo pode não existir, ou o programa pode não ter permissão de acesso.
  • Sempre feche arquivos: use fclose() para liberar recursos — inclusive em caminhos de erro, antes de retornar.
  • Use o modo apropriado: "r" para leitura, "w" para escrita, "a" para anexar, acrescentando "b" quando o arquivo for binário.
  • fgets é mais seguro que fscanf com %s: fgets sempre respeita o tamanho do vetor informado; fscanf com %s, não.
  • Use fread/fwrite para dados binários estruturados: são mais diretos e eficientes do que montar/interpretar texto com fprintf/fscanf para esse fim.
  • Lembre-se do cursor: toda leitura/escrita avança o cursor do arquivo. Use rewind() ou fseek() para reposicioná-lo quando necessário.

Arquivos de texto × arquivos binários: comparativo

Depois de ver as duas formas na prática, vale reunir num só lugar quando cada uma costuma ser a melhor escolha:

Arquivos de texto Arquivos binários
Legibilidade Vantagem: podem ser abertos e lidos em qualquer editor de texto comum. Desvantagem: não são legíveis diretamente — é preciso um programa que saiba interpretar o formato.
Tamanho ocupado Desvantagem: números ocupam um byte por dígito (mais um separador), o que costuma gerar arquivos maiores. Vantagem: cada valor ocupa exatamente o tamanho do seu tipo em memória (ex.: 4 bytes por int), gerando arquivos mais compactos.
Velocidade de leitura/escrita Desvantagem: exige converter entre texto e número a cada leitura/escrita (fprintf/fscanf). Vantagem: grava/lê os bytes exatamente como estão na memória, sem conversão — mais rápido, especialmente para grandes volumes de dados.
Portabilidade entre programas/sistemas Vantagem: um formato de texto bem definido (como .csv) pode ser lido por praticamente qualquer programa, em qualquer sistema. Desvantagem: detalhes como tamanho exato dos tipos ou ordem dos bytes podem variar entre compiladores/sistemas diferentes, dificultando a leitura em outro ambiente.
Facilidade de edição manual Vantagem: pode ser corrigido "na mão", em qualquer editor, sem precisar de um programa especial. Desvantagem: editar diretamente (fora de um programa) é praticamente inviável, por não ser legível.

Na prática: prefira arquivos de texto quando a legibilidade humana e a portabilidade importarem mais (arquivos de configuração, dados para serem abertos em outras ferramentas, planilhas). Prefira arquivos binários quando o desempenho e o tamanho do arquivo forem prioridade, e o arquivo só precisar ser lido pelo seu próprio programa (ou por outro que conheça exatamente o mesmo formato).


Resumo

  • Arquivo: sequência de bytes em memória secundária (persistente), diferente da RAM (volátil). "Texto" e "binário" são apenas formas diferentes de interpretar esses bytes.
  • fopen/fclose: abrem e fecham um arquivo, através de um ponteiro FILE *. Sempre verifique se fopen não retornou NULL.
  • Cursor do arquivo: controla onde a próxima leitura/escrita acontece. rewind, ftell e fseek permitem consultar e reposicionar o cursor.
  • fread/fwrite: leem/escrevem blocos de bytes brutos (arquivos binários), recebendo tamanho (geralmente sizeof(tipo)) e quantidade de elementos.
  • fprintf/fscanf: escrita/leitura formatada em arquivos de texto, análogas a printf/scanf.
  • fgets/fgetc: leem, respectivamente, uma linha inteira (com tamanho controlado) ou um único caractere (retornado como int, podendo ser EOF) de um arquivo de texto.

Exercícios

Nível fácil

  1. Escreva um programa que abra um arquivo "saida.txt" em modo escrita e grave, usando fprintf, três linhas com números de 1 a 3. Feche o arquivo ao final. Verifique se fopen retornou NULL.
  2. Escreva um programa que abra o arquivo criado no exercício anterior em modo leitura e imprima, na tela, todo o seu conteúdo usando fgets em um laço.
  3. Escreva um programa que abra um arquivo de texto e conte quantos caracteres ele possui, lendo um de cada vez com fgetc até encontrar EOF.

Nível médio

  1. Escreva um programa que peça ao usuário um nome e uma idade, e grave essas informações em um arquivo de texto usando fprintf. Em seguida, no mesmo programa, reabra o arquivo e leia os valores de volta com fscanf, imprimindo-os na tela para conferência.
  2. Defina uma struct Produto (nome, preço). Escreva um programa que grave um vetor de 3 produtos em um arquivo binário usando fwrite (uma única chamada, escrevendo os 3 de uma vez), e depois leia esse vetor de volta com fread, imprimindo cada produto lido.
  3. Explique, sem executar o código, qual será o conteúdo de "copia.txt" ao final do programa abaixo, e por quê:
    #include <stdio.h>
    
    int main() {
        FILE *saida = fopen("copia.txt", "w");
        fprintf(saida, "Primeira linha\n");
        fclose(saida);
    
        saida = fopen("copia.txt", "w");
        fprintf(saida, "Segunda linha\n");
        fclose(saida);
    
        return 0;
    }
    

Nível difícil

  1. Escreva um programa que leia um arquivo de texto e grave, em um segundo arquivo, apenas as linhas com mais de 10 caracteres (use fgets para ler linha por linha, strlen para medir o tamanho, e fprintf para gravar as linhas selecionadas).
  2. Escreva um programa que grave um vetor de 5 inteiros em um arquivo binário, depois use fseek para reposicionar o cursor diretamente sobre o terceiro elemento (sem ler os dois primeiros), leia apenas esse valor com fread, e o imprima. Lembre-se de que o deslocamento em fseek é sempre medido em bytes, não em "elementos".

Sugestões de Respostas dos Exercícios

Exercício 1
#include <stdio.h>

int main() {
    int i;

    FILE *arquivo = fopen("saida.txt", "w");
    if (arquivo == NULL) {
        printf("Erro ao abrir arquivo\n");
        return 1;
    }

    for (i = 1; i <= 3; i++) {
        fprintf(arquivo, "%d\n", i);
    }

    fclose(arquivo);
    return 0;
}

Exercício 2
#include <stdio.h>

int main() {
    char linha[256];

    FILE *arquivo = fopen("saida.txt", "r");
    if (arquivo == NULL) {
        printf("Erro ao abrir arquivo\n");
        return 1;
    }

    while (fgets(linha, 256, arquivo) != NULL) {
        printf("%s", linha);
    }

    fclose(arquivo);
    return 0;
}

Exercício 3
#include <stdio.h>

int main() {
    int caractere;
    int contador = 0;

    FILE *arquivo = fopen("saida.txt", "r");
    if (arquivo == NULL) {
        printf("Erro ao abrir arquivo\n");
        return 1;
    }

    caractere = fgetc(arquivo);
    while (caractere != EOF) {
        contador++;
        caractere = fgetc(arquivo);
    }

    printf("Total de caracteres: %d\n", contador);

    fclose(arquivo);
    return 0;
}

Exercício 4
#include <stdio.h>

int main() {
    char nome[50];
    int idade;
    FILE *arquivo;

    printf("Digite seu nome: ");
    scanf("%s", nome);
    printf("Digite sua idade: ");
    scanf("%d", &idade);

    arquivo = fopen("pessoa.txt", "w");
    if (arquivo == NULL) {
        return 1;
    }
    fprintf(arquivo, "%s %d\n", nome, idade);
    fclose(arquivo);

    arquivo = fopen("pessoa.txt", "r");
    if (arquivo == NULL) {
        return 1;
    }
    fscanf(arquivo, "%s %d", nome, &idade);
    fclose(arquivo);

    printf("Lido do arquivo -> nome: %s, idade: %d\n", nome, idade);

    return 0;
}

Exercício 5
#include <stdio.h>

typedef struct {
    char nome[50];
    float preco;
} Produto;

int main() {
    Produto produtos[3] = {
        {"Caneta", 2.50},
        {"Caderno", 15.90},
        {"Borracha", 1.20}
    };
    Produto lidos[3];
    int i;
    FILE *arquivo;

    arquivo = fopen("produtos.bin", "wb");
    if (arquivo == NULL) {
        return 1;
    }
    fwrite(produtos, sizeof(Produto), 3, arquivo);
    fclose(arquivo);

    arquivo = fopen("produtos.bin", "rb");
    if (arquivo == NULL) {
        return 1;
    }
    fread(lidos, sizeof(Produto), 3, arquivo);
    fclose(arquivo);

    for (i = 0; i < 3; i++) {
        printf("%s: R$ %.2f\n", lidos[i].nome, lidos[i].preco);
    }

    return 0;
}

Exercício 6

O conteúdo final será apenas "Segunda linha" — sem nenhum vestígio de "Primeira linha". Isso acontece porque o modo "w" sempre sobrescreve o conteúdo do arquivo: cada chamada a fopen(..., "w") apaga o que já existia e começa um arquivo novo (vazio). A segunda abertura em modo "w" não "acrescenta" à primeira — ela recomeça do zero. Se a intenção fosse manter as duas linhas, seria preciso abrir o arquivo em modo "a" (anexação) na segunda vez, ou escrever as duas linhas antes de fechar o arquivo pela primeira vez.


Exercício 7
#include <stdio.h>
#include <string.h>

int main() {
    char linha[256];
    FILE *entrada;
    FILE *saida;

    entrada = fopen("entrada.txt", "r");
    if (entrada == NULL) {
        return 1;
    }

    saida = fopen("grandes.txt", "w");
    if (saida == NULL) {
        fclose(entrada);
        return 1;
    }

    while (fgets(linha, 256, entrada) != NULL) {
        if (strlen(linha) > 10) {
            fprintf(saida, "%s", linha);
        }
    }

    fclose(entrada);
    fclose(saida);

    return 0;
}

Exercício 8
#include <stdio.h>

int main() {
    int valores[5] = {10, 20, 30, 40, 50};
    int terceiro;
    FILE *arquivo;

    arquivo = fopen("numeros.bin", "wb");
    if (arquivo == NULL) {
        return 1;
    }
    fwrite(valores, sizeof(int), 5, arquivo);
    fclose(arquivo);

    arquivo = fopen("numeros.bin", "rb");
    if (arquivo == NULL) {
        return 1;
    }

    // terceiro elemento = indice 2, cada int ocupa sizeof(int) bytes
    fseek(arquivo, 2 * sizeof(int), SEEK_SET);
    fread(&terceiro, sizeof(int), 1, arquivo);

    printf("Terceiro elemento: %d\n", terceiro); // 30

    fclose(arquivo);
    return 0;
}