Projeto Lista de Tarefas
A Ana anota o que precisa fazer na lojinha em papeizinhos colados na parede: comprar papel, pagar a luz, ligar para o fornecedor. Neste projeto, os papeizinhos viram uma lista ligada em C. Cada tarefa é criada com malloc quando a Ana a digita e destruída com free quando sai da lista, e a lista cresce e encolhe sem tamanho máximo. No fim, o valgrind confere que nenhum byte foi esquecido.
- Montar uma lista ligada de structs com
->, inserindo no fim. - Reservar dois blocos por tarefa com malloc: o nó e o título, do tamanho exato (Strings Dinâmicas).
- Achar a tarefa pelo número, removê-la (inclusive a primeira) e remover várias numa passada só.
- Liberar tudo, na ordem certa, e ler os relatórios do valgrind.
- Guardar a lista num arquivo de texto e montá-la de novo ao abrir.
O que vamos construir
O programa pronto, rodando dentro do valgrind na máquina da Ana. Ele carrega as tarefas salvas, a Ana conclui uma, acrescenta outra, remove uma terceira e limpa as feitas. No fim, o relatório: toda a memória foi devolvida.
$ valgrind --leak-check=full ./tarefas ==452535== Memcheck, a memory error detector ==452535== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==452535== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info ==452535== Command: ./tarefas ==452535== === Lista de Tarefas === 1. [ ] Comprar papel A4 2. [ ] Ligar para o fornecedor 3. [ ] Arrumar a vitrine 4. [ ] Fazer o pedido de cadernos Pendentes: 4 de 4 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 3 Número da tarefa: 1 Feito: Comprar papel A4 Opção: 2 Tarefa: Pagar o aluguel Opção: 4 Número da tarefa: 2 Removida: Ligar para o fornecedor Opção: 5 Tarefas feitas removidas: 1 Opção: 1 1. [ ] Arrumar a vitrine 2. [ ] Fazer o pedido de cadernos 3. [ ] Pagar o aluguel Pendentes: 3 de 3 Opção: 0 Tarefas salvas em tarefas.txt. Até logo! ==452535== ==452535== HEAP SUMMARY: ==452535== in use at exit: 0 bytes in 0 blocks ==452535== total heap usage: 16 allocs, 16 frees, 11,406 bytes allocated ==452535== ==452535== All heap blocks were freed -- no leaks are possible ==452535== ==452535== For lists of detected and suppressed errors, rerun with: -s ==452535== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
- A tarefa, a lista e o primeiro valgrind.
- O menu: adicionar e concluir.
- Remover e limpar as feitas.
- Guardar a lista num arquivo.
Antes de começar
Este é o projeto mais avançado do guia. Você precisa ter visto Listas Ligadas (inserir, percorrer, remover e liberar), malloc e free, Structs e Ponteiros e Valgrind. Todos os programas foram compilados com -g, para o valgrind mostrar o arquivo e a linha de cada problema:
$ mkdir -p c/tarefas $ cd c/tarefas
Versão 1 · A tarefa, a lista e o primeiro valgrind
Imagine os papeizinhos da Ana presos por clipes, um atrás do outro: cada papel tem a tarefa e um clipe que segura o próximo. Para achar a terceira tarefa, você começa pelo primeiro papel e segue os clipes. Na lista ligada, o papel é uma struct, o clipe é um ponteiro (prox), e o último papel tem o clipe solto: NULL.
lista ──► ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ "Comprar papel A4" │ │ "Pagar a conta de…" │ │ "Ligar para o forn…" │
│ feita: false │ │ feita: true │ │ feita: false │
│ prox ────────────────┼──► │ prox ────────────────┼──► │ prox: NULL │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
A diferença para a lista de números de Listas Ligadas é o texto. Um título pode ter 5 letras ou 200. Em vez de reservar um array de 200 bytes em toda tarefa, o nó guarda um ponteiro para um texto criado com malloc, do tamanho exato. Assim, cada tarefa ocupa dois blocos de memória, e os dois têm de ser liberados.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
int main(void) {
Tarefa *lista = NULL; // a lista começa vazia
lista = adicionar(lista, nova_tarefa("Comprar papel A4"));
lista = adicionar(lista, nova_tarefa("Pagar a conta de luz"));
lista = adicionar(lista, nova_tarefa("Ligar para o fornecedor"));
lista->prox->feita = true; // a segunda já foi feita
listar(lista);
liberar(lista);
return 0;
}
| Linha | O que faz |
|---|---|
typedef struct Tarefa { … struct Tarefa *prox; } Tarefa; | A struct tem nome (struct Tarefa) porque precisa citar a si mesma no campo prox; o typedef deixa escrever só Tarefa no resto do programa (aula typedef). |
malloc(sizeof *t) | Reserva um nó. sizeof *t é "o tamanho daquilo para que t aponta": 24 bytes no gcc do laboratório e 12 no editor do guia, que tem ponteiros de 4 bytes. Escrito assim, o tamanho está sempre certo. |
malloc(strlen(titulo) + 1) | Reserva o texto: as letras e mais 1 byte para o '\0'. Esquecer o + 1 é o erro mais comum com strings dinâmicas (aula Strings Dinâmicas). |
if (t == NULL || copia == NULL) | O malloc devolve NULL quando falta memória. Aqui o programa desiste com exit(1) (aula exit): numa lista de tarefas, não há o que fazer sem memória. |
strcpy(copia, titulo); | Copia o texto para o bloco novo. É seguro: o bloco foi reservado com o tamanho do próprio texto. |
Tarefa *adicionar(Tarefa *lista, Tarefa *t) | Anda até a última tarefa (a que tem prox == NULL) e prende a nova nela. Devolve o começo da lista, que só muda quando a lista estava vazia; por isso a chamada é lista = adicionar(lista, …). |
void apagar(Tarefa *t) | Libera os dois blocos de uma tarefa: primeiro o título, depois o nó (veja os cenários). |
p->feita ? 'x' : ' ' | Um x entre os colchetes se a tarefa foi feita, um espaço se não (aula Operador Ternário). O %c mostra um caractere. |
Tarefa *prox = lista->prox; | Em liberar: guarda o endereço da próxima tarefa antes de apagar a atual. Depois do free, lista->prox não pode mais ser lido. |
lista->prox->feita = true; | Segue um clipe: a segunda tarefa. Só para o teste; a versão 2 terá um jeito de verdade. |
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas $ ./tarefas 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor Pendentes: 2 de 3
O programa funciona, mas a saída não diz nada sobre a memória. Quem diz é o valgrind:
$ valgrind --leak-check=full ./tarefas ==452466== Memcheck, a memory error detector ==452466== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==452466== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info ==452466== Command: ./tarefas ==452466== 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor Pendentes: 2 de 3 ==452466== ==452466== HEAP SUMMARY: ==452466== in use at exit: 0 bytes in 0 blocks ==452466== total heap usage: 7 allocs, 7 frees, 1,158 bytes allocated ==452466== ==452466== All heap blocks were freed -- no leaks are possible ==452466== ==452466== For lists of detected and suppressed errors, rerun with: -s ==452466== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
- in use at exit: 0 bytes in 0 blocks
- Na saída do programa, nenhum bloco ficou reservado.
- 7 allocs, 7 frees
- Sete
malloc, setefree. Seis são nossos: 3 tarefas × 2 blocos (nó e título). O sétimo é do próprioprintf, que reserva um buffer para a saída e o devolve sozinho no fim. - 1,158 bytes allocated
- 3 nós de 24 bytes (72), os 3 títulos (17 + 21 + 24 = 62 bytes, contando os
'\0') e os 1024 bytes do buffer do terminal. - All heap blocks were freed -- no leaks are possible
- "Todos os blocos foram liberados, não há vazamento possível." É a frase que você quer ver.
- ERROR SUMMARY: 0 errors
- Nenhum acesso errado à memória (leitura fora do bloco, uso depois do
free).
definitely lost: 62 bytes in 3 blocks
Na primeira versão do apagar, a Ana liberou só o nó: free(t);. O programa mostrou a lista igualzinha, sem erro nenhum. O valgrind não deixou passar:
$ valgrind --leak-check=full ./tarefas ==452478== Memcheck, a memory error detector ==452478== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==452478== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info ==452478== Command: ./tarefas ==452478== 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor Pendentes: 2 de 3 ==452478== ==452478== HEAP SUMMARY: ==452478== in use at exit: 62 bytes in 3 blocks ==452478== total heap usage: 7 allocs, 4 frees, 1,158 bytes allocated ==452478== ==452478== 17 bytes in 1 blocks are definitely lost in loss record 1 of 3 ==452478== at 0x4850858: malloc (vg_replace_malloc.c:447) ==452478== by 0x400125E: nova_tarefa (tarefas.c:14) ==452478== by 0x4001468: main (tarefas.c:70) ==452478== ==452478== 21 bytes in 1 blocks are definitely lost in loss record 2 of 3 ==452478== at 0x4850858: malloc (vg_replace_malloc.c:447) ==452478== by 0x400125E: nova_tarefa (tarefas.c:14) ==452478== by 0x400148D: main (tarefas.c:71) ==452478== ==452478== 24 bytes in 1 blocks are definitely lost in loss record 3 of 3 ==452478== at 0x4850858: malloc (vg_replace_malloc.c:447) ==452478== by 0x400125E: nova_tarefa (tarefas.c:14) ==452478== by 0x40014B2: main (tarefas.c:72) ==452478== ==452478== LEAK SUMMARY: ==452478== definitely lost: 62 bytes in 3 blocks ==452478== indirectly lost: 0 bytes in 0 blocks ==452478== possibly lost: 0 bytes in 0 blocks ==452478== still reachable: 0 bytes in 0 blocks ==452478== suppressed: 0 bytes in 0 blocks ==452478== ==452478== For lists of detected and suppressed errors, rerun with: -s ==452478== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
O que significa: 7 malloc e só 4 free. Os três blocos perdidos têm 17, 21 e 24 bytes: os tamanhos de "Comprar papel A4", "Pagar a conta de luz" e "Ligar para o fornecedor" mais o '\0'. Cada relatório diz onde o bloco nasceu: o malloc da linha 14 (o do título), chamado por nova_tarefa, chamada pelo main nas linhas 70, 71 e 72. "Definitely lost" (perdido com certeza) quer dizer que nenhum ponteiro aponta mais para eles: depois do free(t), o endereço do título sumiu junto com o nó.
Como consertar: para cada malloc, um free. Quem reservou dois blocos em nova_tarefa libera dois em apagar. Num programa que roda o dia todo, cada tarefa removida vazaria o seu título, e a memória usada só cresceria (aula Vazamentos de Memória).
warning: pointer ‘t’ used after ‘free’ e Invalid read of size 8
Depois, a Ana pôs os dois free, mas na ordem trocada: primeiro free(t);, depois free(t->titulo);. Desta vez o gcc percebeu:
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas
tarefas.c: In function ‘apagar’:
tarefas.c:41:11: warning: pointer ‘t’ used after ‘free’ [-Wuse-after-free]
41 | free(t->titulo);
| ~^~~~~~~~
tarefas.c:40:5: note: call to ‘free’ here
40 | free(t);
| ^~~~~~~
$ valgrind --leak-check=full ./tarefas
==452486== Memcheck, a memory error detector
==452486== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==452486== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==452486== Command: ./tarefas
==452486==
1. [ ] Comprar papel A4
2. [x] Pagar a conta de luz
3. [ ] Ligar para o fornecedor
Pendentes: 2 de 3
==452486== Invalid read of size 8
==452486== at 0x4001345: apagar (tarefas.c:41)
==452486== by 0x4001441: liberar (tarefas.c:64)
==452486== by 0x40014FB: main (tarefas.c:77)
==452486== Address 0x4a8e040 is 0 bytes inside a block of size 24 free'd
==452486== at 0x48538BF: free (vg_replace_malloc.c:990)
==452486== by 0x4001340: apagar (tarefas.c:40)
==452486== by 0x4001441: liberar (tarefas.c:64)
==452486== by 0x40014FB: main (tarefas.c:77)
==452486== Block was alloc'd at
==452486== at 0x4850858: malloc (vg_replace_malloc.c:447)
==452486== by 0x4001242: nova_tarefa (tarefas.c:13)
==452486== by 0x4001477: main (tarefas.c:71)
==452486==
==452486==
==452486== HEAP SUMMARY:
==452486== in use at exit: 0 bytes in 0 blocks
==452486== total heap usage: 7 allocs, 7 frees, 1,158 bytes allocated
==452486==
==452486== All heap blocks were freed -- no leaks are possible
==452486==
==452486== For lists of detected and suppressed errors, rerun with: -s
==452486== ERROR SUMMARY: 3 errors from 1 contexts (suppressed: 0 from 0)
O que significa: o aviso do gcc diz que t foi usado depois do free da linha 40. O valgrind confirma ao rodar: na linha 41, o programa leu 8 bytes (o ponteiro t->titulo) de dentro de "um bloco de 24 bytes já liberado" (o nó, liberado na linha 40). Não há vazamento, porque os 7 blocos foram liberados, mas houve 3 leituras proibidas, uma por tarefa. Funcionou por sorte: a memória liberada ainda guardava o valor antigo. O Clang do editor não dá esse aviso, e o editor também não reclama ao rodar (a memória do WebAssembly é organizada de outro jeito); por isso este exemplo é do laboratório.
Como consertar: libere de dentro para fora. Primeiro o que o nó aponta (free(t->titulo)), depois o nó (free(t)). Depois do free(t), não encoste mais em t.
Missão (versão 1): este adicionar é o mais curto possível: põe a tarefa nova na frente. Rode e veja a lista de trás para frente, com a tarefa errada marcada. Reescreva-o para pôr a tarefa no fim: se a lista estiver vazia, a nova tarefa é a lista; senão, ande até a tarefa com prox == NULL e prenda a nova nela.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
t->prox = lista; // assim a tarefa nova entra na FRENTE
return t;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
int main(void) {
Tarefa *lista = NULL; // a lista começa vazia
lista = adicionar(lista, nova_tarefa("Comprar papel A4"));
lista = adicionar(lista, nova_tarefa("Pagar a conta de luz"));
lista = adicionar(lista, nova_tarefa("Ligar para o fornecedor"));
lista->prox->feita = true; // a segunda já foi feita
listar(lista);
liberar(lista);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
int main(void) {
Tarefa *lista = NULL; // a lista começa vazia
lista = adicionar(lista, nova_tarefa("Comprar papel A4"));
lista = adicionar(lista, nova_tarefa("Pagar a conta de luz"));
lista = adicionar(lista, nova_tarefa("Ligar para o fornecedor"));
lista->prox->feita = true; // a segunda já foi feita
listar(lista);
liberar(lista);
return 0;
}
Versão 2 · O menu: adicionar e concluir
Agora a lista começa vazia e a Ana a monta pelo teclado. Para concluir uma tarefa, ela digita o número que aparece na listagem. Numa lista ligada não existe lista[2]: para chegar na tarefa 3, é preciso começar na primeira e seguir os clipes, contando. É o que faz a função achar. As funções de leitura são as mesmas da Calculadora e do Estoque.
As partes novas (as funções da versão 1 continuam iguais):
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
int main(void) {
Tarefa *lista = NULL;
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
liberar(lista);
printf("\nAté logo!\n");
return 0;
}
- Tarefa *achar(Tarefa *lista, int n)
- Percorre a lista contando 1, 2, 3… e devolve a tarefa de número
n. Se a lista acabar antes (p == NULL), devolveNULL: a tarefa não existe. Comnigual a-1(resposta inválida), oi == nnunca acontece, e também devolveNULL. - int ler_numero(const char *pergunta)
- Lê a linha e converte com
strtol, recusando texto, números com sobra (2a) e números menores que 1. Devolve-1se a resposta não serve. - lista = adicionar(lista, nova_tarefa(linha));
nova_tarefacopia o texto delinhapara um bloco novo. Isso é essencial:linhaé reaproveitada na próxima volta do menu, e se o nó guardasse o endereço delinha, todas as tarefas mostrariam o último texto digitado.- linha[0] != '\0'
- Uma linha vazia (só Enter) não vira tarefa.
- Tarefa *t = achar(…); if (t == NULL) …
- Com o ponteiro para a tarefa,
t->feita = truemuda a tarefa dentro da lista: não é uma cópia. - liberar(lista);
- Antes de sair, a lista inteira é devolvida. O sistema recuperaria a memória sozinho no fim do programa, mas sem o
liberarovalgrindacusaria vazamento, e o hábito certo é sempre liberar.
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas $ ./tarefas === Lista de Tarefas === Nenhuma tarefa. Aproveite o dia! 1) Listar 2) Adicionar 3) Concluir 0) Sair Opção: 1 Nenhuma tarefa. Aproveite o dia! Opção: 2 Tarefa: Comprar papel A4 Opção: 2 Tarefa: Pagar a conta de luz Opção: 2 Tarefa: Ligar para o fornecedor Opção: 3 Número da tarefa: 2 Feito: Pagar a conta de luz Opção: 3 Número da tarefa: 7 Essa tarefa não existe. Opção: 3 Número da tarefa: dois Essa tarefa não existe. Opção: 1 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor Pendentes: 2 de 3 Opção: 0 Até logo!
Missão (versão 2): a função achar sempre devolve NULL, e nenhuma tarefa pode ser concluída. Escreva-a: percorra a lista com um ponteiro p, contando a partir de 1, e devolva p quando a conta chegar em n. A entrada da missão adiciona duas tarefas, conclui a 2 e lista.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
// ande pela lista contando 1, 2, 3... e devolva a tarefa de número n
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir 0) Sair\n");
}
int main(void) {
Tarefa *lista = NULL;
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
liberar(lista);
printf("\nAté logo!\n");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir 0) Sair\n");
}
int main(void) {
Tarefa *lista = NULL;
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
liberar(lista);
printf("\nAté logo!\n");
return 0;
}
Versão 3 · Remover e limpar as feitas
Duas opções novas: 4) Remover tira uma tarefa pelo número, e 5) Limpar feitas tira todas as que já foram feitas, de uma vez. Remover de uma lista ligada é "soltar o clipe": a tarefa de antes passa a segurar a de depois, e a do meio fica livre para o free.
antes: [Comprar] ──► [Pagar] ──► [Ligar] ──► NULL
ant p
depois: [Comprar] ─────────────► [Ligar] ──► NULL ant->prox = p->prox;
[Pagar] apagar(p);
E se a tarefa removida for a primeira? Não existe "tarefa de antes" (ant é NULL): quem muda é o próprio começo da lista, que passa a ser a segunda tarefa. Por isso remover devolve o começo da lista, como adicionar.
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
if (ant == NULL) {
lista = p->prox; // era a primeira: a lista começa na segunda
} else {
ant->prox = p->prox;
}
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
// no while do main, duas opções novas:
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
- for (int i = 1; p != NULL && i < n; i++) { ant = p; p = p->prox; }
- Anda até a tarefa
n, levando juntoant, um passo atrás. No fim,pé a tarefa a remover eanta de antes (ouNULL, sepé a primeira). - if (p == NULL || n < 1)
- A lista acabou antes de chegar em
n, ou o número é inválido: não remove nada. - lista = p->prox;
- O caso da primeira tarefa: a lista passa a começar na segunda.
- printf("Removida: %s\n", p->titulo); apagar(p);
- O título é mostrado antes do
apagar; depois dele,p->titulojá não existe. - Tarefa *prox = p->prox; // guarde antes de apagar p
- Em
limpar_feitas, o laço passa por todas as tarefas e pode apagar várias. Como emliberar, o endereço da próxima é guardado antes, porque depois doapagar(p)não dá mais para lerp->prox. - else { ant = p; }
antsó anda quando a tarefa fica na lista. Se ela foi apagada, a "tarefa de antes" continua a mesma.
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas $ ./tarefas === Lista de Tarefas === Nenhuma tarefa. Aproveite o dia! 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Tarefa: Comprar papel A4 Opção: 2 Tarefa: Pagar a conta de luz Opção: 2 Tarefa: Ligar para o fornecedor Opção: 2 Tarefa: Arrumar a vitrine Opção: 3 Número da tarefa: 2 Feito: Pagar a conta de luz Opção: 3 Número da tarefa: 4 Feito: Arrumar a vitrine Opção: 1 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor 4. [x] Arrumar a vitrine Pendentes: 2 de 4 Opção: 5 Tarefas feitas removidas: 2 Opção: 1 1. [ ] Comprar papel A4 2. [ ] Ligar para o fornecedor Pendentes: 2 de 2 Opção: 4 Número da tarefa: 1 Removida: Comprar papel A4 Opção: 1 1. [ ] Ligar para o fornecedor Pendentes: 1 de 1 Opção: 4 Número da tarefa: 5 Essa tarefa não existe. Opção: 0 Até logo!
Repare que os números mudam: depois da limpeza, Ligar para o fornecedor deixou de ser a 3 e virou a 2. O número não é guardado na tarefa; é só a posição na lista na hora de listar. A mesma sessão (sem as listagens) dentro do valgrind, para conferir as remoções:
$ valgrind --leak-check=full ./tarefas … « a mesma sessão, cortada Opção: 5 Tarefas feitas removidas: 2 Opção: 4 Número da tarefa: 1 Removida: Comprar papel A4 Opção: 0 Até logo! ==452963== ==452963== HEAP SUMMARY: ==452963== in use at exit: 0 bytes in 0 blocks ==452963== total heap usage: 10 allocs, 10 frees, 2,224 bytes allocated ==452963== ==452963== All heap blocks were freed -- no leaks are possible ==452963== ==452963== For lists of detected and suppressed errors, rerun with: -s ==452963== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
10 malloc: 4 tarefas × 2 e mais 2 buffers da biblioteca (um para a saída e um para a entrada, que agora o programa lê). As 3 tarefas removidas pelo menu e a que sobrou, liberada no fim: 10 free.
Falha de segmentação ao remover a primeira tarefa
A primeira versão do remover tinha só a linha do caso geral, ant->prox = p->prox;. Com a tarefa 2 funcionava. Com a 1:
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas $ ./tarefas === Lista de Tarefas === Nenhuma tarefa. Aproveite o dia! 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Tarefa: Comprar papel A4 Opção: 2 Tarefa: Ligar para o fornecedor Opção: 4 Número da tarefa: 1 Falha de segmentação (imagem do núcleo gravada) ./tarefas $ echo $? 139
O que significa: para a primeira tarefa, o laço nem anda, e ant continua NULL. ant->prox tenta escrever no endereço zero, e o sistema mata o programa (aula Ponteiro Nulo). Pior: o programa morreu sem salvar nada. O editor do guia dá a mesma falha, com o código 139.
Como consertar: trate a primeira tarefa à parte: se ant == NULL, o começo da lista passa a ser p->prox. Em listas ligadas, teste sempre os três casos: o primeiro, um do meio e o último (e a lista vazia).
Invalid read of size 8 ao limpar as feitas
Na primeira versão do limpar_feitas, a Ana não guardou o prox antes de apagar e terminou o laço com p = p->prox;, como num percurso comum:
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
if (p->feita) {
if (ant == NULL) {
lista = p->prox;
} else {
ant->prox = p->prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = p->prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
O programa parecia funcionar: limpou a tarefa feita e listou as outras. O valgrind mostrou o problema:
$ valgrind --leak-check=full ./tarefas … « três tarefas, a 2 concluída Opção: 5 ==453156== Invalid read of size 8 ==453156== at 0x4001581: limpar_feitas (tarefas.c:95) ==453156== by 0x4001A1F: main (tarefas.c:181) ==453156== Address 0x4a8e990 is 16 bytes inside a block of size 24 free'd ==453156== at 0x48538BF: free (vg_replace_malloc.c:990) ==453156== by 0x400143B: apagar (tarefas.c:53) ==453156== by 0x400156E: limpar_feitas (tarefas.c:90) ==453156== by 0x4001A1F: main (tarefas.c:181) ==453156== Block was alloc'd at ==453156== at 0x4850858: malloc (vg_replace_malloc.c:447) ==453156== by 0x40012E2: nova_tarefa (tarefas.c:13) ==453156== by 0x40018F5: main (tarefas.c:168) ==453156== Tarefas feitas removidas: 1 Opção: 1 1. [ ] Comprar papel A4 2. [ ] Ligar para o fornecedor Pendentes: 2 de 2 Opção: 0 Até logo! ==453156== ==453156== HEAP SUMMARY: ==453156== in use at exit: 0 bytes in 0 blocks ==453156== total heap usage: 8 allocs, 8 frees, 2,182 bytes allocated ==453156== ==453156== All heap blocks were freed -- no leaks are possible ==453156== ==453156== For lists of detected and suppressed errors, rerun with: -s ==453156== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
O que significa: a linha 95 (p = p->prox;) leu 8 bytes "16 bytes para dentro de um bloco de 24 já liberado". Os 16 bytes são a posição do campo prox na struct (depois do titulo, 8 bytes, e do feita com o enchimento, mais 8), e o bloco é o nó da tarefa 2, liberado pelo free da linha 53 (em apagar), chamado pela linha 90. Deu certo por sorte, porque o free ainda não tinha reaproveitado aquela memória. Com outro malloc no meio, p poderia apontar para qualquer coisa. O editor do guia roda essa versão sem reclamar (o uso de memória liberada passa calado no WebAssembly); só o valgrind ou o -fsanitize=address (aula Sanitizers) mostram o erro.
Como consertar: guarde p->prox numa variável antes de apagar p, e avance com ela: Tarefa *prox = p->prox; … p = prox;.
Missão (versão 3): é o remover do cenário: clique em Rodar e veja a Falha de segmentação ao remover a tarefa 1. Conserte-o com um if (ant == NULL): nesse caso, a lista passa a começar em p->prox; senão, ant->prox = p->prox;.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
ant->prox = p->prox; // e se p for a primeira tarefa (ant == NULL)?
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir\n");
printf("4) Remover 5) Limpar feitas 0) Sair\n");
}
int main(void) {
Tarefa *lista = NULL;
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
liberar(lista);
printf("\nAté logo!\n");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
if (ant == NULL) {
lista = p->prox; // era a primeira: a lista começa na segunda
} else {
ant->prox = p->prox;
}
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir\n");
printf("4) Remover 5) Limpar feitas 0) Sair\n");
}
int main(void) {
Tarefa *lista = NULL;
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
liberar(lista);
printf("\nAté logo!\n");
return 0;
}
Versão 4 · Guardar a lista num arquivo
Até agora, a lista morria com o programa. A versão 4 salva as tarefas ao sair e as carrega ao abrir. No Estoque da Lojinha, o arquivo era binário e o fwrite copiava as structs inteiras. Aqui isso não funciona: o nó guarda endereços (o do título e o da próxima tarefa), que só valem nesta execução. Gravá-los seria gravar o "número da casa" de uma casa que vai ser demolida (o cenário "struct com ponteiro" de Arquivos Binários).
A saída é um arquivo de texto, uma tarefa por linha, com a marca na frente. Ele tem uma vantagem a mais: a Ana pode ler e até editar o arquivo com qualquer editor.
[ ] Comprar papel A4 [x] Pagar a conta de luz [ ] Ligar para o fornecedor
#define ARQUIVO "tarefas.txt"
// Lê tarefas.txt: uma tarefa por linha, "[x] título" ou "[ ] título".
Tarefa *carregar(void) {
FILE *f = fopen(ARQUIVO, "r");
if (f == NULL) {
return NULL; // primeira vez: lista vazia
}
Tarefa *lista = NULL;
char linha[300];
while (fgets(linha, sizeof linha, f) != NULL) {
linha[strcspn(linha, "\n")] = '\0';
if (strlen(linha) < 5 || linha[0] != '[' || linha[2] != ']') {
continue; // linha em branco ou estranha: pula
}
Tarefa *t = nova_tarefa(linha + 4);
t->feita = (linha[1] == 'x');
lista = adicionar(lista, t);
}
fclose(f);
return lista;
}
bool salvar(const Tarefa *lista) {
FILE *f = fopen(ARQUIVO, "w");
if (f == NULL) {
perror(ARQUIVO);
return false;
}
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
fprintf(f, "[%c] %s\n", p->feita ? 'x' : ' ', p->titulo);
}
return fclose(f) == 0;
}
int main(void) {
Tarefa *lista = carregar();
// ... o menu é igual ao da versão 3 ...
bool ok = salvar(lista);
liberar(lista);
if (!ok) {
return 1;
}
printf("\nTarefas salvas em %s. Até logo!\n", ARQUIVO);
return 0;
}
- if (f == NULL) return NULL;
- Na primeira vez,
tarefas.txtnão existe: a lista começa vazia, sem mensagem de erro. - while (fgets(linha, sizeof linha, f) != NULL)
- Lê uma linha por vez até o fim do arquivo (aula Ler Arquivos).
- strlen(linha) < 5 || linha[0] != '[' || linha[2] != ']'
- Uma linha válida tem pelo menos
[ ]e uma letra. Linhas em branco ou digitadas erradas são puladas, em vez de virarem tarefas estranhas. - nova_tarefa(linha + 4)
linha + 4é o endereço do 5º caractere: o título começa depois de[x](aula Ponteiros e Strings).- t->feita = (linha[1] == 'x');
- O caractere entre os colchetes diz se a tarefa foi feita.
- fopen(ARQUIVO, "w")
- Em
salvar, o modo"w"recria o arquivo do zero: a lista inteira é escrita de novo a cada saída (aula Escrever Arquivos). - return fclose(f) == 0;
- Um disco cheio pode aparecer só no
fclose. Omainlibera a lista mesmo se salvar falhou, e só então sai com o código 1.
Primeiro dia: a Ana cadastra três tarefas, conclui uma e sai. O arquivo aparece:
$ gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas $ ./tarefas === Lista de Tarefas === Nenhuma tarefa. Aproveite o dia! 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Tarefa: Comprar papel A4 Opção: 2 Tarefa: Pagar a conta de luz Opção: 2 Tarefa: Ligar para o fornecedor Opção: 3 Número da tarefa: 2 Feito: Pagar a conta de luz Opção: 0 Tarefas salvas em tarefas.txt. Até logo! $ cat tarefas.txt [ ] Comprar papel A4 [x] Pagar a conta de luz [ ] Ligar para o fornecedor
Segundo dia: a lista volta como estava, a Ana acrescenta uma tarefa e limpa a feita.
$ ./tarefas === Lista de Tarefas === 1. [ ] Comprar papel A4 2. [x] Pagar a conta de luz 3. [ ] Ligar para o fornecedor Pendentes: 2 de 3 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 2 Tarefa: Arrumar a vitrine Opção: 5 Tarefas feitas removidas: 1 Opção: 1 1. [ ] Comprar papel A4 2. [ ] Ligar para o fornecedor 3. [ ] Arrumar a vitrine Pendentes: 3 de 3 Opção: 0 Tarefas salvas em tarefas.txt. Até logo! $ cat tarefas.txt [ ] Comprar papel A4 [ ] Ligar para o fornecedor [ ] Arrumar a vitrine
Como o arquivo é texto, dá até para acrescentar uma tarefa sem abrir o programa. O >> do terminal escreve no fim do arquivo:
$ echo '[ ] Fazer o pedido de cadernos' >> tarefas.txt $ ./tarefas === Lista de Tarefas === 1. [ ] Comprar papel A4 2. [ ] Ligar para o fornecedor 3. [ ] Arrumar a vitrine 4. [ ] Fazer o pedido de cadernos Pendentes: 4 de 4 1) Listar 2) Adicionar 3) Concluir 4) Remover 5) Limpar feitas 0) Sair Opção: 0 Tarefas salvas em tarefas.txt. Até logo!
Missão (versão 4): a aba tarefas.txt tem três tarefas, e a segunda está feita ([x]). Mas o carregar esqueceu de ler a marca, e todas aparecem pendentes. No lugar do comentário, faça t->feita valer verdadeiro quando linha[1] for 'x'.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ARQUIVO "tarefas.txt"
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
if (ant == NULL) {
lista = p->prox; // era a primeira: a lista começa na segunda
} else {
ant->prox = p->prox;
}
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
// Lê tarefas.txt: uma tarefa por linha, "[x] título" ou "[ ] título".
Tarefa *carregar(void) {
FILE *f = fopen(ARQUIVO, "r");
if (f == NULL) {
return NULL; // primeira vez: lista vazia
}
Tarefa *lista = NULL;
char linha[300];
while (fgets(linha, sizeof linha, f) != NULL) {
linha[strcspn(linha, "\n")] = '\0';
if (strlen(linha) < 5 || linha[0] != '[' || linha[2] != ']') {
continue; // linha em branco ou estranha: pula
}
Tarefa *t = nova_tarefa(linha + 4);
// a tarefa está feita se linha[1] for 'x'
lista = adicionar(lista, t);
}
fclose(f);
return lista;
}
bool salvar(const Tarefa *lista) {
FILE *f = fopen(ARQUIVO, "w");
if (f == NULL) {
perror(ARQUIVO);
return false;
}
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
fprintf(f, "[%c] %s\n", p->feita ? 'x' : ' ', p->titulo);
}
return fclose(f) == 0;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir\n");
printf("4) Remover 5) Limpar feitas 0) Sair\n");
}
int main(void) {
Tarefa *lista = carregar();
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
bool ok = salvar(lista);
liberar(lista);
if (!ok) {
return 1;
}
printf("\nTarefas salvas em %s. Até logo!\n", ARQUIVO);
return 0;
}
[ ] Comprar papel A4 [x] Pagar a conta de luz [ ] Ligar para o fornecedor
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ARQUIVO "tarefas.txt"
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
if (ant == NULL) {
lista = p->prox; // era a primeira: a lista começa na segunda
} else {
ant->prox = p->prox;
}
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
// Lê tarefas.txt: uma tarefa por linha, "[x] título" ou "[ ] título".
Tarefa *carregar(void) {
FILE *f = fopen(ARQUIVO, "r");
if (f == NULL) {
return NULL; // primeira vez: lista vazia
}
Tarefa *lista = NULL;
char linha[300];
while (fgets(linha, sizeof linha, f) != NULL) {
linha[strcspn(linha, "\n")] = '\0';
if (strlen(linha) < 5 || linha[0] != '[' || linha[2] != ']') {
continue; // linha em branco ou estranha: pula
}
Tarefa *t = nova_tarefa(linha + 4);
t->feita = (linha[1] == 'x');
lista = adicionar(lista, t);
}
fclose(f);
return lista;
}
bool salvar(const Tarefa *lista) {
FILE *f = fopen(ARQUIVO, "w");
if (f == NULL) {
perror(ARQUIVO);
return false;
}
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
fprintf(f, "[%c] %s\n", p->feita ? 'x' : ' ', p->titulo);
}
return fclose(f) == 0;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir\n");
printf("4) Remover 5) Limpar feitas 0) Sair\n");
}
int main(void) {
Tarefa *lista = carregar();
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
bool ok = salvar(lista);
liberar(lista);
if (!ok) {
return 1;
}
printf("\nTarefas salvas em %s. Até logo!\n", ARQUIVO);
return 0;
}
O programa completo
A versão final, igual à versão 4. Compile com gcc -std=c23 -Wall -Wextra -g tarefas.c -o tarefas:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ARQUIVO "tarefas.txt"
typedef struct Tarefa {
char *titulo; // texto do tamanho exato, criado com malloc
bool feita;
struct Tarefa *prox; // a próxima tarefa (NULL na última)
} Tarefa;
// Cria uma tarefa nova, fora da lista. Encerra o programa se faltar memória.
Tarefa *nova_tarefa(const char *titulo) {
Tarefa *t = malloc(sizeof *t);
char *copia = malloc(strlen(titulo) + 1);
if (t == NULL || copia == NULL) {
fprintf(stderr, "Sem memória\n");
exit(1);
}
strcpy(copia, titulo);
t->titulo = copia;
t->feita = false;
t->prox = NULL;
return t;
}
// Põe a tarefa no fim da lista e devolve o começo da lista.
Tarefa *adicionar(Tarefa *lista, Tarefa *t) {
if (lista == NULL) {
return t;
}
Tarefa *p = lista;
while (p->prox != NULL) {
p = p->prox;
}
p->prox = t;
return lista;
}
// Devolve a tarefa de número n (1, 2, 3...) ou NULL se ela não existe.
Tarefa *achar(Tarefa *lista, int n) {
int i = 1;
for (Tarefa *p = lista; p != NULL; p = p->prox) {
if (i == n) {
return p;
}
i++;
}
return NULL;
}
void apagar(Tarefa *t) {
free(t->titulo); // primeiro o texto...
free(t); // ...depois a tarefa
}
// Tira da lista a tarefa de número n e devolve o começo da lista.
Tarefa *remover(Tarefa *lista, int n) {
Tarefa *ant = NULL;
Tarefa *p = lista;
for (int i = 1; p != NULL && i < n; i++) {
ant = p;
p = p->prox;
}
if (p == NULL || n < 1) {
printf("Essa tarefa não existe.\n");
return lista;
}
if (ant == NULL) {
lista = p->prox; // era a primeira: a lista começa na segunda
} else {
ant->prox = p->prox;
}
printf("Removida: %s\n", p->titulo);
apagar(p);
return lista;
}
// Tira da lista todas as tarefas feitas e devolve o começo da lista.
Tarefa *limpar_feitas(Tarefa *lista) {
Tarefa *ant = NULL;
Tarefa *p = lista;
int removidas = 0;
while (p != NULL) {
Tarefa *prox = p->prox; // guarde antes de apagar p
if (p->feita) {
if (ant == NULL) {
lista = prox;
} else {
ant->prox = prox;
}
apagar(p);
removidas++;
} else {
ant = p;
}
p = prox;
}
printf("Tarefas feitas removidas: %d\n", removidas);
return lista;
}
void listar(const Tarefa *lista) {
if (lista == NULL) {
printf("Nenhuma tarefa. Aproveite o dia!\n");
return;
}
int n = 0;
int pendentes = 0;
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
n++;
printf("%d. [%c] %s\n", n, p->feita ? 'x' : ' ', p->titulo);
if (!p->feita) {
pendentes++;
}
}
printf("Pendentes: %d de %d\n", pendentes, n);
}
void liberar(Tarefa *lista) {
while (lista != NULL) {
Tarefa *prox = lista->prox;
apagar(lista);
lista = prox;
}
}
// 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 de 1 a 100000. Devolve -1 se não for um número assim.
int ler_numero(const char *pergunta) {
char linha[100];
if (!ler_linha(pergunta, linha, sizeof linha)) {
return -1;
}
char *fim = NULL;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || n < 1 || n > 100000) {
return -1;
}
return (int)n;
}
// Lê tarefas.txt: uma tarefa por linha, "[x] título" ou "[ ] título".
Tarefa *carregar(void) {
FILE *f = fopen(ARQUIVO, "r");
if (f == NULL) {
return NULL; // primeira vez: lista vazia
}
Tarefa *lista = NULL;
char linha[300];
while (fgets(linha, sizeof linha, f) != NULL) {
linha[strcspn(linha, "\n")] = '\0';
if (strlen(linha) < 5 || linha[0] != '[' || linha[2] != ']') {
continue; // linha em branco ou estranha: pula
}
Tarefa *t = nova_tarefa(linha + 4);
t->feita = (linha[1] == 'x');
lista = adicionar(lista, t);
}
fclose(f);
return lista;
}
bool salvar(const Tarefa *lista) {
FILE *f = fopen(ARQUIVO, "w");
if (f == NULL) {
perror(ARQUIVO);
return false;
}
for (const Tarefa *p = lista; p != NULL; p = p->prox) {
fprintf(f, "[%c] %s\n", p->feita ? 'x' : ' ', p->titulo);
}
return fclose(f) == 0;
}
void mostrar_menu(void) {
printf("1) Listar 2) Adicionar 3) Concluir\n");
printf("4) Remover 5) Limpar feitas 0) Sair\n");
}
int main(void) {
Tarefa *lista = carregar();
printf("=== Lista de Tarefas ===\n");
listar(lista);
mostrar_menu();
char linha[300];
while (ler_linha("\nOpção: ", linha, sizeof linha)) {
if (strcmp(linha, "1") == 0) {
listar(lista);
} else if (strcmp(linha, "2") == 0) {
if (ler_linha("Tarefa: ", linha, sizeof linha) && linha[0] != '\0') {
lista = adicionar(lista, nova_tarefa(linha));
}
} else if (strcmp(linha, "3") == 0) {
Tarefa *t = achar(lista, ler_numero("Número da tarefa: "));
if (t == NULL) {
printf("Essa tarefa não existe.\n");
} else {
t->feita = true;
printf("Feito: %s\n", t->titulo);
}
} else if (strcmp(linha, "4") == 0) {
lista = remover(lista, ler_numero("Número da tarefa: "));
} else if (strcmp(linha, "5") == 0) {
lista = limpar_feitas(lista);
} else if (strcmp(linha, "0") == 0) {
break;
} else {
printf("Opção desconhecida: %s\n", linha);
mostrar_menu();
}
}
bool ok = salvar(lista);
liberar(lista);
if (!ok) {
return 1;
}
printf("\nTarefas salvas em %s. Até logo!\n", ARQUIVO);
return 0;
}
O que o projeto juntou: listas ligadas, structs e ponteiros, typedef, malloc e free, strings dinâmicas, fgets, strtol, leitura e escrita de arquivos de texto e o valgrind.
Rodando no laboratório
A sessão do começo da página é a versão final dentro do valgrind: 16 malloc e 16 free. Dez blocos são das tarefas: as 4 carregadas do arquivo e a nova, com 2 blocos cada uma. Os outros 6 são da biblioteca: os buffers da tela e do teclado e, para cada arquivo aberto (o tarefas.txt lido no começo e o gravado no fim), o FILE e o seu buffer. O fclose devolve esses dois; sem ele, o valgrind acusaria o vazamento. Depois da sessão, o arquivo ficou assim:
$ cat tarefas.txt [ ] Arrumar a vitrine [ ] Fazer o pedido de cadernos [ ] Pagar o aluguel
Formas diferentes
| Em vez de | Dá para usar | Quando vale a pena |
|---|---|---|
malloc(strlen(titulo) + 1) e strcpy | strdup(titulo), que faz as duas coisas C23 | Sempre que o compilador for C23 (no C17 e antes, o strdup era só do POSIX). O resultado também é liberado com free. |
char *titulo com malloc | char titulo[100] dentro do nó | Um bloco só por tarefa e um free só, mas todo título ocupa 100 bytes e nenhum pode passar de 99 letras. |
andar até o fim em cada adicionar | guardar também um ponteiro para a última tarefa | Listas com milhares de itens (veja Listas Ligadas). Com dezenas de tarefas, andar até o fim é instantâneo. |
devolver o começo (lista = remover(lista, n)) | receber Tarefa **lista e mudar o começo lá dentro | Quando a função já devolve outra coisa (aula Ponteiro para Ponteiro). |
Desafios extras
- Editar o título. Uma opção que troca o texto de uma tarefa. Cuidado com a memória: libere o título antigo e reserve um novo, do tamanho certo. Confira com o
valgrind. - Mover para o topo. Uma opção que leva a tarefa
npara o começo da lista, sem criar nem apagar nós: só trocando ponteiros. - Desmarcar. Concluir uma tarefa já feita desfaz a marca (
t->feita = !t->feita). - Prioridade. Acrescente
bool urgentee grave[!]no arquivo para as urgentes; liste as urgentes primeiro. - Desfazer a última remoção. Em vez de apagar o nó removido, guarde-o numa variável
lixeirae ofereça "desfazer"; apague-o de verdade só quando outra remoção acontecer ou no fim do programa.
Exercícios
1. Cada tarefa da lista é criada com dois malloc (o nó e o título). Quantos free são precisos para apagar uma tarefa?
2. Num laço que apaga nós, por que se guarda Tarefa *prox = p->prox; antes do apagar(p)?
3. Ao remover a primeira tarefa, o que muda?
4. Por que a lista de tarefas é salva em texto, e não com fwrite das structs?
5. Complete o tamanho do bloco para guardar uma cópia do texto titulo:
6. Complete a frase que o valgrind mostra quando nenhum bloco ficou sem free:
Resumo
- Cada tarefa é um nó criado com
malloc, ligado ao próximo porprox; a última aponta paraNULL. - Um título de tamanho exato é um segundo bloco:
malloc(strlen(t) + 1). Doismalloc, doisfree: primeiro o título, depois o nó. - Para achar a tarefa
n, percorra contando; numa lista ligada não existelista[n]. - Remover é ligar a anterior à seguinte; a primeira tarefa é um caso à parte, porque muda o começo da lista.
- Num laço que apaga, guarde o
proxantes dofree. - Ponteiros não vão para arquivo: salve o conteúdo (aqui, em texto) e monte a lista de novo ao abrir.
- O programa "funcionar" não prova nada sobre a memória: rode o
valgrind --leak-check=fulle procureAll heap blocks were freede0 errors.
Você chegou ao fim dos projetos. Para rever a lista inteira, volte a Sobre os Projetos; para consultar, siga para a Referência.