Chegamos ao fim. Cinquenta e uma aulas atrás, você escreveu int main pela primeira vez, e prometemos que ao final desta jornada você não teria apenas "estudado C" — teria construído algo real em C. Este último artigo cumpre essa promessa. Não vamos aprender um conceito novo; vamos integrar tudo que aprendemos num projeto completo, do zero, do tipo que um programador C profissional escreve. E depois, olharemos para trás — para o caminho inteiro que percorremos — e reconheceremos o quanto você cresceu. Respire fundo. Este é o momento de colher.
O projeto: um gerenciador de tarefas com persistência
Nosso capstone será um gerenciador de tarefas de linha de comando — um programa que permite adicionar, listar, marcar como concluídas e remover tarefas, salvando tudo em disco para que persista entre execuções. É um projeto modesto em escopo, mas que exige, para ser feito bem, quase todas as habilidades do curso: estruturas de dados dinâmicas (para guardar as tarefas), alocação de memória (para criá-las e destruí-las), arquivos (para persistir), modularização (para organizar), tratamento de erros (para robustez), argumentos de linha de comando (para a interface), e código limpo (para que seja mantível). Vamos construí-lo por partes, e você verá as peças da jornada se encaixando.
A estrutura do projeto
Aplicando a organização da aula Organização de Projetos e Convenções da Comunidade, estruturamos o projeto em módulos:
gerenciador-tarefas/
├── src/
│ ├── main.c # interface de linha de comando
│ ├── tarefa.c # a lógica das tarefas e da lista
│ └── persistencia.c # salvar e carregar do disco
├── include/
│ ├── tarefa.h
│ └── persistencia.h
├── Makefile
└── README.md
Cada módulo tem uma responsabilidade única: tarefa cuida da estrutura de dados e das operações; persistencia cuida do disco; main cuida da interface. Essa separação — interface, lógica, persistência — é um padrão comum em software real, e reflete tudo que aprendemos sobre modularização e código limpo.
O modelo de dados: struct e lista encadeada
Começamos definindo o que é uma tarefa e como as guardamos. Uma tarefa é uma struct (aula Structs: Criando Seus Próprios Tipos); a coleção delas é uma lista encadeada (aula Listas Encadeadas: A Estrutura que Só Existe com Ponteiros). No cabeçalho tarefa.h, a interface pública:
// include/tarefa.h
#ifndef TAREFA_H
#define TAREFA_H
#include <stdbool.h>
#define MAX_DESCRICAO 200
typedef struct Tarefa {
int id;
char descricao[MAX_DESCRICAO];
bool concluida;
struct Tarefa *proxima; // lista encadeada
} Tarefa;
// operações da lista de tarefas
Tarefa *tarefa_adicionar(Tarefa *lista, const char *descricao);
Tarefa *tarefa_remover(Tarefa *lista, int id);
bool tarefa_concluir(Tarefa *lista, int id);
void tarefa_listar(const Tarefa *lista);
void tarefa_liberar(Tarefa *lista);
int tarefa_contar(const Tarefa *lista);
#endif // TAREFA_H
Reconheça aqui a soma do curso: a struct autorreferente (struct Tarefa *proxima) da aula typedef e o Design de Tipos Legíveis, a guarda de inclusão de O Pré-processador: #define, Macros e Inclusões, o prefixo de módulo (tarefa_) de Organização de Projetos e Convenções da Comunidade, o bool de <stdbool.h> de Quando o Programa Decide: if, else e switch, e a interface limpa que esconde a implementação. Em uma única declaração de tipo e um punhado de assinaturas, meia dúzia de aulas se manifestam.
Implementando as operações
No tarefa.c, damos vida às operações, cada uma aplicando técnicas que dominamos:
// src/tarefa.c (trechos essenciais)
#include "tarefa.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// adiciona uma tarefa ao FIM da lista
Tarefa *tarefa_adicionar(Tarefa *lista, const char *descricao) {
Tarefa *nova = malloc(sizeof(Tarefa));
if (nova == NULL) { // sempre verificar malloc (aula Alocação Dinâmica: malloc, calloc, realloc e free)
return lista; // falha: lista inalterada
}
// gera o próximo id (maior id existente + 1)
int max_id = 0;
for (const Tarefa *t = lista; t != NULL; t = t->proxima) {
if (t->id > max_id) max_id = t->id;
}
nova->id = max_id + 1;
strncpy(nova->descricao, descricao, MAX_DESCRICAO - 1); // cópia segura (aula Strings: Vetores de Caracteres e a Biblioteca string.h)
nova->descricao[MAX_DESCRICAO - 1] = '\0'; // garante terminador
nova->concluida = false;
nova->proxima = NULL;
if (lista == NULL) {
return nova; // lista estava vazia
}
Tarefa *ultima = lista;
while (ultima->proxima != NULL) ultima = ultima->proxima; // caminha até o fim
ultima->proxima = nova;
return lista;
}
bool tarefa_concluir(Tarefa *lista, int id) {
for (Tarefa *t = lista; t != NULL; t = t->proxima) {
if (t->id == id) {
t->concluida = true;
return true; // encontrou e concluiu
}
}
return false; // id não encontrado
}
void tarefa_listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa.\n");
return;
}
for (const Tarefa *t = lista; t != NULL; t = t->proxima) {
printf("[%d] [%s] %s\n",
t->id,
t->concluida ? "x" : " ", // marca de concluída
t->descricao);
}
}
// libera TODA a lista (disciplina de memória, aula Listas Encadeadas: A Estrutura que Só Existe com Ponteiros)
void tarefa_liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *proxima = lista->proxima; // guarda antes de liberar (evita use-after-free)
free(lista);
lista = proxima;
}
}
Cada função é um pequeno resumo do curso. tarefa_adicionar combina malloc com verificação (aula Alocação Dinâmica: malloc, calloc, realloc e free), cópia segura de string com strncpy e terminador garantido (aula Strings: Vetores de Caracteres e a Biblioteca string.h), e inserção no fim de uma lista encadeada (aula Listas Encadeadas: A Estrutura que Só Existe com Ponteiros). tarefa_liberar aplica a disciplina de liberar nó por nó, guardando o próximo antes do free para evitar o use-after-free (aula Vazamentos de Memória e Como Caçá-los). O uso de const nos parâmetros de leitura (aula Ponteiros para Struct e o Operador Seta), os nomes que revelam intenção (aula Código Limpo em C: Nomes, Funções e Legibilidade) — tudo está aqui, não como teoria, mas em serviço de um programa que funciona.
A persistência: salvando em disco
Para as tarefas sobreviverem entre execuções, o módulo persistencia as grava em arquivo e as lê de volta, aplicando a Fase 4:
// src/persistencia.c (trecho)
#include "persistencia.h"
#include "tarefa.h"
#include <stdio.h>
#define ARQUIVO "tarefas.txt"
// salva a lista num arquivo de texto
bool persistencia_salvar(const Tarefa *lista) {
FILE *f = fopen(ARQUIVO, "w"); // modo escrita (aula Lendo e Gravando Arquivos de Texto)
if (f == NULL) {
perror("Erro ao salvar"); // tratamento de erro (aula Tratamento de Erros Idiomático: errno e Códigos de Retorno)
return false;
}
for (const Tarefa *t = lista; t != NULL; t = t->proxima) {
// formato: id|concluida|descricao
fprintf(f, "%d|%d|%s\n", t->id, t->concluida ? 1 : 0, t->descricao);
}
fclose(f); // sempre fechar (par com fopen)
return true;
}
Aqui a Fase 4 inteira comparece: fopen com verificação, fprintf para escrever num formato estruturado, fclose para garantir a gravação, e perror para relatar falhas (aula Tratamento de Erros Idiomático: errno e Códigos de Retorno). O carregamento (que omito por brevidade, mas você implementará nos exercícios) faria o inverso: abrir em modo leitura, ler linha a linha com fgets, separar os campos com strtok (aula Manipulação de Strings sem Sofrimento), e reconstruir a lista. O programa, assim, lembra — persiste através do tempo, que era a promessa da aula Lendo e Gravando Arquivos de Texto.
A interface: juntando tudo no main
Finalmente, o main amarra os módulos, interpretando os argumentos de linha de comando (aula Ponteiro para Ponteiro e Argumentos de Linha de Comando) para oferecer os comandos ao usuário:
// src/main.c (esqueleto)
#include "tarefa.h"
#include "persistencia.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
Tarefa *lista = persistencia_carregar(); // carrega o que estava salvo
if (argc < 2) {
printf("Uso: %s [listar|adicionar|concluir|remover] ...\n", argv[0]);
tarefa_liberar(lista);
return 1;
}
if (strcmp(argv[1], "listar") == 0) {
tarefa_listar(lista);
} else if (strcmp(argv[1], "adicionar") == 0 && argc >= 3) {
lista = tarefa_adicionar(lista, argv[2]);
persistencia_salvar(lista);
printf("Tarefa adicionada.\n");
} else if (strcmp(argv[1], "concluir") == 0 && argc >= 3) {
int id = atoi(argv[2]);
if (tarefa_concluir(lista, id)) {
persistencia_salvar(lista);
printf("Tarefa %d concluida.\n", id);
} else {
printf("Tarefa %d nao encontrada.\n", id);
}
} else if (strcmp(argv[1], "remover") == 0 && argc >= 3) {
lista = tarefa_remover(lista, atoi(argv[2]));
persistencia_salvar(lista);
printf("Tarefa removida.\n");
} else {
printf("Comando invalido.\n");
}
tarefa_liberar(lista); // libera tudo antes de sair (sem vazamentos)
return 0;
}
Este main é o maestro: carrega o estado, interpreta o comando do usuário (via argc/argv da aula Ponteiro para Ponteiro e Argumentos de Linha de Comando e strcmp de Strings: Vetores de Caracteres e a Biblioteca string.h), chama a operação apropriada, persiste as mudanças, e — crucialmente — libera toda a memória antes de sair (aula Vazamentos de Memória e Como Caçá-los), para que o programa passe limpo por um teste do Valgrind (aula Caçando Bugs de Memória com Valgrind e Sanitizers). Compilado com um Makefile (aula Automação de Build com Make) usando -Wall -Wextra (nossa recomendação constante), com as tarefas persistindo em disco, você tem um programa real, útil, que você poderia usar de verdade. Isto é C aplicado.
Usando o programa
$ make
$ ./tarefas adicionar "Revisar o artigo 52"
Tarefa adicionada.
$ ./tarefas adicionar "Celebrar o fim do curso"
Tarefa adicionada.
$ ./tarefas listar
[1] [ ] Revisar o artigo 52
[2] [ ] Celebrar o fim do curso
$ ./tarefas concluir 1
Tarefa 1 concluida.
$ ./tarefas listar
[1] [x] Revisar o artigo 52
[2] [ ] Celebrar o fim do curso
Feche o programa, abra de novo dias depois: as tarefas continuam lá, porque foram salvas em disco. Um programa que lembra, organizado em módulos, seguro na memória, robusto nos erros. Você o construiu com o que aprendeu.
A retrospectiva: o caminho que percorremos
Agora, o momento de olhar para trás. Lembra do mapa que tracei em O Mapa da Jornada, das nove fases? Percorremos todas.
Você começou nos fundamentos, aprendendo por que o C existe e como guardar dados, tomar decisões e repetir tarefas. Atravessou a fase mais transformadora, a dos ponteiros e da memória — aquele momento em que endereços, malloc, pilha e heap deixaram de ser mistério e você entendeu o que realmente acontece quando um programa roda. Aprendeu a criar seus próprios tipos com structs, enums e unions, e a organizar código em módulos. Deu voz e memória aos programas com entrada, saída e arquivos. Construiu, do zero, as estruturas de dados que outras linguagens dão prontas — listas, pilhas, filas, árvores, tabelas hash — e entendeu como funcionam por dentro. Adotou as ferramentas profissionais — GDB, Valgrind, Make, sanitizers, testes — que separam o amador do profissional. Elevou a qualidade do seu código com clareza, disciplina defensiva e organização. E desceu ao nível do sistema operacional na programação de sistemas — processos, threads, sincronização, redes, sinais —, tocando o metal onde o C reina.
Cinquenta e duas aulas. Você não é mais quem começou.
O que você é capaz de fazer agora
Reconheça a extensão do que você conquistou. Você consegue ler código C de projetos reais e entendê-lo. Consegue escrever programas que gerenciam sua própria memória com disciplina, sem vazamentos. Consegue implementar estruturas de dados e algoritmos do zero. Consegue depurar problemas complexos com ferramentas, não com adivinhação. Consegue estruturar um projeto de forma profissional. Consegue conversar com o sistema operacional. E, talvez o mais importante, você desenvolveu a mentalidade do programador C — a consciência da memória, o respeito pela responsabilidade que a linguagem confia a você, o hábito da verificação e da disciplina. Essa mentalidade tornará você melhor em qualquer linguagem que aprender depois, porque você entende o que está por baixo.
Para onde ir a partir daqui
Dominar C não é um fim, mas uma base poderosa. A partir daqui, muitos caminhos se abrem, e todos ficaram mais fáceis. Você pode aprofundar a programação de sistemas e o desenvolvimento de baixo nível — sistemas operacionais, drivers, embarcados. Pode partir para o C++, que estende o C com abstrações poderosas (e, se seguirmos, será uma jornada natural a partir daqui). Pode explorar o Rust, cujas garantias de memória você agora entende profundamente, justamente por conhecer os perigos que ele previne. Pode contribuir para os grandes projetos de código aberto em C que estudamos — o kernel Linux, o SQLite, o Redis — que agora não são mais intimidantes, mas legíveis. Ou pode simplesmente continuar construindo suas próprias ferramentas, cada vez mais ambiciosas. Seja qual for o caminho, a base que você construiu neste ano o sustentará.
Uma palavra final
Quando começamos, eu disse que aprender C é aprender a ser cuidadoso, disciplinado e consciente de cada byte — e que essa disciplina seria o verdadeiro presente da linguagem. Espero que agora você sinta esse presente nas mãos. C não é uma linguagem fácil; ela confia em você e não perdoa descuidos. Mas é justamente por isso que dominá-la significa tanto. Você aprendeu não apenas uma sintaxe, mas uma forma de pensar sobre computação em seu nível mais fundamental.
Foi uma honra ser seu professor ao longo destas 52 aulas. A jornada de um ano que começou com um único int main termina aqui — mas o programador que você se tornou está apenas começando. Continue escrevendo código. Continue curioso. E lembre-se: agora você entende o que a máquina realmente faz.
Parabéns. Você dominou C.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — releia-o agora; você o entenderá de forma completamente nova
- Modern C, Jens Gustedt — para consolidar e atualizar seu conhecimento (C17/C23) — https://gustedt.gitlabpages.inria.fr/modern-c/
- The Linux Programming Interface, Michael Kerrisk — o próximo passo em programação de sistemas
- Projetos de código aberto (SQLite, Redis, cURL, kernel Linux) — leia código real, agora acessível a você
- Expert C Programming, Peter van der Linden — para aprofundar nas sutilezas da linguagem
Exercícios
Exercício 1
Implemente a função tarefa_remover (que omitimos), que remove da lista a tarefa com o id dado, tratando corretamente os casos de remoção do primeiro nó e de um nó do meio, e liberando a memória do nó removido. Teste sob Valgrind para garantir que não há vazamentos.
Ver resposta
✓ Resposta:
Tarefa *tarefa_remover(Tarefa *lista, int id) {
Tarefa *atual = lista;
Tarefa *anterior = NULL;
while (atual != NULL) {
if (atual->id == id) {
if (anterior == NULL) {
lista = atual->proxima; // removendo o primeiro nó: a cabeça muda
} else {
anterior->proxima = atual->proxima; // religa por cima do removido
}
free(atual); // libera o nó removido
return lista;
}
anterior = atual;
atual = atual->proxima;
}
return lista; // id não encontrado: lista inalterada
}
Rastreamos o nó anterior para poder religar a lista ao remover. Se o alvo é a cabeça (anterior == NULL), a cabeça avança; senão, o anterior passa a apontar para o próximo do removido. Em ambos os casos, free(atual) libera o nó. Sob Valgrind, não deve haver vazamentos, pois o nó removido é liberado e o resto da lista é liberado no tarefa_liberar do main.
Exercício 2
Implemente a função persistencia_carregar, que lê o arquivo tarefas.txt, reconstrói a lista de tarefas (usando fgets e strtok para separar os campos id|concluida|descricao), e a retorna. Trate o caso de o arquivo ainda não existir (primeira execução).
Ver resposta
✓ Resposta:
Tarefa *persistencia_carregar(void) {
FILE *f = fopen(ARQUIVO, "r");
if (f == NULL) {
return NULL; // arquivo ainda não existe (primeira execução): lista vazia
}
Tarefa *lista = NULL;
char linha[256];
while (fgets(linha, sizeof(linha), f) != NULL) {
// formato: id|concluida|descricao
char *id_str = strtok(linha, "|");
char *conc_str = strtok(NULL, "|");
char *desc = strtok(NULL, "\n"); // até a quebra de linha
if (id_str && conc_str && desc) {
lista = tarefa_adicionar(lista, desc);
// ajusta id e concluida do nó recém-adicionado (o último da lista)
Tarefa *ultima = lista;
while (ultima->proxima != NULL) ultima = ultima->proxima;
ultima->id = atoi(id_str);
ultima->concluida = (atoi(conc_str) == 1);
}
}
fclose(f);
return lista;
}
Abrimos em modo leitura; se o arquivo não existe (NULL), retornamos uma lista vazia — o caso normal da primeira execução, tratado graciosamente (não é erro, aula Programação Defensiva e Contratos). Para cada linha, strtok separa os três campos, e reconstruímos a tarefa. (Uma implementação mais elegante teria uma função que cria a tarefa com id e status explícitos, evitando o ajuste posterior; deixei assim para reusar tarefa_adicionar.)
Exercício 3
Adicione ao programa um comando contar que use tarefa_contar para exibir quantas tarefas existem no total, quantas estão concluídas e quantas estão pendentes.
Ver resposta
✓ Resposta:
// no main.c, adicionar um novo ramo:
} else if (strcmp(argv[1], "contar") == 0) {
int total = tarefa_contar(lista);
int concluidas = 0;
for (const Tarefa *t = lista; t != NULL; t = t->proxima) {
if (t->concluida) concluidas++;
}
printf("Total: %d | Concluidas: %d | Pendentes: %d\n",
total, concluidas, total - concluidas);
}
Usa tarefa_contar para o total e percorre a lista contando as concluídas; as pendentes são a diferença. Um comando simples que reúne percurso de lista e a lógica de estado das tarefas.
Exercício 4
Escreva um Makefile completo para o projeto (com regras para os três módulos, variáveis CC e CFLAGS incluindo -Wall -Wextra -g, e um alvo clean) e um README.md documentando como compilar e usar o programa.
Ver resposta
✓ Resposta:
# Makefile
CC = gcc
CFLAGS = -Wall -Wextra -g -Iinclude
tarefas: src/main.c src/tarefa.c src/persistencia.c
$(CC) $(CFLAGS) src/main.c src/tarefa.c src/persistencia.c -o tarefas
.PHONY: clean
clean:
rm -f tarefas tarefas.txt
# Gerenciador de Tarefas
Um gerenciador de tarefas de linha de comando em C, com persistência em disco.
## Compilação
make
## Uso
./tarefas adicionar "Descrição da tarefa"
./tarefas listar
./tarefas concluir <id>
./tarefas remover <id>
./tarefas contar
As tarefas são salvas em `tarefas.txt` e persistem entre execuções.
## Requisitos
- GCC ou Clang
- Make
O Makefile usa variáveis, inclui os avisos e a informação de depuração recomendados, adiciona -Iinclude para encontrar os cabeçalhos, e tem um clean. O README documenta compilação e uso — um projeto apresentável (aula Organização de Projetos e Convenções da Comunidade).
Exercício 5
Reflita e escreva: olhando as 52 aulas, qual foi o conceito mais difícil para você, e qual você achou mais poderoso? Como o projeto capstone reuniu o que você aprendeu? Que tipo de programa você gostaria de construir a seguir com C?
Ver resposta
✓ Resposta: (Resposta reflexiva, pessoal — não há gabarito único.) O propósito deste exercício é você consolidar a jornada olhando para trás com consciência. Um bom exercício de reflexão reconheceria, por exemplo: que ponteiros e gerenciamento de memória (Fase 2) costumam ser o conceito mais difícil — o momento em que a forma de pensar sobre o computador muda —, e que essa mesma dificuldade os torna também os mais poderosos, pois destravam tudo o que vem depois (estruturas de dados, programação de sistemas). Sobre como o capstone reuniu o aprendizado: ele mostrou que os conceitos não vivem isolados — uma única função como tarefa_adicionar combinou malloc, verificação de erro, cópia segura de string e inserção em lista encadeada; o programa inteiro integrou structs, listas, arquivos, argumentos de linha de comando, modularização e disciplina de memória, provando que "dominar C" não é saber cada peça, mas saber combiná-las para construir algo real. Quanto ao próximo programa a construir — pode ser qualquer coisa que o motive: um interpretador simples, um servidor web básico (aplicando os sockets da aula Comunicação em Rede com Sockets), um jogo de terminal, uma ferramenta de linha de comando útil, uma biblioteca de estruturas de dados reutilizável, ou uma contribuição a um projeto de código aberto. O importante é continuar construindo — porque é construindo que o conhecimento adquirido neste ano se transforma em maestria duradoura.
Fim da série. Parabéns por chegar até aqui.