data-structures · intermediate · ~35 min
Stable identifiers vs. positional indices; allocation discipline.
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.
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).
Capacity > 0; text is null-terminated.
See API.
Use strdup for text; free on remove and destroy.
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);
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.
Add when full returns -1. Remove non-existent returns 0. Index out of range for todo_text returns NULL.
O(n) per op (linear array). A future variant could use a balanced tree for O(log n).
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.