Study Projects
Project 1 — Dynamic String Builder
Every real program that assembles text needs a growable string. This builder owns a heap buffer, appends by doubling capacity, and always keeps a valid NUL terminator — three lessons from the strings and memory pages in one small type.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data; // heap buffer, always NUL-terminated
size_t len; // current length (excluding the NUL)
size_t cap; // allocated capacity (including room for the NUL)
} Str;
// build an empty builder
Str str_new(void) {
Str s = {0};
s.cap = 16;
s.data = malloc(s.cap); // data[0] will stay '\0' while empty
if (s.data) s.data[0] = '\0';
return s;
}
// append text, growing by doubling when needed
int str_append(Str *s, const char *text) {
size_t need = s->len + strlen(text) + 1; // +1 for the terminator
if (need > s->cap) {
while (s->cap < need) s->cap *= 2; // geometric growth: O(1)/append
char *grown = realloc(s->data, s->cap);
if (grown == NULL) return -1; // failure: old buffer intact
s->data = grown;
}
strcpy(s->data + s->len, text); // append at the current end
s->len += strlen(text);
return 0;
}
void str_free(Str *s) { free(s->data); s->data = NULL; s->len = s->cap = 0; }
int main(void) {
Str s = str_new();
if (!s.data) return 1;
str_append(&s, "Hello, ");
str_append(&s, "C study projects!");
printf("%s\n", s.data); // "Hello, C study projects!"
str_free(&s);
return 0;
}
Challenge
Add str_append_char and str_trim, then test the builder under -fsanitize=address with 10,000 appends. Sanitizers cannot lie: leaks and overflows will be reported the moment they happen.
Project 2 — Linked List with Deletion
The collections page built a list with push and print. Deletion is where the pointer discipline really bites: removing a node requires its predecessor, which you chase from the head. This version also deletes by value and frees everything on exit.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node Node;
struct Node { int value; Node *next; };
// remove the FIRST node whose value equals v; return the (possibly new) head
Node *list_remove(Node *head, int v) {
Node *cur = head, *prev = NULL;
while (cur != NULL && cur->value != v) { // walk until found or tail
prev = cur;
cur = cur->next;
}
if (cur == NULL) return head; // not found: nothing changes
if (prev == NULL) head = cur->next; // removing the head itself
else prev->next = cur->next; // unlink from the middle
free(cur); // release the node's memory
return head;
}
void list_free(Node *head) {
while (head) { Node *n = head->next; free(head); head = n; }
}
int main(void) {
Node *head = NULL;
for (int v = 1; v <= 5; v++) {
Node *n = malloc(sizeof(Node));
if (!n) { list_free(head); return 1; }
n->value = v; n->next = head; // push front
head = n;
}
head = list_remove(head, 3); // 5 4 3 2 1 -> 5 4 2 1
for (Node *c = head; c; c = c->next) printf("%d ", c->value);
printf("\n");
list_free(head);
return 0;
}
Challenge
Extend it to a doubly linked list (add a prev pointer) so deletion no longer needs a predecessor walk — and confirm deletion becomes O(1) with a node pointer in hand.
Project 3 — Word-Frequency Counter
The capstone: read a text file, count how often each word appears, and print the words sorted by frequency. It touches file I/O (input page), the hash table (collections page), and sorting with callbacks (algorithms page). Every piece is already familiar — assembled, they become a real tool.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUCKETS 256
typedef struct Entry Entry;
struct Entry { char word[64]; int count; Entry *next; };
static unsigned long hash_str(const char *s) { // djb2 hash (see collections)
unsigned long h = 5381;
while (*s) h = h * 33 + (unsigned char)*s++;
return h;
}
// find-or-create the bucket chain entry for a word
static Entry *entry_for(Entry *table[], const char *word) {
Entry **slot = &table[hash_str(word) % BUCKETS];
for (Entry *e = *slot; e; e = e->next)
if (strcmp(e->word, word) == 0) return e; // already counted
Entry *e = calloc(1, sizeof(Entry)); // new word: create
if (!e) return NULL;
snprintf(e->word, sizeof(e->word), "%s", word);
e->next = *slot; *slot = e; // chain it into the bucket
return e;
}
// comparator for qsort: most frequent first
static int cmp_count(const void *a, const void *b) {
const Entry *ea = *(const Entry *const *)a;
const Entry *eb = *(const Entry *const *)b;
return (eb->count > ea->count) - (eb->count < ea->count);
}
int main(void) {
Entry *table[BUCKETS] = {0};
FILE *f = fopen("text.txt", "r");
if (f == NULL) { perror("text.txt"); return 1; }
char word[64];
// fscanf with a width bound: reads at most 63 chars — overflow-proof
while (fscanf(f, "%63s", word) == 1) {
Entry *e = entry_for(table, word);
if (e) e->count++;
}
fclose(f);
// flatten the chains into one array for sorting
size_t n = 0, cap = 16;
Entry **all = malloc(cap * sizeof(*all));
for (size_t b = 0; b < BUCKETS; b++)
for (Entry *e = table[b]; e; e = e->next) {
if (n == cap) all = realloc(all, (cap *= 2) * sizeof(*all));
all[n++] = e;
}
qsort(all, n, sizeof(*all), cmp_count);
for (size_t i = 0; i < n && i < 10; i++)
printf("%4d %s\n", all[i]->count, all[i]->word);
free(all);
return 0;
}
Challenge
Make the word list stop ignoring punctuation (strip .,;:!? before counting) and add an --top N command-line argument instead of the hard-coded 10. Both features reuse only what this roadmap taught — that is the point.
redis, curl, or TheAlgorithms/C — and read a single file end to end, tracing every pointer and buffer. When you can explain the file to someone else, you have graduated from this track.