Study Projects

Three small projects pull the whole track together. Type each one yourself — do not copy-paste — then break it on purpose and repair it. These are the kinds of programs that teach more than any reading: a string builder, a linked list with deletion, and a word-frequency counter that combines files, hashing, and sorting.

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.

Next challenge: pick one of the repositories on the references page — 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.