data-structures · intermediate · ~35 min

Final Project: todo-list-cli — add/remove/list

Stable identifiers vs. positional indices; allocation discipline.

Challenge

Implement a fixed-capacity to-do list:

typedef struct todo todo_t;
todo_t *todo_create(int capacity);
int     todo_add(todo_t *t, const char *text);  /* returns id >= 1, or -1 if full */
int     todo_remove(todo_t *t, int id);         /* returns 1 if removed, 0 if not found */
int     todo_count(todo_t *t);
const char *todo_text(todo_t *t, int index);   /* by 0-based position */
void    todo_destroy(todo_t *t);

IDs are stable: adding an item gives it a fresh monotonic ID; removing does not renumber surviving IDs.

Why this matters

A CRUD-style to-do list with stable identifiers is the simplest realistic API design exercise. It teaches you the difference between an index (positional) and an ID (stable, never reused).

Input format

Capacity > 0; text is null-terminated.

Output format

See API.

Constraints

Use strdup for text; free on remove and destroy.

Starter code

typedef struct todo todo_t;
todo_t *todo_create(int capacity);
int     todo_add(todo_t *t, const char *text);
int     todo_remove(todo_t *t, int id);
int     todo_count(todo_t *t);
const char *todo_text(todo_t *t, int index);
void    todo_destroy(todo_t *t);

Common mistakes

Reusing IDs after delete (breaks every caller that stored the ID). Confusing index with ID. Forgetting to free the strdup'd text on remove and destroy.

Edge cases to handle

Add when full returns -1. Remove non-existent returns 0. Index out of range for todo_text returns NULL.

Complexity

O(n) per op (linear array). A future variant could use a balanced tree for O(log n).

Background lessons

Up next

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.