📚 Capítulo do curso: 18 — Manipulação de Arquivos, Streams e Persistência Binária da Especialização em Sistemas com C. Leia o capítulo antes de começar.
Banco de dados transacional em disco com serialização fread/fwrite e busca posicional.
| ⏱️ Tempo estimado | 📦 Você vai entregar | ✅ Pronto quando |
|---|---|---|
| 55 min | main.c + banco.dat gerado |
o registro de índice 2 é lido no offset 96 e só ele é regravado |
fwrite, em formato binário (não texto), preservando o layout exato de memória.fseek para pular direto até ele, sem ler os anteriores — acesso O(1), não O(n).fread a partir de um offset arbitrário.Arquivos de texto são ótimos para humanos, mas ruins para acesso aleatório: para ler a linha 1000 você normalmente precisa ler as 999 anteriores. Arquivos binários de tamanho de registro fixo resolvem isso: se todo registro ocupa exatamente sizeof(struct Registro) bytes, o registro de índice K está sempre no byte K * sizeof(struct Registro) — você pode saltar direto para lá com fseek.
Seu programa (main.c) deve:
struct Registro { int id; char nome[30]; double saldo; };.banco.dat, usando fopen(..., "wb") e fwrite."rb+" (leitura e escrita binária, sem truncar).fseek(arquivo, indice * sizeof(struct Registro), SEEK_SET) para pular diretamente até o registro de índice 2 (terceiro registro), sem ler os registros 0 e 1.fread, imprimir seus campos, e confirmar pelo id que é o registro certo.saldo desse registro (ex: somar 1000.00) e regravar apenas ele no mesmo offset com fseek + fwrite, sem tocar nos outros 4 registros.| ID | Requisito |
|---|---|
| RF01 | O programa deve compilar sem nenhum aviso com gcc -Wall -Wextra -Wpedantic -std=c17. |
| RF02 | Deve gravar 5 registros em banco.dat, abrindo o arquivo em modo "wb" e usando fwrite. |
| RF03 | Deve reabrir o arquivo em modo "rb+" após fechar o handle de escrita. |
| RF04 | Deve calcular o offset do registro de índice K como K * sizeof(struct Registro) e usar fseek(..., SEEK_SET) para posicionar o cursor sem ler os registros anteriores. |
| RF05 | Deve ler o registro de índice 2 com fread e validar pelo campo id que é o registro esperado. |
| RF06 | Deve atualizar o campo saldo do registro lido e regravá-lo no mesmo offset (via novo fseek + fwrite), sem regravar o arquivo inteiro. |
| RF07 | Deve validar o retorno de fopen, fseek, fread e fwrite, encerrando com mensagem de erro em caso de falha. |
graph LR
A["fopen('wb')\n+ fwrite x5"] --> B["fclose"]
B --> C["fopen('rb+')"]
C --> D["fseek(indice * sizeof(Registro))\nSEEK_SET"]
D --> E["fread registro alvo"]
E --> F["atualiza saldo\nem memoria"]
F --> G["fseek de volta ao\nmesmo offset"]
G --> H["fwrite so' o registro\natualizado"]
style A fill:#0284c7,stroke:#38bdf8,color:#ffffff
style D fill:#7c3aed,stroke:#a78bfa,color:#ffffff
style H fill:#059669,stroke:#34d399,color:#ffffff
O mesmo do Miniprojeto 01: gcc 8 ou mais novo. No Windows, rode chcp 65001 se os emojis aparecerem quebrados.
🎯 Objetivo: partir de um código que compila e roda, antes de escrever qualquer linha.
⌨️ Faça: crie a pasta c_mini_18, salve o código abaixo como main.c e rode:
gcc -Wall -Wextra -Wpedantic -std=c17 main.c -o app
./app # Linux/macOS
.\app.exe # Windows (PowerShell)
📄 src/esqueleto.c
/*
* Miniprojeto 18: Mini SGBD Binario com Acesso Direto (fseek O(1))
* Especializacao em Engenharia de Sistemas em C
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TOTAL_REGISTROS 5
#define NOME_ARQUIVO "banco.dat"
struct Registro {
int id;
char nome[30];
double saldo;
};
int main(void) {
printf("====================================================\n");
printf(" ⚡ Miniprojeto 18: Mini SGBD Binario com Acesso Direto\n");
printf("====================================================\n");
struct Registro registros[TOTAL_REGISTROS] = {
{1, "Ana Silva", 1500.00},
{2, "Bruno Costa", 320.50},
{3, "Carla Souza", 9800.75},
{4, "Diego Alves", 150.00},
{5, "Elisa Nunes", 4200.25}
};
printf("\nRegistros em memoria (%zu bytes cada):\n", sizeof(struct Registro));
for (int i = 0; i < TOTAL_REGISTROS; i++) {
printf(" [%d] id=%d nome=%s saldo=%.2f\n",
i, registros[i].id, registros[i].nome, registros[i].saldo);
}
// TODO 1: abra NOME_ARQUIVO em modo "wb", valide contra NULL,
// grave todos os TOTAL_REGISTROS de uma vez com fwrite(), valide o
// numero de itens gravados, e feche o arquivo com fclose().
const int indice_alvo = 2;
printf("\nRegistro alvo: indice %d\n", indice_alvo);
// TODO 2: reabra NOME_ARQUIVO em modo "rb+" e valide contra NULL.
// TODO 3: calcule o offset = indice_alvo * sizeof(struct Registro)
// (use "long" para o tipo do offset) e chame
// fseek(arquivo, offset, SEEK_SET), validando o retorno (0 = sucesso).
// TODO 4: declare "struct Registro alvo;", leia com fread() um unico
// registro a partir da posicao atual do arquivo, valide que leu
// exatamente 1 item, e imprima id/nome/saldo.
// TODO 5: some 1000.00 ao campo saldo de "alvo", faca fseek() de
// volta para o MESMO offset, e grave "alvo" de volta com fwrite()
// (1 unico item). Feche o arquivo.
printf("====================================================\n");
printf(" ✅ Status: Operacional e Validado!\n");
printf("====================================================\n");
return EXIT_SUCCESS;
}
✅ Checkpoint: zero avisos do gcc, e a saída é:
🖥️ Saída esperada:
====================================================
⚡ Miniprojeto 18: Mini SGBD Binario com Acesso Direto
====================================================
Registros em memoria (48 bytes cada):
[0] id=1 nome=Ana Silva saldo=1500.00
[1] id=2 nome=Bruno Costa saldo=320.50
[2] id=3 nome=Carla Souza saldo=9800.75
[3] id=4 nome=Diego Alves saldo=150.00
[4] id=5 nome=Elisa Nunes saldo=4200.25
Registro alvo: indice 2
====================================================
✅ Status: Operacional e Validado!
====================================================
Repare no 48 bytes cada: int (4) + char[30] (30) + 6 bytes de padding + double (8). É esse tamanho fixo que vai permitir calcular onde cada registro começa no arquivo.
🎯 Objetivo: despejar structs inteiras em disco, em binário.
⌨️ Faça: FILE *arquivo = fopen(NOME_ARQUIVO, "wb");, valide contra NULL, grave tudo de uma vez com fwrite(registros, sizeof(struct Registro), TOTAL_REGISTROS, arquivo), confira se o retorno é 5 e feche com fclose. Imprima >> 5 registros gravados em 'banco.dat'.
✅ Checkpoint: banco.dat aparece na pasta com 240 bytes (5 × 48). Confira com ls -l banco.dat (Linux/macOS) ou (Get-Item banco.dat).Length (PowerShell). Você pode apagar a listagem Registros em memoria do esqueleto.
🧯 Se der errado: abrir o arquivo de 240 bytes num editor de texto mostra nomes legíveis no meio de símbolos estranhos: o arquivo é binário (os int e double estão na representação da memória), e isso é o esperado.
🎯 Objetivo: acesso O(1): calcular a posição em vez de ler os registros anteriores.
⌨️ Faça:
fopen(NOME_ARQUIVO, "rb+"), para leitura e escrita sem apagar o conteúdo.long offset = (long)indice_alvo * (long)sizeof(struct Registro); e fseek(arquivo, offset, SEEK_SET), conferindo se devolveu 0.struct Registro alvo;, fread(&alvo, sizeof(struct Registro), 1, arquivo), conferindo se leu 1. Imprima o registro e o offset.✅ Checkpoint: --- Registro lido diretamente no indice 2 (offset=96) --- e id=3 nome=Carla Souza saldo=9800.75.
🧯 Se der errado: se você abriu com "wb+" em vez de "rb+", o arquivo foi truncado para zero bytes e o fread devolve 0.
🎯 Objetivo: reescrever 48 bytes no meio do arquivo sem tocar no resto.
⌨️ Faça: alvo.saldo += 1000.00;. Depois, volte ao mesmo offset com fseek (o fread avançou o cursor 48 bytes), grave com fwrite(&alvo, sizeof(struct Registro), 1, arquivo) e feche o arquivo.
✅ Checkpoint: >> Registro 3 atualizado: novo saldo = 10800.75, e o arquivo continua com 240 bytes.
🧯 Se der errado: se o arquivo passou a ter o registro 4 corrompido (ou cresceu), faltou o fseek de volta: o fwrite gravou a partir do byte 144, em cima do registro seguinte.
🎯 Objetivo: validar a saída completa e o código de retorno.
✅ Checkpoint: a saída é esta, e o código de retorno é 0 (echo $? no Linux/macOS ou echo $LASTEXITCODE no PowerShell):
🖥️ Saída esperada:
====================================================
⚡ Miniprojeto 18: Mini SGBD Binario com Acesso Direto
====================================================
>> 5 registros gravados em 'banco.dat'
--- Registro lido diretamente no indice 2 (offset=96) ---
id=3 nome=Carla Souza saldo=9800.75
>> Registro 3 atualizado: novo saldo = 10800.75
>> Apenas esse registro foi regravado; os demais 4 ficaram intactos.
====================================================
✅ Status: Operacional e Validado!
====================================================
Só abra o gabarito depois de chegar até aqui:
src/main.c📄 src/main.c
/*
* Miniprojeto 18: Mini SGBD Binario com Acesso Direto (fseek O(1))
* Especializacao em Engenharia de Sistemas em C
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TOTAL_REGISTROS 5
#define NOME_ARQUIVO "banco.dat"
struct Registro {
int id;
char nome[30];
double saldo;
};
int main(void) {
printf("====================================================\n");
printf(" ⚡ Miniprojeto 18: Mini SGBD Binario com Acesso Direto\n");
printf("====================================================\n");
struct Registro registros[TOTAL_REGISTROS] = {
{1, "Ana Silva", 1500.00},
{2, "Bruno Costa", 320.50},
{3, "Carla Souza", 9800.75},
{4, "Diego Alves", 150.00},
{5, "Elisa Nunes", 4200.25}
};
/* --- Etapa 1: gravar todos os registros no arquivo binario --- */
FILE *arquivo = fopen(NOME_ARQUIVO, "wb");
if (arquivo == NULL) {
fprintf(stderr, "ERRO: nao foi possivel criar %s\n", NOME_ARQUIVO);
return EXIT_FAILURE;
}
size_t gravados = fwrite(registros, sizeof(struct Registro), TOTAL_REGISTROS, arquivo);
fclose(arquivo);
if (gravados != (size_t)TOTAL_REGISTROS) {
fprintf(stderr, "ERRO: gravacao incompleta (%zu de %d registros)\n",
gravados, TOTAL_REGISTROS);
return EXIT_FAILURE;
}
printf("\n>> %d registros gravados em '%s'\n", TOTAL_REGISTROS, NOME_ARQUIVO);
/* --- Etapa 2: reabrir e acessar diretamente o registro de indice K --- */
const int indice_alvo = 2;
arquivo = fopen(NOME_ARQUIVO, "rb+");
if (arquivo == NULL) {
fprintf(stderr, "ERRO: nao foi possivel reabrir %s\n", NOME_ARQUIVO);
return EXIT_FAILURE;
}
/* O offset e' calculado por MULTIPLICACAO, nao por leitura dos
* registros anteriores: e' isso que torna o acesso O(1) em vez de
* O(n). Todo registro ocupa exatamente sizeof(struct Registro)
* bytes, entao o registro "indice_alvo" comeca no byte
* indice_alvo * sizeof(struct Registro). */
long offset = (long)indice_alvo * (long)sizeof(struct Registro);
if (fseek(arquivo, offset, SEEK_SET) != 0) {
fprintf(stderr, "ERRO: fseek falhou.\n");
fclose(arquivo);
return EXIT_FAILURE;
}
struct Registro alvo;
size_t lidos = fread(&alvo, sizeof(struct Registro), 1, arquivo);
if (lidos != 1) {
fprintf(stderr, "ERRO: falha ao ler registro de indice %d\n", indice_alvo);
fclose(arquivo);
return EXIT_FAILURE;
}
printf("\n--- Registro lido diretamente no indice %d (offset=%ld) ---\n",
indice_alvo, offset);
printf("id=%d nome=%s saldo=%.2f\n", alvo.id, alvo.nome, alvo.saldo);
/* --- Etapa 3: atualizar o saldo e regravar somente esse registro --- */
alvo.saldo += 1000.00;
/* fread avancou o cursor do arquivo: precisamos voltar ao MESMO
* offset antes de regravar, senao escreveriamos logo apos o
* registro lido, corrompendo o proximo. */
if (fseek(arquivo, offset, SEEK_SET) != 0) {
fprintf(stderr, "ERRO: fseek de retorno falhou.\n");
fclose(arquivo);
return EXIT_FAILURE;
}
size_t gravado_unico = fwrite(&alvo, sizeof(struct Registro), 1, arquivo);
fclose(arquivo);
if (gravado_unico != 1) {
fprintf(stderr, "ERRO: falha ao regravar registro atualizado.\n");
return EXIT_FAILURE;
}
printf("\n>> Registro %d atualizado: novo saldo = %.2f\n", alvo.id, alvo.saldo);
printf(">> Apenas esse registro foi regravado; os demais %d ficaram intactos.\n",
TOTAL_REGISTROS - 1);
printf("====================================================\n");
printf(" ✅ Status: Operacional e Validado!\n");
printf("====================================================\n");
return EXIT_SUCCESS;
}
Por que isso importa: esse é exatamente o princípio por trás de índices de bancos de dados reais e de formatos como .wav/.bmp: registros de tamanho fixo permitem acesso aleatório O(1) a qualquer posição do arquivo, sem precisar carregar tudo em memória.
Escreva uma função void listar_todos(const char *nome_arquivo) que reabra banco.dat em modo "rb", use fseek(arquivo, 0, SEEK_END) seguido de ftell(arquivo) para descobrir o tamanho do arquivo em bytes, calcule quantos registros ele contém (tamanho / sizeof(struct Registro)), volte ao início com rewind(arquivo) e imprima todos os registros em um laço — confirmando visualmente que só o registro de índice 2 foi alterado.
✅ Checkpoint: listar_todos encontra 5 registros (240 / 48). Só o id=3 mostra 10800.75, e os outros quatro mantêm o saldo original.
Quando terminar, siga para o Mini-SGBD Binário com Alocação Dinâmica. Ele leva este mesmo motor um passo além: registros alocados com malloc/free, um Makefile e auditoria de vazamentos com Valgrind.
📋 Projetos do curso de C | 📚 Teoria do Capítulo 18 | 🗂️ Hub de projetos