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.

Você vai praticar
  • 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)
  1. A tarefa, a lista e o primeiro valgrind.
  2. O menu: adicionar e concluir.
  3. Remover e limpar as feitas.
  4. 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;
}
LinhaO 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, sete free. Seis são nossos: 3 tarefas × 2 blocos (nó e título). O sétimo é do próprio printf, 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), devolve NULL: a tarefa não existe. Com n igual a -1 (resposta inválida), o i == n nunca acontece, e também devolve NULL.
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 -1 se a resposta não serve.
lista = adicionar(lista, nova_tarefa(linha));
nova_tarefa copia o texto de linha para um bloco novo. Isso é essencial: linha é reaproveitada na próxima volta do menu, e se o nó guardasse o endereço de linha, 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 = true muda 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 liberar o valgrind acusaria 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 junto ant, um passo atrás. No fim, p é a tarefa a remover e ant a de antes (ou NULL, se p é 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->titulo já 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 em liberar, o endereço da próxima é guardado antes, porque depois do apagar(p) não dá mais para ler p->prox.
else { ant = p; }
ant só 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.txt nã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. O main libera 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 deDá para usarQuando vale a pena
malloc(strlen(titulo) + 1) e strcpystrdup(titulo), que faz as duas coisas C23Sempre 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 mallocchar 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 adicionarguardar também um ponteiro para a última tarefaListas 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á dentroQuando a função já devolve outra coisa (aula Ponteiro para Ponteiro).

Desafios extras

  1. 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.
  2. Mover para o topo. Uma opção que leva a tarefa n para o começo da lista, sem criar nem apagar nós: só trocando ponteiros.
  3. Desmarcar. Concluir uma tarefa já feita desfaz a marca (t->feita = !t->feita).
  4. Prioridade. Acrescente bool urgente e grave [!] no arquivo para as urgentes; liste as urgentes primeiro.
  5. Desfazer a última remoção. Em vez de apagar o nó removido, guarde-o numa variável lixeira e 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:

char *copia = malloc(strlen(titulo) + );

6. Complete a frase que o valgrind mostra quando nenhum bloco ficou sem free:

All heap blocks were -- no leaks are possible

Resumo

  • Cada tarefa é um nó criado com malloc, ligado ao próximo por prox; a última aponta para NULL.
  • Um título de tamanho exato é um segundo bloco: malloc(strlen(t) + 1). Dois malloc, dois free: primeiro o título, depois o nó.
  • Para achar a tarefa n, percorra contando; numa lista ligada não existe lista[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 prox antes do free.
  • 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=full e procure All heap blocks were freed e 0 errors.

Você chegou ao fim dos projetos. Para rever a lista inteira, volte a Sobre os Projetos; para consultar, siga para a Referência.