C Escrever Arquivos

Você já sabe abrir um arquivo (fopen e fclose) e ler o que tem dentro (Ler Arquivos). Falta o caminho de volta: escrever. A boa notícia é que você já conhece quase tudo, porque escrever num arquivo é muito parecido com escrever na tela: o printf tem um irmão, o fprintf, que faz o mesmo num arquivo. Nesta aula você vai gerar a lista de compras e o relatório de vendas da lojinha da Ana, entender por que o texto às vezes demora para chegar ao disco (o buffer) e aprender a ter certeza de que o arquivo foi mesmo salvo.

Você vai aprender
  • As três funções de escrita: fputc (um caractere), fputs (um texto) e fprintf (texto formatado).
  • Por que o fputs não pula linha sozinho.
  • Como gravar um relatório CSV a partir de um array de structs.
  • Como acrescentar ao fim de um registro (log) com o modo "a".
  • O que é o buffer, o que o fflush faz e o que se perde quando o programa cai.
  • Como conferir se a gravação deu certo, até o fclose, e como salvar sem risco de perder o arquivo antigo.

Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: três jeitos de escrever no caderno

No caderno da lojinha, a Ana escreve de três jeitos:

  • um sinal só, como um * para marcar um item importante;
  • uma frase pronta, copiada como está: "arroz";
  • um formulário com espaços para preencher: "___ pacotes de café, total R$ ___".

Em C, são três funções, todas do <stdio.h>, e todas recebem o FILE * do arquivo aberto:

No cadernoFunçãoExemploA versão "tela"
Um sinalfputc ("file put character")fputc('*', arq);putchar('*');
Uma frase prontafputs ("file put string")fputs("arroz\n", arq);fputs("arroz\n", stdout);
Um formuláriofprintf ("file print formatted")fprintf(arq, "%d pacotes\n", 2);printf("%d pacotes\n", 2);

1. As três funções juntas

#include <stdio.h>

int main(void) {
    FILE *arq = fopen("compras.txt", "w");
    if (arq == NULL) {
        perror("compras.txt");
        return 1;
    }

    fputc('*', arq);
    fputc('\n', arq);
    fputs("arroz\n", arq);
    fputs("feijão\n", arq);
    fprintf(arq, "%d pacotes de café\n", 2);
    fprintf(arq, "Total: R$ %.2f\n", 31.5);

    fclose(arq);
    printf("Lista gravada.\n");
    return 0;
}
fopen("compras.txt", "w")
Cria o arquivo (ou apaga o que havia) para escrever. Conferir o NULL é obrigatório, como sempre.
fputc('*', arq)
Escreve um caractere, entre aspas simples. Depois, fputc('\n', arq) escreve a quebra de linha, que também é um caractere.
fputs("arroz\n", arq)
Escreve o texto exatamente como está, sem formatação. O texto vem primeiro e o arquivo depois.
fprintf(arq, "%d pacotes de café\n", 2)
Igual ao printf, com um argumento a mais no começo: o arquivo. Todos os formatos (%d, %.2f, %s, %-10s…) funcionam (aula Formatos do printf).
printf("Lista gravada.\n")
Este vai para a tela: é só um aviso para a Ana.
$ gcc -std=c23 -Wall -Wextra tres.c -o tres
$ ./tres
Lista gravada.
$ cat compras.txt
*
arroz
feijão
2 pacotes de café
Total: R$ 31.50
Repare na ordem dos argumentos. No fprintf, o arquivo vem no começo; no fputs e no fputc, vem no fim. É uma pegadinha antiga da biblioteca do C. Se trocar, o compilador reclama (veja "Quando dá errado").

2. O fputs não pula linha

O puts, que escreve na tela, acrescenta um \n no fim por conta própria. O fputs, não: ele escreve só o que você mandou. Esquecer o \n gruda tudo:

    fputs("arroz", arq);
    fputs("feijão", arq);
    fputs("café", arq);
$ ./grudado
$ cat compras.txt
arrozfeijãocafé$ cat -A compras.txt
arrozfeijM-CM-#ocafM-CM-)$

A lista virou uma palavra só, e até o $ do terminal veio parar na mesma linha, porque o arquivo não termina com \n. O cat -A mostra os caracteres invisíveis: se houvesse quebras de linha, apareceria um $ no fim de cada linha. Os M-CM-# e M-CM-) são os 2 bytes do ã e do é em UTF-8.

Missão: o programa grava três itens e depois lê o arquivo de volta, linha por linha. Rode: saiu uma linha só. Conserte a parte da gravação para cada item ficar na sua linha, e a saída ser - arroz, - feijão, - café e (fim da lista), cada um numa linha.

#include <stdio.h>

int main(void) {
    FILE *arq = fopen("compras.txt", "w");
    if (arq == NULL) {
        perror("compras.txt");
        return 1;
    }
    fputs("arroz", arq);
    fputs("feijão", arq);
    fputs("café", arq);
    fclose(arq);

    /* agora lê de volta, linha por linha */
    arq = fopen("compras.txt", "r");
    if (arq == NULL) {
        perror("compras.txt");
        return 1;
    }
    char linha[100];
    while (fgets(linha, sizeof linha, arq) != NULL) {
        printf("- %s", linha);
    }
    fclose(arq);
    printf("(fim da lista)\n");
    return 0;
}
#include <stdio.h>

int main(void) {
    FILE *arq = fopen("compras.txt", "w");
    if (arq == NULL) {
        perror("compras.txt");
        return 1;
    }
    fputs("arroz\n", arq);
    fputs("feijão\n", arq);
    fputs("café\n", arq);
    fclose(arq);

    /* agora lê de volta, linha por linha */
    arq = fopen("compras.txt", "r");
    if (arq == NULL) {
        perror("compras.txt");
        return 1;
    }
    char linha[100];
    while (fgets(linha, sizeof linha, arq) != NULL) {
        printf("- %s", linha);
    }
    fclose(arq);
    printf("(fim da lista)\n");
    return 0;
}

Boa prática: todo arquivo de texto termina com \n. Muitos programas do Linux (o wc -l, por exemplo) só contam uma linha quando ela termina com \n.


3. Um relatório com fprintf

O fprintf brilha quando os dados estão num array de structs. A Ana quer o relatório de vendas do dia num arquivo CSV, que abre em qualquer planilha:

#include <stdio.h>

struct Venda {
    char produto[20];
    int quantidade;
    double preco;
};

int main(void) {
    struct Venda vendas[] = {
        {"Caderno", 3, 15.90},
        {"Caneta", 10, 2.50},
        {"Mochila", 1, 89.00},
    };
    int n = sizeof vendas / sizeof vendas[0];

    FILE *arq = fopen("vendas.csv", "w");
    if (arq == NULL) {
        perror("vendas.csv");
        return 1;
    }

    fprintf(arq, "produto;quantidade;preco;subtotal\n");
    double total = 0.0;
    for (int i = 0; i < n; i++) {
        double sub = vendas[i].quantidade * vendas[i].preco;
        fprintf(arq, "%s;%d;%.2f;%.2f\n",
                vendas[i].produto, vendas[i].quantidade, vendas[i].preco, sub);
        total += sub;
    }
    fprintf(arq, "TOTAL;;;%.2f\n", total);

    if (fclose(arq) != 0) {
        perror("vendas.csv");
        return 1;
    }
    printf("Relatório com %d vendas gravado em vendas.csv\n", n);
    return 0;
}
fprintf(arq, "produto;quantidade;preco;subtotal\n")
A primeira linha do CSV é o cabeçalho: o nome de cada coluna. Sem nenhum %, o fprintf escreve o texto como está.
"%s;%d;%.2f;%.2f\n"
Uma linha por venda: os valores separados por ;, o preço e o subtotal com 2 casas. É o formato que o sscanf da aula Ler Arquivos sabe ler de volta.
"TOTAL;;;%.2f\n"
Duas colunas vazias (;;) para o total cair na coluna do subtotal.
if (fclose(arq) != 0)
O fclose devolve 0 se deu tudo certo e EOF se falhou ao gravar o final. Num arquivo importante, confira (seção 6).
$ gcc -std=c23 -Wall -Wextra relatorio.c -o relatorio
$ ./relatorio
Relatório com 3 vendas gravado em vendas.csv
$ cat vendas.csv
produto;quantidade;preco;subtotal
Caderno;3;15.90;47.70
Caneta;10;2.50;25.00
Mochila;1;89.00;89.00
TOTAL;;;161.70
Ponto ou vírgula nos números?

O %.2f escreve o número com ponto (15.90), como o C faz por padrão. Uma planilha configurada para o Brasil pode esperar vírgula (15,90) e tratar 15.90 como texto. Na hora de abrir, escolha o ponto como separador decimal. (Um setlocale(LC_ALL, "") no começo do programa faz o fprintf usar vírgula no Linux da Ana; aí, use ; para separar as colunas, como aqui, e nunca vírgula.)


4. Acrescentar: um registro de pedidos

Muitos programas mantêm um registro (log): um arquivo em que cada acontecimento vira uma linha no fim, sem apagar as anteriores. É o modo "a" (append) da aula Arquivos:

#include <stdio.h>

int main(void) {
    FILE *arq = fopen("pedidos.log", "a");
    if (arq == NULL) {
        perror("pedidos.log");
        return 1;
    }
    fprintf(arq, "pedido recebido\n");
    fclose(arq);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra registrar.c -o registrar
$ ./registrar
$ ./registrar
$ ./registrar
$ cat pedidos.log
pedido recebido
pedido recebido
pedido recebido

Três execuções, três linhas. Com "w", sobraria uma. Num registro de verdade, cada linha leva também a data e a hora, que a aula Data e Hora ensina a obter.


5. O buffer: o texto ainda não chegou ao disco

Escrever no disco, um pedacinho por vez, seria muito lento. Por isso o fprintf não escreve no disco na hora: ele guarda o texto numa área de espera na memória, o buffer, e só manda tudo de uma vez quando o buffer enche, quando você chama fflush ou quando o arquivo é fechado. Para ver o buffer acontecendo, este programa escreve uma linha, "trabalha" por 3 segundos e escreve outra:

#include <stdio.h>
#include <unistd.h>

int main(void) {
    FILE *arq = fopen("log.txt", "w");
    if (arq == NULL) {
        perror("log.txt");
        return 1;
    }
    fprintf(arq, "Começou o fechamento do caixa\n");
    sleep(3);                  /* finge um trabalho demorado */
    fprintf(arq, "Terminou\n");
    fclose(arq);
    return 0;
}

A Ana roda o programa em segundo plano (o & no fim), espera 1 segundo e olha o arquivo enquanto ele ainda está rodando. Depois espera ele terminar (wait) e olha de novo:

$ gcc -std=c23 -Wall -Wextra lento.c -o lento
$ ./lento & sleep 1; cat log.txt; echo "(fim do cat)"; wait; cat log.txt
[1] 453249
(fim do cat)                          « no meio: o arquivo está vazio!
[1]+  Concluído                 ./lento
Começou o fechamento do caixa
Terminou                              « depois do fclose, tudo lá

Um segundo depois do fprintf, o arquivo ainda estava vazio: a primeira linha estava no buffer, na memória do programa. Só o fclose, no fim, levou tudo para o disco. O fflush ("descarregar") manda o buffer para o disco na hora, sem fechar o arquivo:

    fprintf(arq, "Começou o fechamento do caixa\n");
    fflush(arq);               /* manda para o disco agora */
    sleep(3);
$ ./lento2 & sleep 1; cat log.txt; echo "(fim do cat)"; wait; cat log.txt
[1] 453254
Começou o fechamento do caixa         « agora já aparece no meio
(fim do cat)
[1]+  Concluído                 ./lento2
Começou o fechamento do caixa
Terminou

Quando o programa cai, o buffer se perde

Se o programa termina normalmente (return no main ou exit), o C esvazia os buffers e fecha os arquivos sozinho. Mas se ele morre no meio, o que estava no buffer some:

    fprintf(arq, "Venda 1: R$ 15.90\n");
    fprintf(arq, "Venda 2: R$ 2.50\n");

    int *p = NULL;
    printf("%d\n", *p);        /* um erro qualquer derruba o programa */

    fclose(arq);
$ ./caiu
Falha de segmentação     (imagem do núcleo gravada) ./caiu
$ ls -l log.txt
-rw-rw-r-- 1 ana ana 0 out 17 11:43 log.txt     « 0 bytes: as duas vendas se perderam

As duas vendas foram "escritas" com fprintf, mas nunca saíram do buffer. Num registro, é justamente a última linha antes da queda que você mais queria ler. Por isso, em registros importantes, chame fflush(arq) depois de cada linha. Não exagere em programas que escrevem muito: cada fflush é uma ida ao disco.


6. Conferir se gravou de verdade

Escrever num arquivo pode falhar: o disco enche, o pendrive é arrancado, a rede cai (num arquivo em outra máquina). Cada função avisa do seu jeito:

FunçãoDeu certoDeu errado
fputc(c, arq)o próprio caractereEOF
fputs(texto, arq)um número que não é negativoEOF
fprintf(arq, …)quantos bytes escreveuum número negativo
fflush(arq) / fclose(arq)0EOF

Por causa do buffer, o erro muitas vezes só aparece no fim. O Linux tem um arquivo especial, /dev/full, que finge ser um disco sempre cheio. Perfeito para testar:

#include <stdio.h>

int main(void) {
    FILE *arq = fopen("/dev/full", "w");
    if (arq == NULL) {
        perror("/dev/full");
        return 1;
    }
    int r = fprintf(arq, "Dados importantes\n");
    printf("fprintf devolveu %d\n", r);

    if (fclose(arq) != 0) {
        perror("fclose");
        return 1;
    }
    printf("Salvo com sucesso!\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra cheio.c -o cheio
$ ./cheio
fprintf devolveu 18
fclose: No space left on device

O fprintf devolveu 18 (os 18 bytes de "Dados importantes\n") e parecia ter dado certo: o texto foi para o buffer, que tinha espaço. O erro só apareceu quando o fclose tentou levar o buffer para o "disco": No space left on device, "não há espaço no dispositivo". Um programa que não conferisse o fclose diria "Salvo com sucesso!" e perderia os dados calado. O /dev/full não existe no editor do guia: lá, este fopen falha com No such file or directory.

Regra prática: para arquivos que importam (um "salvar" do usuário, um relatório), confira o fclose. Para conferir no meio, sem esperar o fim, chame fflush e confira o retorno dele, ou use ferror(arq), que fica verdadeiro depois de qualquer falha de escrita.

7. Salvar sem arriscar o arquivo antigo

Há uma armadilha escondida no "w": ele apaga o arquivo na hora em que abre. Se a gravação falhar no meio (disco cheio, programa que cai), o arquivo novo fica pela metade e o antigo já foi embora. Os editores de texto e os jogos evitam isso com um truque em três passos:

  1. grave tudo num arquivo temporário (config.txt.tmp);
  2. confira que deu tudo certo, inclusive o fclose;
  3. só então troque o antigo pelo novo, com rename ("renomear", do stdio.h), que no Linux faz a troca de uma vez só: quem abrir o arquivo vê o antigo inteiro ou o novo inteiro, nunca a metade.
#include <stdio.h>

int salvar(const char *nome, const char *texto) {
    char temp[100];
    snprintf(temp, sizeof temp, "%s.tmp", nome);

    FILE *arq = fopen(temp, "w");
    if (arq == NULL) {
        perror(temp);
        return 0;
    }
    if (fputs(texto, arq) == EOF) {
        perror(temp);
        fclose(arq);
        remove(temp);
        return 0;
    }
    if (fclose(arq) != 0) {
        perror(temp);
        remove(temp);
        return 0;
    }
    if (rename(temp, nome) != 0) {
        perror(nome);
        remove(temp);
        return 0;
    }
    return 1;
}

int main(void) {
    if (salvar("config.txt", "volume=7\ntema=escuro\n")) {
        printf("Configuração salva.\n");
    }
    return 0;
}
LinhaO que faz
snprintf(temp, sizeof temp, "%s.tmp", nome)Monta o nome do temporário: config.txt.tmp (aula Texto ↔ Número).
fputs(…) == EOFConfere a escrita. Se falhou, fecha, apaga o temporário e desiste; o config.txt antigo continua intacto.
fclose(arq) != 0Confere o fim da gravação, onde aparecem os erros do buffer (seção 6).
remove(temp)Apaga um arquivo (do stdio.h). Aqui, o temporário que não deu certo.
rename(temp, nome)Troca o antigo pelo novo. Devolve 0 se deu certo.
return 1; / return 0;A função diz se salvou (1) ou não (0), para o main decidir o que mostrar.
$ gcc -std=c23 -Wall -Wextra seguro.c -o seguro
$ ./seguro
Configuração salva.
$ ls config*
config.txt                       « o .tmp já virou config.txt
$ cat config.txt
volume=7
tema=escuro

Formas de escrever: qual usar

Você quer escrever…UseLembre
Um caractere por vez (copiar, transformar)fputc(c, arq)O arquivo vem por último.
Um texto prontofputs(texto, arq)Não acrescenta \n.
Números, colunas, relatóriosfprintf(arq, formato, …)O arquivo vem primeiro.
Uma linha que precisa sobreviver a uma quedafprintf + fflush(arq)Cada fflush é uma ida ao disco.
Um "salvar" do usuárioTemporário + fclose conferido + renameSeção 7.
Os bytes de uma struct ou de um arrayfwriteAula Arquivos Binários.
Uma mensagem de errofprintf(stderr, …)Aula stdin, stdout e stderr.

Quando dá errado

"passing argument 1 of ‘fprintf’ from incompatible pointer type": os argumentos trocados
    fprintf("arroz\n", arq);
    fputs(arq, "feijão\n");
$ gcc -std=c23 -Wall -Wextra ordem.c -o ordem
ordem.c: In function ‘main’:
ordem.c:8:13: error: passing argument 1 of ‘fprintf’ from incompatible pointer type [-Wincompatible-pointer-types]
    8 |     fprintf("arroz\n", arq);
      |             ^~~~~~~~~
      |             |
      |             char *
In file included from ordem.c:1:
/usr/include/stdio.h:364:38: note: expected ‘FILE * restrict’ but argument is of type ‘char *’
  364 | extern int fprintf (FILE *__restrict __stream,
      |                     ~~~~~~~~~~~~~~~~~^~~~~~~~
«…»
ordem.c:9:11: error: passing argument 1 of ‘fputs’ from incompatible pointer type [-Wincompatible-pointer-types]
    9 |     fputs(arq, "feijão\n");
      |           ^~~
      |           |
      |           FILE *
/usr/include/stdio.h:711:42: note: expected ‘const char * restrict’ but argument is of type ‘FILE *’
  711 | extern int fputs (const char *__restrict __s, FILE *__restrict __stream)
      |                   ~~~~~~~~~~~~~~~~~~~~~~~^~~
«…»

O que aconteceu: "o argumento 1 tem um tipo de ponteiro incompatível". As notas mostram o que cada função esperava: o fprintf quer o FILE * primeiro, e o fputs quer o texto primeiro e o FILE * por último. O Clang do editor diz incompatible pointer types passing 'char[7]' to parameter of type 'FILE *'.

Como resolver: fprintf(arq, "arroz\n"); e fputs("feijão\n", arq);. Na dúvida, leia a linha note: expected …: ela mostra a declaração da função.

O arquivo ficou vazio (ou faltando o fim)

O que aconteceu: três causas comuns:

  • o programa caiu antes do fclose, e o buffer se perdeu (seção 5);
  • você olhou o arquivo enquanto o programa ainda rodava, antes do fclose ou do fflush;
  • você usou printf em vez de fprintf(arq, …): o texto foi para a tela, e o arquivo continuou vazio.

Como resolver: feche o arquivo assim que terminar de escrever; para registros, fflush(arq) depois de cada linha; e confira se cada linha de escrita tem o arq.

fprintf devolveu -1 e "Bad file descriptor": o arquivo foi aberto para leitura
    FILE *arq = fopen("compras.txt", "r");
    /* … if (arq == NULL) … */
    int r = fprintf(arq, "leite\n");
    printf("fprintf devolveu %d\n", r);
    if (ferror(arq)) {
        perror("fprintf");
    }
$ ./modoerrado
fprintf devolveu -1
fprintf: Bad file descriptor

O que aconteceu: o arquivo foi aberto com "r", só para leitura, e o fprintf tentou escrever. Ele devolveu -1 (negativo = erro), o ferror ficou verdadeiro, e o motivo é "descritor de arquivo ruim": esse arquivo não aceita escrita. O compilador não tem como perceber. No editor do guia, o fprintf também devolve -1, mas o perror mostra Success, porque lá o motivo não é anotado: confie no retorno, e não só na mensagem.

Como resolver: abra com o modo de escrita certo ("w" ou "a"), ou com "r+" se precisa ler e escrever no mesmo arquivo (aula fseek).

"Salvei, mas os dados antigos sumiram"

O que aconteceu: o programa abriu com "w" para acrescentar uma linha, e o "w" apagou tudo o que havia antes (aula Arquivos, seção 3).

Como resolver: para acrescentar, use "a". Para reescrever o arquivo inteiro com segurança, use o temporário + rename da seção 7.


Referência rápida

EscritaO que faz
fputc('*', arq)Escreve um caractere; devolve EOF se falhar.
fputs("texto\n", arq)Escreve o texto como está, sem acrescentar \n.
fprintf(arq, "%s;%d\n", nome, qtd)Como o printf, no arquivo; devolve os bytes escritos ou um negativo.
fopen(nome, "a")Acrescenta no fim, sem apagar.
fflush(arq)Manda o buffer para o disco agora, sem fechar.
if (fclose(arq) != 0)Confere se o fim da gravação deu certo.
ferror(arq)Verdadeiro se alguma escrita (ou leitura) falhou.
remove("velho.txt")Apaga um arquivo; 0 se deu certo.
rename("novo.tmp", "dados.txt")Renomeia (e substitui) um arquivo; 0 se deu certo.

Exercícios

1. O argumento que falta

Complete para escrever a nota no arquivo arq, com uma casa decimal:

fprintf(, "Nota: %.1f\n", nota);

2. A quebra de linha

Complete o texto para que leite fique sozinho na sua linha do arquivo:

fputs("", arq);

3. Agora!

Qual função manda o buffer de arq para o disco sem fechar o arquivo?

;

4. O que o arquivo contém depois de fputs("Ana", arq); fputs("Bia", arq);?

5. Um programa faz fprintf(arq, …) e cai com falha de segmentação antes do fclose. O que é mais provável encontrar no arquivo?

6. No exemplo do /dev/full, por que o fprintf devolveu 18 se o "disco" estava cheio?

Missão final: o boletim da turma

Missão: grave o boletim em boletim.txt: dentro do for, use fprintf para escrever uma linha por aluno, com o formato "%-6s %4.1f %s\n" (nome, nota e a situação: aprovado se a nota for 6.0 ou mais, reprovado se não). A parte 2, que lê o arquivo e mostra na tela, já está pronta. A saída deve ser:

Ana     8.5  aprovado
Bruno   5.5  reprovado
Carla   9.0  aprovado
Davi    6.0  aprovado
#include <stdio.h>

struct Aluno {
    char nome[20];
    double nota;
};

int main(void) {
    struct Aluno turma[] = {
        {"Ana", 8.5}, {"Bruno", 5.5}, {"Carla", 9.0}, {"Davi", 6.0},
    };
    int n = sizeof turma / sizeof turma[0];

    /* 1) grava o boletim */
    FILE *arq = fopen("boletim.txt", "w");
    if (arq == NULL) {
        perror("boletim.txt");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        /* escreva a linha do aluno turma[i] com fprintf */
    }
    if (fclose(arq) != 0) {
        perror("boletim.txt");
        return 1;
    }

    /* 2) mostra o que ficou no arquivo */
    arq = fopen("boletim.txt", "r");
    if (arq == NULL) {
        perror("boletim.txt");
        return 1;
    }
    char linha[100];
    while (fgets(linha, sizeof linha, arq) != NULL) {
        printf("%s", linha);
    }
    fclose(arq);
    return 0;
}
#include <stdio.h>

struct Aluno {
    char nome[20];
    double nota;
};

int main(void) {
    struct Aluno turma[] = {
        {"Ana", 8.5}, {"Bruno", 5.5}, {"Carla", 9.0}, {"Davi", 6.0},
    };
    int n = sizeof turma / sizeof turma[0];

    /* 1) grava o boletim */
    FILE *arq = fopen("boletim.txt", "w");
    if (arq == NULL) {
        perror("boletim.txt");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        fprintf(arq, "%-6s %4.1f  %s\n", turma[i].nome, turma[i].nota,
                turma[i].nota >= 6.0 ? "aprovado" : "reprovado");
    }
    if (fclose(arq) != 0) {
        perror("boletim.txt");
        return 1;
    }

    /* 2) mostra o que ficou no arquivo */
    arq = fopen("boletim.txt", "r");
    if (arq == NULL) {
        perror("boletim.txt");
        return 1;
    }
    char linha[100];
    while (fgets(linha, sizeof linha, arq) != NULL) {
        printf("%s", linha);
    }
    fclose(arq);
    return 0;
}

Dica: o operador ternário (condição ? "aprovado" : "reprovado", aula Operador Ternário) escolhe o texto sem precisar de um if.


Resumo

  • fputc escreve um caractere, fputs escreve um texto (sem acrescentar \n) e fprintf é o printf dos arquivos.
  • Ordem dos argumentos: fprintf(arq, …) com o arquivo primeiro; fputs(texto, arq) e fputc(c, arq) com o arquivo por último.
  • "w" recomeça o arquivo; "a" acrescenta no fim, como num registro.
  • O texto passa por um buffer antes de ir ao disco. fflush e fclose esvaziam o buffer; se o programa cai antes, o que estava nele se perde.
  • Confira os retornos, principalmente o do fclose: é nele que aparece um "disco cheio".
  • Para salvar sem arriscar o arquivo antigo: grave num temporário, confira tudo e troque com rename.