Projeto Estoque da Lojinha

A Ana tem uma lojinha de papelaria e anota o estoque num caderno: quantos cadernos, canetas e borrachas ainda tem, e quanto custa cada um. Neste projeto ela troca o caderno por um programa em C que lembra de tudo de um dia para o outro. O segredo é um arquivo binário: o programa copia as fichas dos produtos da memória para o disco com fwrite e, na próxima vez, copia de volta com fread.

Você vai praticar
  • Desenhar uma struct que pode ir para o disco: com int32_t e o preço em centavos.
  • Gravar um array de structs com fwrite e ler de volta com fread (Arquivos Binários), conferindo o que eles devolvem.
  • Olhar os bytes do arquivo com xxd e achar cada campo.
  • Fazer um menu que lê linhas com fgets e converte números com strtol e strtod.
  • Arredondar dinheiro sem perder um centavo.
  • Pôr um cabeçalho no arquivo (assinatura e versão) e salvar com segurança, num arquivo temporário trocado com rename.

O que vamos construir

O programa pronto, na máquina da Ana. Ele abre, carrega o estoque que estava salvo, ela cadastra a cola que chegou, vende 5 canetas e sai. Na próxima vez, tudo continua lá:

$ ./estoque
=== Estoque da Lojinha ===
4 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  3) Entrada  4) Saída
5) Salvar  6) Desfazer   0) Sair

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    10  R$   15,90
  2  Caneta                     25  R$    2,50
  3  Borracha                   23  R$    1,75
  4  Apontador                   8  R$    4,35
Valor do estoque: R$ 296,55

Opção: 2
Nome: Cola
Quantidade: 10
Preço (R$): 3,50
Cadastrado: 5 - Cola

Opção: 4
Código: 2
Quantas unidades? 5
Caneta: agora são 20.

Opção: 4
Código: 4
Quantas unidades? 9
Só há 8 de Apontador.

Opção: 0

Salvei 5 produtos em estoque.bin. Até amanhã!
  1. A ficha do produto e o primeiro arquivo (fwrite e fread).
  2. O menu e o cadastro (e o centavo que sumia).
  3. Entrada, saída, salvar e desfazer.
  4. Um cabeçalho no arquivo e salvar sem perder nada.

Antes de começar

Você precisa ter visto Structs, typedef, Arquivos (fopen e fclose) e, principalmente, Arquivos Binários, que explica o fwrite e o fread argumento por argumento. O arquivo vai se chamar estoque.c:

$ mkdir -p c/estoque
$ cd c/estoque

Versão 1 · A ficha do produto e o primeiro arquivo

Pense numa caixa de fichas de papelão, todas do mesmo tamanho: cada produto tem a sua, com os mesmos campos no mesmo lugar. Em C, a ficha é uma struct e a caixa é um array. Gravar o estoque é copiar a caixa inteira para o disco; ler é copiar de volta.

Antes do código, duas decisões sobre a ficha, porque ela vai parar no disco:

  • Os números são int32_t, e não int ou long. O int32_t tem 4 bytes em qualquer computador (aula Inteiros de Tamanho Fixo). Assim, a ficha tem sempre o mesmo tamanho, e o arquivo gravado hoje continua legível amanhã, mesmo com outro compilador.
  • O preço é guardado em centavos: R$ 15,90 vira o inteiro 1590. Um double não guarda 15,90 exato (aula float e double), e somar preços com vírgula vai acumulando erros. Com centavos inteiros, a conta é exata.

A primeira versão ainda não tem menu. Ela carrega o arquivo; se ele não existe, começa com três produtos. Depois mostra o estoque, "vende" um caderno e salva. Cada vez que roda, o caderno diminui: é o jeito mais simples de ver que o programa lembra.

#include <stdio.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);

    if (n == 0) {
        printf("Arquivo novo: começando com 3 produtos.\n");
        estoque[0] = (Produto){1, "Caderno", 12, 1590};
        estoque[1] = (Produto){2, "Caneta", 40, 250};
        estoque[2] = (Produto){3, "Borracha", 25, 175};
        n = 3;
    } else {
        printf("Li %d produtos de %s.\n", n, ARQUIVO);
    }

    for (int i = 0; i < n; i++) {
        printf("%d  %-10s %3d  R$ %d,%02d\n", estoque[i].codigo, estoque[i].nome,
               estoque[i].quantidade, estoque[i].preco / 100, estoque[i].preco % 100);
    }

    estoque[0].quantidade--;        // a Ana vendeu um caderno
    printf("Vendi 1 %s. Agora são %d.\n", estoque[0].nome, estoque[0].quantidade);

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("Salvei %d produtos (%zu bytes).\n", n, n * sizeof(Produto));
    return 0;
}
LinhaO que faz
#include <stdint.h>Traz o int32_t.
typedef struct { … } Produto;A ficha: código, nome (até 23 letras e o '\0'), quantidade e preço em centavos. São 4 + 24 + 4 + 4 = 36 bytes.
int carregar(Produto v[], int max)Abre o arquivo com "rb" (ler, binário). Se ele não existe, o fopen devolve NULL e a função devolve 0: estoque vazio, nada de erro.
fread(v, sizeof(Produto), (size_t)max, f)"Leia até max fichas de 36 bytes para dentro de v." O arquivo pode ter menos: o fread devolve quantas fichas inteiras leu, e é esse o número de produtos. O (size_t) converte o int para o tipo que o fread espera.
bool salvar(const Produto v[], int n)Abre com "wb" (escrever, binário), que cria o arquivo ou apaga o que havia, e grava as n fichas de uma vez. O const promete que salvar não muda o estoque.
return gravados == (size_t)n;Verdadeiro só se o fwrite gravou todas as fichas. Com o disco cheio, ele grava menos, e o main avisa.
estoque[0] = (Produto){1, "Caderno", 12, 1590};Preenche uma ficha inteira de uma vez, na ordem dos campos. O (Produto){…} é um "literal composto" C99: uma struct sem nome, criada ali mesmo.
"R$ %d,%02d", preco / 100, preco % 100Mostra os centavos como reais: 1590 / 100 é 15 e 1590 % 100 é 90. O %02d põe o zero da esquerda: 250 centavos viram 2,50, e não 2,5.
n * sizeof(Produto)O tamanho do arquivo: 3 × 36 = 108 bytes. O resultado é size_t, por isso o %zu.

A Ana compilou e rodou três vezes seguidas:

$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque
Arquivo novo: começando com 3 produtos.
1  Caderno     12  R$ 15,90
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 11.
Salvei 3 produtos (108 bytes).
$ ./estoque
Li 3 produtos de estoque.bin.
1  Caderno     11  R$ 15,90           « lembrou da venda de ontem
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 10.
Salvei 3 produtos (108 bytes).
$ ./estoque
Li 3 produtos de estoque.bin.
1  Caderno     10  R$ 15,90
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 9.
Salvei 3 produtos (108 bytes).
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 108 out 17 11:29 estoque.bin

O programa terminou três vezes, e a memória dele sumiu três vezes. Quem lembrou do caderno foi o estoque.bin, com os seus 108 bytes. Veja o que tem dentro com o xxd (aula Arquivos Binários):

$ xxd estoque.bin
00000000: 0100 0000 4361 6465 726e 6f00 0000 0000  ....Caderno.....
00000010: 0000 0000 0000 0000 0000 0000 0900 0000  ................
00000020: 3606 0000 0200 0000 4361 6e65 7461 0000  6.......Caneta..
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 2800 0000 fa00 0000 0300 0000 426f 7272  (...........Borr
00000050: 6163 6861 0000 0000 0000 0000 0000 0000  acha............
00000060: 0000 0000 1900 0000 af00 0000            ............
Bytes 0 a 3: 01 00 00 00
O código do caderno, 1. O byte pequeno vem primeiro (little-endian, explicado em Arquivos Binários).
Bytes 4 a 27: 43 61 64 65 72 6e 6f 00 …
O nome: as letras de "Caderno", o '\0' e zeros até completar 24 bytes. Um nome curto ocupa a ficha inteira, como num formulário de papel.
Bytes 28 a 31: 09 00 00 00
A quantidade: 9 cadernos, depois das três vendas.
Bytes 32 a 35: 36 06 00 00
O preço: 0x0636 é 1590 centavos.
Byte 36 (0x24) em diante
A ficha da caneta começa exatamente onde a do caderno acaba: 02 00 00 00 é o código 2. Cada produto ocupa 36 bytes, um atrás do outro, sem separador.

Como cada ficha tem tamanho fixo, dá para ir direto a um campo. O od abaixo pula 28 bytes (-j 28) e lê 4 (-N 4) como um inteiro (-t d4): é a quantidade de cadernos.

$ od -A d -t d4 -j 28 -N 4 estoque.bin
0000028           9
0000032

Por que int32_t: o editor e o gcc não concordam sobre o long

A primeira ideia da Ana era usar os tipos de sempre: long no código e double no preço. Este programa compara o tamanho das duas fichas:

#include <stdio.h>
#include <stdint.h>

typedef struct {            // a ficha com os tipos de sempre
    long codigo;
    char nome[24];
    int quantidade;
    double preco;
} ProdutoVelho;

typedef struct {            // a ficha do projeto
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;
} Produto;

int main(void) {
    printf("long: %zu bytes, double: %zu bytes\n", sizeof(long), sizeof(double));
    printf("ProdutoVelho: %zu bytes\n", sizeof(ProdutoVelho));
    printf("Produto:      %zu bytes\n", sizeof(Produto));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra tamanhos.c -o tamanhos
$ ./tamanhos
long: 8 bytes, double: 8 bytes
ProdutoVelho: 48 bytes
Produto:      36 bytes

No editor do guia, o mesmo programa mostra outra coisa:

$ clang -std=c23 -Wall -Wextra tamanhos.c -o tamanhos
$ ./tamanhos
long: 4 bytes, double: 8 bytes
ProdutoVelho: 40 bytes
Produto:      36 bytes

O editor é um computador de 32 bits (WebAssembly), em que o long tem 4 bytes; no Linux de 64 bits do laboratório, 8. A ProdutoVelho tem 48 bytes num e 40 no outro (e ainda há bytes de enchimento entre os campos, para alinhar o double; veja Structs). Um arquivo gravado com ela num lado seria lido torto no outro. A Produto, só com int32_t e char, tem 36 bytes nos dois, sem enchimento.

O estoque volta sempre para o começo

Na pressa, a Ana copiou a linha do fopen de salvar para carregar e deixou o modo "wb" nas duas. Ela guardou essa cópia como errado.c, que compilou sem nenhum aviso. Rodou duas vezes o programa certo e depois o errado:

$ gcc -std=c23 -Wall -Wextra errado.c -o estoque-errado
$ rm estoque.bin                          « começa do zero
$ ./estoque
Arquivo novo: começando com 3 produtos.
1  Caderno     12  R$ 15,90
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 11.
Salvei 3 produtos (108 bytes).
$ ./estoque
Li 3 produtos de estoque.bin.
1  Caderno     11  R$ 15,90
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 10.
Salvei 3 produtos (108 bytes).
$ ./estoque-errado
Arquivo novo: começando com 3 produtos.        « os 10 cadernos sumiram
1  Caderno     12  R$ 15,90
2  Caneta      40  R$ 2,50
3  Borracha    25  R$ 1,75
Vendi 1 Caderno. Agora são 11.
Salvei 3 produtos (108 bytes).

O que aconteceu: o "w" quer dizer "vou escrever um arquivo novo". O fopen com "wb" apaga o conteúdo na mesma hora, antes de qualquer leitura. Depois, o fread num arquivo aberto só para escrita não lê nada e devolve 0, e o programa achou que era a primeira vez. O estoque de verdade foi perdido sem nenhuma mensagem de erro.

Como consertar: para ler, "rb"; para gravar tudo de novo, "wb". Quando estiver em dúvida, consulte a tabela de modos em Arquivos (fopen e fclose), e guarde uma cópia do arquivo (cp estoque.bin backup.bin) antes de testar um programa novo.

Missão (versão 1): as funções carregar e salvar estão sem o fread e o fwrite. Troque os dois 0 marcados pelas chamadas certas, com sizeof(Produto) como tamanho de cada item. No editor, a pasta começa vazia a cada Rodar; por isso, no fim do main, o programa lê de novo o que acabou de gravar, num array novo, e mostra o que achou no disco.

#include <stdio.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = 0;           // troque o 0 pelo fread
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = 0;        // troque o 0 pelo fwrite
    fclose(f);
    return gravados == (size_t)n;
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);

    if (n == 0) {
        printf("Arquivo novo: começando com 3 produtos.\n");
        estoque[0] = (Produto){1, "Caderno", 12, 1590};
        estoque[1] = (Produto){2, "Caneta", 40, 250};
        estoque[2] = (Produto){3, "Borracha", 25, 175};
        n = 3;
    } else {
        printf("Li %d produtos de %s.\n", n, ARQUIVO);
    }

    for (int i = 0; i < n; i++) {
        printf("%d  %-10s %3d  R$ %d,%02d\n", estoque[i].codigo, estoque[i].nome,
               estoque[i].quantidade, estoque[i].preco / 100, estoque[i].preco % 100);
    }

    estoque[0].quantidade--;        // a Ana vendeu um caderno
    printf("Vendi 1 %s. Agora são %d.\n", estoque[0].nome, estoque[0].quantidade);

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("Salvei %d produtos (%zu bytes).\n", n, n * sizeof(Produto));

    // Só no editor: a pasta começa vazia a cada Rodar. Para ver o arquivo
    // funcionando, lemos de novo, num array novo, o que acabamos de gravar.
    Produto conferencia[MAX_PRODUTOS];
    int lidos = carregar(conferencia, MAX_PRODUTOS);
    printf("Conferência: li %d produtos do disco.\n", lidos);
    if (lidos > 0) {
        printf("No disco, o %s tem %d.\n", conferencia[0].nome, conferencia[0].quantidade);
    }
    return 0;
}
#include <stdio.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);

    if (n == 0) {
        printf("Arquivo novo: começando com 3 produtos.\n");
        estoque[0] = (Produto){1, "Caderno", 12, 1590};
        estoque[1] = (Produto){2, "Caneta", 40, 250};
        estoque[2] = (Produto){3, "Borracha", 25, 175};
        n = 3;
    } else {
        printf("Li %d produtos de %s.\n", n, ARQUIVO);
    }

    for (int i = 0; i < n; i++) {
        printf("%d  %-10s %3d  R$ %d,%02d\n", estoque[i].codigo, estoque[i].nome,
               estoque[i].quantidade, estoque[i].preco / 100, estoque[i].preco % 100);
    }

    estoque[0].quantidade--;        // a Ana vendeu um caderno
    printf("Vendi 1 %s. Agora são %d.\n", estoque[0].nome, estoque[0].quantidade);

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("Salvei %d produtos (%zu bytes).\n", n, n * sizeof(Produto));

    // Só no editor: a pasta começa vazia a cada Rodar. Para ver o arquivo
    // funcionando, lemos de novo, num array novo, o que acabamos de gravar.
    Produto conferencia[MAX_PRODUTOS];
    int lidos = carregar(conferencia, MAX_PRODUTOS);
    printf("Conferência: li %d produtos do disco.\n", lidos);
    if (lidos > 0) {
        printf("No disco, o %s tem %d.\n", conferencia[0].nome, conferencia[0].quantidade);
    }
    return 0;
}

Versão 2 · O menu e o cadastro

Vender um caderno a cada execução foi só um teste. Agora o programa vira uma ferramenta: carrega o arquivo, mostra um menu e repete até a Ana escolher 0; aí salva. Os produtos de exemplo saem, e entra o cadastro. Como a Ana vai digitar nomes, quantidades e preços, toda leitura é feita com fgets, uma linha inteira por vez, e depois convertida e conferida, como na Calculadora.

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

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}
FunçãoO que faz
ler_linhaMostra a pergunta, lê a linha com fgets e tira o Enter do fim com strcspn. Devolve false quando a entrada acaba (Ctrl+D), e o while do main para.
ler_inteiroConverte a linha com strtol (aula Texto ↔ Número). Recusa se não leu nada (fim == linha), se sobrou texto depois do número (*fim != '\0') ou se o número é negativo ou grande demais. Devolve -1 para dizer "inválido": nenhuma quantidade de verdade é negativa.
ler_precoTroca a vírgula por ponto (a Ana digita 15,90, mas o strtod só entende 15.90), converte para double e transforma em centavos, arredondando (veja o cenário abaixo).
listarA tabela, com %3d, %-23s e %4d (aula Formatos do printf), o aviso (acabando!) para quem tem 5 ou menos e o valor total. O total é long long e cada conta usa (long long), porque 100 produtos × 1000 unidades × R$ 500,00 passam do maior int32_t.
cadastrarRecebe int *n, um ponteiro para o contador do main (aula Passar por Ponteiro), porque precisa aumentá-lo. Confere se cabe, se o nome tem até 23 letras, a quantidade e o preço. O código novo é o maior que já existe mais 1.
Produto p = {0};Começa a ficha toda zerada, inclusive os 24 bytes do nome. Sem isso, os bytes depois do '\0' seriam lixo da memória, e esse lixo iria parar no arquivo.
strcmp(linha, "1") == 0Compara a opção como texto (aula Funções de String): assim, 12 ou 1a não passam por 1.

Primeiro dia: a Ana cadastra o que tem na loja. No dia seguinte, abre de novo, e os três produtos estão lá; ela cadastra o apontador:

$ rm estoque.bin                          « o arquivo da versão 1 tinha os exemplos
$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque
=== Estoque da Lojinha ===
0 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  0) Sair

Opção: 1
O estoque está vazio.

Opção: 2
Nome: Caderno
Quantidade: 12
Preço (R$): 15,90
Cadastrado: 1 - Caderno

Opção: 2
Nome: Caneta
Quantidade: 40
Preço (R$): 2.50
Cadastrado: 2 - Caneta

Opção: 2
Nome: Borracha
Quantidade: 3
Preço (R$): 1.75
Cadastrado: 3 - Borracha

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    12  R$   15,90
  2  Caneta                     40  R$    2,50
  3  Borracha                    3  R$    1,75  (acabando!)
Valor do estoque: R$ 296,05

Opção: 0

Salvei 3 produtos em estoque.bin. Até amanhã!
$ ./estoque
=== Estoque da Lojinha ===
3 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  0) Sair

Opção: 2
Nome: Apontador
Quantidade: 8
Preço (R$): 4.35
Cadastrado: 4 - Apontador

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    12  R$   15,90
  2  Caneta                     40  R$    2,50
  3  Borracha                    3  R$    1,75  (acabando!)
  4  Apontador                   8  R$    4,35
Valor do estoque: R$ 330,85

Opção: 0

Salvei 4 produtos em estoque.bin. Até amanhã!
$ ls -l estoque.bin
-rw-rw-r-- 1 ana ana 144 out 17 11:29 estoque.bin      « 4 × 36 bytes

E as respostas erradas? Cada uma é recusada com uma mensagem, e o menu continua:

$ ./estoque
=== Estoque da Lojinha ===
4 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  0) Sair

Opção: 2
Nome: Caderno universitario 200 folhas
Nome comprido demais (no máximo 23 letras).

Opção: 2
Nome: Cola
Quantidade: doze
"doze" não é um número válido.

Opção: 2
Nome: Cola
Quantidade: 10
Preço (R$): 3 reais
"3 reais" não é um preço válido.

Opção: 7
Opção desconhecida: 7
1) Listar  2) Cadastrar  0) Sair

Opção: 0

Salvei 4 produtos em estoque.bin. Até amanhã!
Por que conferir o tamanho do nome antes do strcpy

O strcpy copia até achar o '\0', sem saber o tamanho do destino. Com o nome de 32 letras acima, ele escreveria 33 bytes nos 24 de p.nome e estragaria os campos seguintes (aula Segurança). Conferido o strlen antes, o strcpy é seguro. Os nomes do exemplo não têm acento de propósito: cada letra acentuada ocupa 2 bytes e desalinha a tabela, como no Boletim de Notas.

R$ 4,35 virou R$ 4,34

Na primeira tentativa, a Ana escreveu a conversão para centavos sem o + 0.5: return (int32_t)(reais * 100);. Compilou sem aviso e cadastrou dois produtos:

$ cp estoque.bin bom.bin                  « uma cópia do estoque, antes do teste
$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque
=== Estoque da Lojinha ===
4 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  0) Sair

Opção: 2
Nome: Cola
Quantidade: 6
Preço (R$): 4.35
Cadastrado: 5 - Cola

Opção: 2
Nome: Clipes
Quantidade: 30
Preço (R$): 0.29
Cadastrado: 6 - Clipes

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    12  R$   15,90
  2  Caneta                     40  R$    2,50
  3  Borracha                    3  R$    1,75  (acabando!)
  4  Apontador                   8  R$    4,35
  5  Cola                        6  R$    4,34          « digitou 4.35
  6  Clipes                     30  R$    0,28          « digitou 0.29
Valor do estoque: R$ 365,29

Opção: 0

Salvei 6 produtos em estoque.bin. Até amanhã!
$ cp bom.bin estoque.bin                  « devolve o estoque de antes do teste

O que aconteceu: o double guarda os números em binário, e 4,35 não tem representação exata: o mais perto que ele consegue é um pouquinho menos, e 4.35 * 100 dá 434.99999999999994. A conversão (int32_t) não arredonda: ela corta a parte depois do ponto, e sobram 434 centavos. Com 0.29, o mesmo: 28.999999999999996 vira 28. O erro acontece só com alguns preços (15.90 e 2.50 deram certo), e por isso é traiçoeiro.

Como consertar: some meio centavo antes de cortar: (int32_t)(reais * 100 + 0.5). 434,99… + 0,5 = 435,49…, cortado dá 435. (Funciona para números positivos, que é o caso de preços. Outra saída é lround, do <math.h>, que no gcc pede -lm; veja Matemática.) É exatamente por isso que o programa guarda centavos: o double aparece uma única vez, na leitura, e é arredondado ali.

Missão (versão 2): este é o programa com o defeito do cenário: a Ana cadastra o apontador a R$ 4,35 e os clipes a R$ 0,29, e a tabela mostra 4,34 e 0,28. Conserte a conversão no fim de ler_preco para os dois preços saírem certos.

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

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100);
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}

Versão 3 · Entrada, saída, salvar e desfazer

Um estoque muda o dia todo: chega mercadoria (entrada) e sai mercadoria (venda). A versão 3 acrescenta quatro opções ao menu:

  • 3) Entrada e 4) Saída: a Ana diz o código do produto e quantas unidades; a saída não deixa vender mais do que existe.
  • 5) Salvar: grava agora, sem sair.
  • 6) Desfazer: joga fora o que mudou desde o último salvamento e carrega de novo o arquivo. É o "voltar" que o caderno de papel não tem, e vem de graça: o arquivo no disco é a última versão salva.

As partes novas (o resto é igual à versão 2):

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    if (sinal < 0 && qtd > v[i].quantidade) {
        printf("Só há %d de %s.\n", v[i].quantidade, v[i].nome);
        return;
    }
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

// dentro do while do main:
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            } else {
                printf("Erro: o estoque não foi salvo!\n");
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
int achar(const Produto v[], int n, int32_t codigo)
Procura o código, um produto por vez, e devolve a posição no array, ou -1 se não achou. Com a posição, o resto do programa mexe na ficha certa.
void movimentar(Produto v[], int n, int sinal)
Uma função só para entrada e saída: o sinal é +1 ou -1, e sinal * qtd soma ou tira. Duas funções quase iguais seriam o dobro de código para manter.
if (qtd <= 0) return;
O ler_inteiro já mostrou a mensagem quando a resposta era inválida (-1); mexer em 0 unidades não faz sentido.
if (sinal < 0 && qtd > v[i].quantidade)
Só na saída: não dá para vender 5 borrachas se há 3. Sem esse teste, o estoque ficaria negativo.
n = carregar(estoque, MAX_PRODUTOS);
O desfazer: o array é sobrescrito com o que está no disco, e n volta a ser o número de produtos salvos (um produto cadastrado e não salvo também some).
$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque
=== Estoque da Lojinha ===
4 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  3) Entrada  4) Saída
5) Salvar  6) Desfazer   0) Sair

Opção: 4
Código: 2
Quantas unidades? 15
Caneta: agora são 25.

Opção: 4
Código: 3
Quantas unidades? 5
Só há 3 de Borracha.

Opção: 3
Código: 3
Quantas unidades? 20
Borracha: agora são 23.

Opção: 4
Código: 9
Não existe produto com o código 9.

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    12  R$   15,90
  2  Caneta                     25  R$    2,50
  3  Borracha                   23  R$    1,75
  4  Apontador                   8  R$    4,35
Valor do estoque: R$ 328,35

Opção: 6
Voltei ao que estava salvo: 4 produtos.

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    12  R$   15,90
  2  Caneta                     40  R$    2,50          « as mudanças foram desfeitas
  3  Borracha                    3  R$    1,75  (acabando!)
  4  Apontador                   8  R$    4,35
Valor do estoque: R$ 330,85

Opção: 4
Código: 2
Quantas unidades? 15
Caneta: agora são 25.

Opção: 3
Código: 3
Quantas unidades? 20
Borracha: agora são 23.

Opção: 5
Salvei 4 produtos em estoque.bin.

Opção: 0

Salvei 4 produtos em estoque.bin. Até amanhã!

Repare: o programa trabalha o tempo todo com o array na memória, que é rápido, e só encosta no disco para carregar e para salvar. Para um estoque de lojinha, com dezenas ou centenas de produtos, regravar o arquivo inteiro leva um instante. Para arquivos enormes, dá para regravar só uma ficha, com fseek e o modo "r+b" (aula Posição no Arquivo).

Missão (versão 3): a função movimentar está sem o teste da saída, e a Ana consegue vender 5 borrachas quando só tem 3 (o estoque fica em -2). Escreva o teste no lugar do comentário: na saída (sinal < 0), se qtd for maior que a quantidade, mostre Só há 3 de Borracha. (com %d e %s) e saia da função. A entrada da missão cadastra 3 borrachas, tenta vender 5 e depois vende 2.

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

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    // confira aqui se há unidades suficientes para a saída
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  3) Entrada  4) Saída\n");
    printf("5) Salvar  6) Desfazer   0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            } else {
                printf("Erro: o estoque não foi salvo!\n");
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

// Lê o estoque do disco e devolve quantos produtos leu.
// Se o arquivo ainda não existe, devolve 0.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)max, f);
    fclose(f);
    return (int)lidos;
}

// Grava os n produtos no disco. Devolve true se gravou todos.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(ARQUIVO, "wb");
    if (f == NULL) {
        perror(ARQUIVO);
        return false;
    }
    size_t gravados = fwrite(v, sizeof(Produto), (size_t)n, f);
    fclose(f);
    return gravados == (size_t)n;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    if (sinal < 0 && qtd > v[i].quantidade) {
        printf("Só há %d de %s.\n", v[i].quantidade, v[i].nome);
        return;
    }
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  3) Entrada  4) Saída\n");
    printf("5) Salvar  6) Desfazer   0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            } else {
                printf("Erro: o estoque não foi salvo!\n");
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        printf("Erro: o estoque não foi salvo!\n");
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}

Versão 4 · Um cabeçalho no arquivo e salvar sem perder nada

O programa funciona, mas confia demais no arquivo. Ele lê qualquer estoque.bin que encontrar, de 36 em 36 bytes, sem saber se aquilo é mesmo um estoque. E, se um dia a ficha mudar (um campo novo, por exemplo), os arquivos antigos vão ser lidos tortos, sem nenhum aviso (o cenário "os dois programas não concordam sobre a struct", em Arquivos Binários). A versão 4 resolve isso de dois jeitos.

1. Um cabeçalho com assinatura e versão

Muitos formatos de arquivo começam com alguns bytes fixos, a assinatura (ou "número mágico"): todo PDF começa com %PDF, todo PNG com \x89PNG. O comando file do Linux reconhece o tipo de um arquivo por esses bytes, sem olhar o nome. O estoque da Ana vai começar com 12 bytes de cabeçalho:

BytesCampoConteúdo
0 a 3assinaturaAs letras L O J A: "isto é um estoque da lojinha".
4 a 7versao1. Se o formato mudar, a versão nova grava 2, e cada programa sabe o que está lendo.
8 a 11totalQuantos produtos vêm depois. Assim dá para saber se o arquivo foi cortado.
12 em dianteos produtosAs fichas de 36 bytes, como antes. O produto i começa no byte 12 + i × 36.

2. Salvar num arquivo temporário e trocar no fim

O salvar das versões anteriores abre estoque.bin com "wb", e nesse instante o estoque antigo já foi apagado. Se o programa morrer no meio da gravação (falta de luz, disco cheio, um Ctrl+C), a Ana fica sem o novo e sem o antigo. O jeito seguro, usado por editores de texto e bancos de dados:

  1. grave tudo num arquivo novo, estoque.tmp;
  2. confira cada fwrite e o fclose;
  3. só então troque o nome com rename("estoque.tmp", "estoque.bin"). No Linux, o rename substitui o arquivo antigo de uma vez só: quem abrir estoque.bin vê o antigo inteiro ou o novo inteiro, nunca um pedaço.
#define TEMPORARIO "estoque.tmp"
#define VERSAO_ARQUIVO 1

typedef struct {
    char assinatura[4];     // sempre 'L', 'O', 'J', 'A'
    int32_t versao;         // o formato do arquivo
    int32_t total;          // quantos produtos vêm depois
} Cabecalho;

// Lê o estoque do disco. Devolve quantos produtos leu (0 se o arquivo
// ainda não existe) ou -1 se o arquivo não é um estoque da lojinha.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    Cabecalho cab;
    if (fread(&cab, sizeof cab, 1, f) != 1 ||
        memcmp(cab.assinatura, "LOJA", 4) != 0) {
        printf("%s não é um arquivo da lojinha.\n", ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.versao != VERSAO_ARQUIVO) {
        printf("%s é da versão %d; eu só leio a versão %d.\n",
               ARQUIVO, cab.versao, VERSAO_ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.total < 0 || cab.total > max) {
        printf("%s diz ter %d produtos; eu aceito até %d.\n",
               ARQUIVO, cab.total, max);
        fclose(f);
        return -1;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)cab.total, f);
    fclose(f);
    if (lidos != (size_t)cab.total) {
        printf("%s está cortado: li %zu de %d produtos.\n",
               ARQUIVO, lidos, cab.total);
        return -1;
    }
    return cab.total;
}

// Grava num arquivo temporário e só no fim troca pelo verdadeiro.
// Devolve true se deu tudo certo.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(TEMPORARIO, "wb");
    if (f == NULL) {
        perror(TEMPORARIO);
        return false;
    }
    Cabecalho cab = {{'L', 'O', 'J', 'A'}, VERSAO_ARQUIVO, n};
    bool ok = fwrite(&cab, sizeof cab, 1, f) == 1 &&
              fwrite(v, sizeof(Produto), (size_t)n, f) == (size_t)n;
    if (fclose(f) != 0) {
        ok = false;
    }
    if (!ok || rename(TEMPORARIO, ARQUIVO) != 0) {
        perror("Erro ao salvar");
        remove(TEMPORARIO);
        return false;
    }
    return true;
}

E o main passa a recusar um arquivo estranho, em vez de sobrescrevê-lo ao sair (na opção 6, o desfazer, o mesmo teste: if (n < 0) return 1;):

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    if (n < 0) {
        printf("Não vou mexer nele. Confira o arquivo e tente de novo.\n");
        return 1;
    }
    // ... o resto do main é igual ao da versão 3
}
typedef struct { char assinatura[4]; int32_t versao; int32_t total; } Cabecalho;
O cabeçalho também é uma struct, gravada e lida com um fwrite e um fread. São 4 + 4 + 4 = 12 bytes, sem enchimento, no editor e no gcc. A assinatura é um array de 4 char, sem '\0': não é uma string, são só 4 bytes.
memcmp(cab.assinatura, "LOJA", 4) != 0
O memcmp ("memory compare", do <string.h>) compara 4 bytes, como o strcmp, mas sem procurar '\0'. Devolve 0 se são iguais.
fread(&cab, sizeof cab, 1, f) != 1 || …
Se o arquivo tem menos de 12 bytes, nem o cabeçalho existe. O || para no primeiro teste verdadeiro, então o memcmp só olha o cabeçalho quando ele foi lido.
cab.total < 0 || cab.total > max
Nunca confie num número que veio do arquivo: um total de 5000 faria o fread escrever além do array de 100 produtos.
lidos != (size_t)cab.total
O cabeçalho promete um número de produtos; se vieram menos, o arquivo foi cortado.
Cabecalho cab = {{'L', 'O', 'J', 'A'}, VERSAO_ARQUIVO, n};
As chaves de dentro preenchem o array da assinatura; depois vêm a versão e o total.
bool ok = fwrite(…) == 1 && fwrite(…) == (size_t)n;
As duas gravações têm de dar certo. O && nem tenta a segunda se a primeira falhou.
if (fclose(f) != 0)
O fwrite guarda os bytes num buffer, e o erro de disco cheio pode aparecer só no fclose. Por isso ele também é conferido.
rename(TEMPORARIO, ARQUIVO)
Troca o nome; devolve 0 se deu certo. Se algo falhou, o remove apaga o temporário, e o estoque.bin antigo continua intacto.

A primeira coisa que a Ana viu com a versão 4 foi a recusa: o estoque.bin da versão 3 não tem cabeçalho (ele começa com 01 00 00 00, o código do caderno):

$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque; echo $?
estoque.bin não é um arquivo da lojinha.
Não vou mexer nele. Confira o arquivo e tente de novo.
1

É a proteção funcionando: sem ela, a versão 4 leria o código do caderno como assinatura e, ao sair, gravaria por cima. Para não perder o estoque, a Ana escreveu um conversor de uso único, que lê as fichas no formato antigo e grava o cabeçalho na frente:

// Transforma o estoque da versão 3 (sem cabeçalho) no formato novo.
#include <stdio.h>
#include <stdint.h>

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;
} Produto;

typedef struct {
    char assinatura[4];
    int32_t versao;
    int32_t total;
} Cabecalho;

int main(void) {
    Produto v[100];
    FILE *f = fopen("estoque-v3.bin", "rb");
    if (f == NULL) {
        perror("estoque-v3.bin");
        return 1;
    }
    size_t n = fread(v, sizeof(Produto), 100, f);
    fclose(f);

    Cabecalho cab = {{'L', 'O', 'J', 'A'}, 1, (int32_t)n};
    f = fopen("estoque.bin", "wb");
    if (f == NULL) {
        perror("estoque.bin");
        return 1;
    }
    bool ok = fwrite(&cab, sizeof cab, 1, f) == 1 &&
              fwrite(v, sizeof(Produto), n, f) == n;
    if (fclose(f) != 0 || !ok) {
        printf("Erro ao gravar estoque.bin\n");
        return 1;
    }
    printf("Converti %zu produtos para o formato novo.\n", n);
    return 0;
}
$ mv estoque.bin estoque-v3.bin
$ gcc -std=c23 -Wall -Wextra converter.c -o converter
$ ./converter
Converti 4 produtos para o formato novo.
$ ls -l estoque.bin estoque-v3.bin
-rw-rw-r-- 1 ana ana 156 out 17 11:29 estoque.bin           « 12 + 4 × 36
-rw-rw-r-- 1 ana ana 144 out 17 11:29 estoque-v3.bin
$ xxd -l 48 estoque.bin
00000000: 4c4f 4a41 0100 0000 0400 0000 0100 0000  LOJA............
00000010: 4361 6465 726e 6f00 0000 0000 0000 0000  Caderno.........
00000020: 0000 0000 0000 0000 0c00 0000 3606 0000  ............6...

Lá está o cabeçalho: 4c 4f 4a 41 são as letras LOJA, depois a versão (01 00 00 00) e o total (04 00 00 00). O caderno começa no byte 12, com o código 01 00 00 00. Agora a versão 4 abre o estoque normalmente, e a Ana registra a venda de 2 cadernos:

$ ./estoque
=== Estoque da Lojinha ===
4 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  3) Entrada  4) Saída
5) Salvar  6) Desfazer   0) Sair

Opção: 4
Código: 1
Quantas unidades? 2
Caderno: agora são 10.

Opção: 0

Salvei 4 produtos em estoque.bin. Até amanhã!

Depois, testou as proteções, guardando uma cópia antes:

$ cp estoque.bin bom.bin
$ echo 'lista de compras' > estoque.bin            « um arquivo que não é estoque
$ ./estoque; echo $?
estoque.bin não é um arquivo da lojinha.
Não vou mexer nele. Confira o arquivo e tente de novo.
1
$ cp bom.bin estoque.bin
$ truncate -s 60 estoque.bin                     « corta no meio do 2º produto
$ ./estoque
estoque.bin está cortado: li 1 de 4 produtos.
Não vou mexer nele. Confira o arquivo e tente de novo.
$ cp bom.bin estoque.bin
$ printf '\x02' | dd of=estoque.bin bs=1 seek=4 conv=notrunc status=none
$ xxd -l 12 estoque.bin
00000000: 4c4f 4a41 0200 0000 0400 0000            LOJA........
$ ./estoque
estoque.bin é da versão 2; eu só leio a versão 1.
Não vou mexer nele. Confira o arquivo e tente de novo.
$ cp bom.bin estoque.bin

O truncate -s 60 deixa só os primeiros 60 bytes: o cabeçalho (12), o caderno (36) e um pedaço da caneta. O printf … | dd … seek=4 conv=notrunc troca só o byte 4 por 02, como se o arquivo viesse de uma versão futura do programa. Nos três casos, o programa explicou o problema e saiu sem salvar: o arquivo ficou como estava, para a Ana investigar.

error: incompatible type for argument 1 of ‘fwrite’

Ao escrever o salvar, a Ana esqueceu o & antes do cabeçalho: fwrite(cab, sizeof cab, 1, f).

$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
estoque.c: In function ‘salvar’:
estoque.c:70:22: error: incompatible type for argument 1 of ‘fwrite’
   70 |     bool ok = fwrite(cab, sizeof cab, 1, f) == 1 &&
      |                      ^~~
      |                      |
      |                      Cabecalho
In file included from estoque.c:1:
/usr/include/stdio.h:739:46: note: expected ‘const void * restrict’ but argument is of type ‘Cabecalho’
  739 | extern size_t fwrite (const void *__restrict __ptr, size_t __size,
      |                       ~~~~~~~~~~~~~~~~~~~~~~~^~~~~

No editor do guia, o Clang diz o mesmo com outras palavras: error: passing 'Cabecalho' to parameter of incompatible type 'const void *'.

O que significa: o primeiro argumento do fwrite é um endereço (const void *, um ponteiro para qualquer coisa; veja Ponteiro void*): "os bytes estão aqui". O cab é a struct inteira, não o endereço dela. A nota mostra a declaração do fwrite no stdio.h e o tipo que ele esperava.

Como consertar: fwrite(&cab, sizeof cab, 1, f). Para um array, como o v dos produtos, o & não vai: o nome do array já é o endereço do primeiro elemento (aula Ponteiros e Arrays).

Missão (versão 4): a aba estoque.bin é um arquivo de texto, uma lista de compras, no lugar do estoque. O carregar lê o cabeçalho mas não confere a assinatura, e a mensagem sai confusa (uma "versão" absurda, que são as letras a de lidas como número). Acrescente o teste da assinatura no if marcado, com memcmp, para o programa dizer estoque.bin não é um arquivo da lojinha. e sair com o código 1.

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

#define ARQUIVO "estoque.bin"
#define TEMPORARIO "estoque.tmp"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5
#define VERSAO_ARQUIVO 1

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

typedef struct {
    char assinatura[4];     // sempre 'L', 'O', 'J', 'A'
    int32_t versao;         // o formato do arquivo
    int32_t total;          // quantos produtos vêm depois
} Cabecalho;

// Lê o estoque do disco. Devolve quantos produtos leu (0 se o arquivo
// ainda não existe) ou -1 se o arquivo não é um estoque da lojinha.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    Cabecalho cab;
    if (fread(&cab, sizeof cab, 1, f) != 1) {  // e a assinatura?
        printf("%s não é um arquivo da lojinha.\n", ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.versao != VERSAO_ARQUIVO) {
        printf("%s é da versão %d; eu só leio a versão %d.\n",
               ARQUIVO, cab.versao, VERSAO_ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.total < 0 || cab.total > max) {
        printf("%s diz ter %d produtos; eu aceito até %d.\n",
               ARQUIVO, cab.total, max);
        fclose(f);
        return -1;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)cab.total, f);
    fclose(f);
    if (lidos != (size_t)cab.total) {
        printf("%s está cortado: li %zu de %d produtos.\n",
               ARQUIVO, lidos, cab.total);
        return -1;
    }
    return cab.total;
}

// Grava num arquivo temporário e só no fim troca pelo verdadeiro.
// Devolve true se deu tudo certo.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(TEMPORARIO, "wb");
    if (f == NULL) {
        perror(TEMPORARIO);
        return false;
    }
    Cabecalho cab = {{'L', 'O', 'J', 'A'}, VERSAO_ARQUIVO, n};
    bool ok = fwrite(&cab, sizeof cab, 1, f) == 1 &&
              fwrite(v, sizeof(Produto), (size_t)n, f) == (size_t)n;
    if (fclose(f) != 0) {
        ok = false;
    }
    if (!ok || rename(TEMPORARIO, ARQUIVO) != 0) {
        perror("Erro ao salvar");
        remove(TEMPORARIO);
        return false;
    }
    return true;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    if (sinal < 0 && qtd > v[i].quantidade) {
        printf("Só há %d de %s.\n", v[i].quantidade, v[i].nome);
        return;
    }
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  3) Entrada  4) Saída\n");
    printf("5) Salvar  6) Desfazer   0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    if (n < 0) {
        printf("Não vou mexer nele. Confira o arquivo e tente de novo.\n");
        return 1;
    }
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            if (n < 0) {
                return 1;
            }
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}
lista de compras
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define ARQUIVO "estoque.bin"
#define TEMPORARIO "estoque.tmp"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5
#define VERSAO_ARQUIVO 1

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

typedef struct {
    char assinatura[4];     // sempre 'L', 'O', 'J', 'A'
    int32_t versao;         // o formato do arquivo
    int32_t total;          // quantos produtos vêm depois
} Cabecalho;

// Lê o estoque do disco. Devolve quantos produtos leu (0 se o arquivo
// ainda não existe) ou -1 se o arquivo não é um estoque da lojinha.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    Cabecalho cab;
    if (fread(&cab, sizeof cab, 1, f) != 1 ||
        memcmp(cab.assinatura, "LOJA", 4) != 0) {
        printf("%s não é um arquivo da lojinha.\n", ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.versao != VERSAO_ARQUIVO) {
        printf("%s é da versão %d; eu só leio a versão %d.\n",
               ARQUIVO, cab.versao, VERSAO_ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.total < 0 || cab.total > max) {
        printf("%s diz ter %d produtos; eu aceito até %d.\n",
               ARQUIVO, cab.total, max);
        fclose(f);
        return -1;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)cab.total, f);
    fclose(f);
    if (lidos != (size_t)cab.total) {
        printf("%s está cortado: li %zu de %d produtos.\n",
               ARQUIVO, lidos, cab.total);
        return -1;
    }
    return cab.total;
}

// Grava num arquivo temporário e só no fim troca pelo verdadeiro.
// Devolve true se deu tudo certo.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(TEMPORARIO, "wb");
    if (f == NULL) {
        perror(TEMPORARIO);
        return false;
    }
    Cabecalho cab = {{'L', 'O', 'J', 'A'}, VERSAO_ARQUIVO, n};
    bool ok = fwrite(&cab, sizeof cab, 1, f) == 1 &&
              fwrite(v, sizeof(Produto), (size_t)n, f) == (size_t)n;
    if (fclose(f) != 0) {
        ok = false;
    }
    if (!ok || rename(TEMPORARIO, ARQUIVO) != 0) {
        perror("Erro ao salvar");
        remove(TEMPORARIO);
        return false;
    }
    return true;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    if (sinal < 0 && qtd > v[i].quantidade) {
        printf("Só há %d de %s.\n", v[i].quantidade, v[i].nome);
        return;
    }
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  3) Entrada  4) Saída\n");
    printf("5) Salvar  6) Desfazer   0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    if (n < 0) {
        printf("Não vou mexer nele. Confira o arquivo e tente de novo.\n");
        return 1;
    }
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            if (n < 0) {
                return 1;
            }
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}

O programa completo

A versão final, igual à versão 4. Compile com gcc -std=c23 -Wall -Wextra estoque.c -o estoque:

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

#define ARQUIVO "estoque.bin"
#define TEMPORARIO "estoque.tmp"
#define MAX_PRODUTOS 100
#define ESTOQUE_BAIXO 5
#define VERSAO_ARQUIVO 1

typedef struct {
    int32_t codigo;
    char nome[24];
    int32_t quantidade;
    int32_t preco;          // em centavos: 1590 = R$ 15,90
} Produto;

typedef struct {
    char assinatura[4];     // sempre 'L', 'O', 'J', 'A'
    int32_t versao;         // o formato do arquivo
    int32_t total;          // quantos produtos vêm depois
} Cabecalho;

// Lê o estoque do disco. Devolve quantos produtos leu (0 se o arquivo
// ainda não existe) ou -1 se o arquivo não é um estoque da lojinha.
int carregar(Produto v[], int max) {
    FILE *f = fopen(ARQUIVO, "rb");
    if (f == NULL) {
        return 0;
    }
    Cabecalho cab;
    if (fread(&cab, sizeof cab, 1, f) != 1 ||
        memcmp(cab.assinatura, "LOJA", 4) != 0) {
        printf("%s não é um arquivo da lojinha.\n", ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.versao != VERSAO_ARQUIVO) {
        printf("%s é da versão %d; eu só leio a versão %d.\n",
               ARQUIVO, cab.versao, VERSAO_ARQUIVO);
        fclose(f);
        return -1;
    }
    if (cab.total < 0 || cab.total > max) {
        printf("%s diz ter %d produtos; eu aceito até %d.\n",
               ARQUIVO, cab.total, max);
        fclose(f);
        return -1;
    }
    size_t lidos = fread(v, sizeof(Produto), (size_t)cab.total, f);
    fclose(f);
    if (lidos != (size_t)cab.total) {
        printf("%s está cortado: li %zu de %d produtos.\n",
               ARQUIVO, lidos, cab.total);
        return -1;
    }
    return cab.total;
}

// Grava num arquivo temporário e só no fim troca pelo verdadeiro.
// Devolve true se deu tudo certo.
bool salvar(const Produto v[], int n) {
    FILE *f = fopen(TEMPORARIO, "wb");
    if (f == NULL) {
        perror(TEMPORARIO);
        return false;
    }
    Cabecalho cab = {{'L', 'O', 'J', 'A'}, VERSAO_ARQUIVO, n};
    bool ok = fwrite(&cab, sizeof cab, 1, f) == 1 &&
              fwrite(v, sizeof(Produto), (size_t)n, f) == (size_t)n;
    if (fclose(f) != 0) {
        ok = false;
    }
    if (!ok || rename(TEMPORARIO, ARQUIVO) != 0) {
        perror("Erro ao salvar");
        remove(TEMPORARIO);
        return false;
    }
    return true;
}

// Mostra a pergunta e lê uma linha, sem o Enter do fim.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        return false;
    }
    linha[strcspn(linha, "\n")] = '\0';
    return true;
}

// Lê um número inteiro de 0 a 999999. Devolve -1 se não for válido.
int32_t ler_inteiro(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *fim = NULL;
    long valor = strtol(linha, &fim, 10);
    if (fim == linha || *fim != '\0' || valor < 0 || valor > 999999) {
        printf("\"%s\" não é um número válido.\n", linha);
        return -1;
    }
    return (int32_t)valor;
}

// Lê um preço em reais ("15.90" ou "15,90") e devolve em centavos.
// Devolve -1 se não for válido.
int32_t ler_preco(const char *pergunta) {
    char linha[100];
    if (!ler_linha(pergunta, linha, sizeof linha)) {
        return -1;
    }
    char *virgula = strchr(linha, ',');
    if (virgula != NULL) {
        *virgula = '.';             // aceita o jeito brasileiro
    }
    char *fim = NULL;
    double reais = strtod(linha, &fim);
    if (fim == linha || *fim != '\0' || reais < 0 || reais > 99999) {
        printf("\"%s\" não é um preço válido.\n", linha);
        return -1;
    }
    return (int32_t)(reais * 100 + 0.5);  // arredonda para o centavo
}

int achar(const Produto v[], int n, int32_t codigo) {
    for (int i = 0; i < n; i++) {
        if (v[i].codigo == codigo) {
            return i;
        }
    }
    return -1;
}

void listar(const Produto v[], int n) {
    if (n == 0) {
        printf("O estoque está vazio.\n");
        return;
    }
    long long total = 0;
    printf("Cód  Produto                  Qtd       Preço\n");
    printf("---  -----------------------  ----  ----------\n");
    for (int i = 0; i < n; i++) {
        printf("%3d  %-23s  %4d  R$ %4d,%02d", v[i].codigo, v[i].nome,
               v[i].quantidade, v[i].preco / 100, v[i].preco % 100);
        if (v[i].quantidade <= ESTOQUE_BAIXO) {
            printf("  (acabando!)");
        }
        printf("\n");
        total += (long long)v[i].quantidade * v[i].preco;
    }
    printf("Valor do estoque: R$ %lld,%02lld\n", total / 100, total % 100);
}

void cadastrar(Produto v[], int *n) {
    if (*n == MAX_PRODUTOS) {
        printf("O estoque está cheio (%d produtos).\n", MAX_PRODUTOS);
        return;
    }
    Produto p = {0};
    char linha[100];
    if (!ler_linha("Nome: ", linha, sizeof linha) || linha[0] == '\0') {
        printf("Cadastro cancelado.\n");
        return;
    }
    if (strlen(linha) >= sizeof p.nome) {
        printf("Nome comprido demais (no máximo %zu letras).\n", sizeof p.nome - 1);
        return;
    }
    strcpy(p.nome, linha);
    p.quantidade = ler_inteiro("Quantidade: ");
    if (p.quantidade < 0) {
        return;
    }
    p.preco = ler_preco("Preço (R$): ");
    if (p.preco < 0) {
        return;
    }
    p.codigo = 1;                   // o próximo código livre
    for (int i = 0; i < *n; i++) {
        if (v[i].codigo >= p.codigo) {
            p.codigo = v[i].codigo + 1;
        }
    }
    v[*n] = p;
    (*n)++;
    printf("Cadastrado: %d - %s\n", p.codigo, p.nome);
}

// sinal = +1 para entrada (chegou mercadoria), -1 para saída (venda)
void movimentar(Produto v[], int n, int sinal) {
    int32_t codigo = ler_inteiro("Código: ");
    if (codigo < 0) {
        return;
    }
    int i = achar(v, n, codigo);
    if (i < 0) {
        printf("Não existe produto com o código %d.\n", codigo);
        return;
    }
    int32_t qtd = ler_inteiro("Quantas unidades? ");
    if (qtd <= 0) {
        return;
    }
    if (sinal < 0 && qtd > v[i].quantidade) {
        printf("Só há %d de %s.\n", v[i].quantidade, v[i].nome);
        return;
    }
    v[i].quantidade += sinal * qtd;
    printf("%s: agora são %d.\n", v[i].nome, v[i].quantidade);
}

void mostrar_menu(void) {
    printf("1) Listar  2) Cadastrar  3) Entrada  4) Saída\n");
    printf("5) Salvar  6) Desfazer   0) Sair\n");
}

int main(void) {
    Produto estoque[MAX_PRODUTOS];
    int n = carregar(estoque, MAX_PRODUTOS);
    if (n < 0) {
        printf("Não vou mexer nele. Confira o arquivo e tente de novo.\n");
        return 1;
    }
    printf("=== Estoque da Lojinha ===\n");
    printf("%d produtos carregados de %s.\n", n, ARQUIVO);
    mostrar_menu();

    char linha[100];
    while (ler_linha("\nOpção: ", linha, sizeof linha)) {
        if (strcmp(linha, "1") == 0) {
            listar(estoque, n);
        } else if (strcmp(linha, "2") == 0) {
            cadastrar(estoque, &n);
        } else if (strcmp(linha, "3") == 0) {
            movimentar(estoque, n, +1);
        } else if (strcmp(linha, "4") == 0) {
            movimentar(estoque, n, -1);
        } else if (strcmp(linha, "5") == 0) {
            if (salvar(estoque, n)) {
                printf("Salvei %d produtos em %s.\n", n, ARQUIVO);
            }
        } else if (strcmp(linha, "6") == 0) {
            n = carregar(estoque, MAX_PRODUTOS);
            if (n < 0) {
                return 1;
            }
            printf("Voltei ao que estava salvo: %d produtos.\n", n);
        } else if (strcmp(linha, "0") == 0) {
            break;
        } else {
            printf("Opção desconhecida: %s\n", linha);
            mostrar_menu();
        }
    }

    if (!salvar(estoque, n)) {
        return 1;
    }
    printf("\nSalvei %d produtos em %s. Até amanhã!\n", n, ARQUIVO);
    return 0;
}

O que o projeto juntou: structs e typedef, int32_t, fwrite e fread, fgets, strtol e strtod, passagem por ponteiro, strcmp, strcpy e memcmp e o perror.

Rodando no laboratório

Depois da sessão do começo da página, a Ana abriu o programa de novo: a cola cadastrada e as 5 canetas vendidas estão lá. E o estoque.tmp não sobrou na pasta, porque o rename o transformou no estoque.bin:

$ ./estoque
=== Estoque da Lojinha ===
5 produtos carregados de estoque.bin.
1) Listar  2) Cadastrar  3) Entrada  4) Saída
5) Salvar  6) Desfazer   0) Sair

Opção: 1
Cód  Produto                  Qtd       Preço
---  -----------------------  ----  ----------
  1  Caderno                    10  R$   15,90
  2  Caneta                     20  R$    2,50
  3  Borracha                   23  R$    1,75
  4  Apontador                   8  R$    4,35
  5  Cola                       10  R$    3,50
Valor do estoque: R$ 319,05

Opção: 0

Salvei 5 produtos em estoque.bin. Até amanhã!
$ ls estoque*
estoque  estoque.bin  estoque.c  estoque-errado  estoque-v3.bin

Desafios extras

  1. Remover um produto. Acrescente a opção 7) Remover: ache a posição com achar e puxe os produtos seguintes uma casa para trás (v[j] = v[j + 1]), diminuindo n.
  2. Buscar pelo nome. Mostre só os produtos cujo nome contém o texto digitado, com strstr (aula Funções de String).
  3. Lista de compras. Uma opção que mostra só os produtos (acabando!) e quanto custaria repor cada um até 20 unidades.
  4. Em ordem de nome. Liste em ordem alfabética com qsort e uma função que compara com strcmp (aula Ordenação).
  5. Exportar para planilha. Grave um estoque.csv com fprintf, uma linha por produto (1;Caderno;10;15,90), para abrir no LibreOffice (aula Escrever Arquivos).
  6. Versão 2 do formato. Acrescente à ficha um campo int32_t minimo (o estoque mínimo de cada produto, no lugar do 5 fixo). Grave com VERSAO_ARQUIVO 2 e faça o carregar aceitar também a versão 1, completando o minimo com 5.

Exercícios

1. Por que o preço é guardado como int32_t em centavos, e não como double em reais?

2. Uma struct com long codigo tem 48 bytes no gcc do laboratório e 40 no editor do guia. O que isso causa num arquivo binário?

3. O que acontece com um estoque.bin de 144 bytes quando o programa faz fopen("estoque.bin", "wb")?

4. Por que a versão 4 grava em estoque.tmp e só depois faz rename para estoque.bin?

5. No arquivo da versão 4, o cabeçalho tem 12 bytes e cada produto 36. Em que byte começa o terceiro produto (o de índice 2)?

byte

6. Complete a conversão de reais para centavos, sem perder o centavo do 4,35:

int32_t centavos = (int32_t)(reais * 100 + );

7. Complete a gravação do cabeçalho cab (uma variável, não um array):

fwrite(, sizeof cab, 1, f);

Resumo

  • Uma struct de tamanho fixo vira um registro no arquivo: fwrite(v, sizeof(Produto), n, f) grava o array inteiro e fread lê de volta.
  • Para o disco, use int32_t e companhia: o long tem 8 bytes no gcc e 4 no editor, e a ficha mudaria de tamanho.
  • Dinheiro em centavos inteiros. Ao converter de double, arredonde com + 0.5: o (int32_t) corta, e 4,35 viraria 4,34.
  • "rb" para ler, "wb" para regravar tudo; o "wb" apaga o arquivo na hora.
  • Confira o que fread, fwrite e fclose devolvem, e nunca confie num número que veio do arquivo.
  • Um cabeçalho com assinatura e versão faz o programa recusar arquivos estranhos, cortados ou de outro formato.
  • Salvar com segurança: grave num temporário, confira tudo e troque com rename.

Próximo projeto: Contador de Palavras.