How Do Lab 8 Project and Generic Programming Fit Together?
Applying generic patterns to a real project — callbacks, nested allocation, and putting it all together
After this lesson, you will be able to:
- Describe the three-layer architecture (driver, generic container, type-specific callbacks)
- Implement
buildTypeWordto read, allocate, and return data asvoid * - Implement
cleanTypeWordto free nested allocations in the correct order - Allocate
strlen + 1bytes for strings and explain the buffer overflow without+1
Connecting the Pieces
You’ve learned function pointers, void pointers, and the generic container pattern. Lab 8 asks you to implement this pattern: write type-specific callbacks for a Word struct that plug into a provided generic array.
This lesson walks through the implementation strategy — what to write first, how to test, and where the tricky memory management lives.
The Lab 8 Architecture
Three Layers
┌─────────────────────────┐
│ main.c (driver) │ ← Reads file, calls generic functions
├─────────────────────────┤
│ genericArray.c │ ← Generic container (provided)
│ Uses void * + callbacks│
├─────────────────────────┤
│ words.c │ ← YOUR callbacks: build, print, compare, clean
│ Works with Word struct │
└─────────────────────────┘
The generic layer calls your functions through function pointers. You never modify the generic code — you just plug in callbacks.
The Word Struct
typedef struct
{
char *letters; // Dynamically allocated string
int length;
} Word;
Two heap allocations per word: one for the struct, one for the string.
The Five Callbacks
1. printTypeWord — easiest, start here:
void printTypeWord(void *data)
{
if (data == NULL) return;
Word *w = (Word *)data;
printf("%s", w->letters);
}
2. compareWords — string comparison:
int compareWords(void *a, void *b)
{
if (a == NULL || b == NULL) return 0;
Word *wa = (Word *)a;
Word *wb = (Word *)b;
return strcmp(wa->letters, wb->letters);
}
3. cleanTypeWord — free inner then outer:
void cleanTypeWord(void *data)
{
if (data == NULL) return;
Word *w = (Word *)data;
free(w->letters); // Free string first
free(w); // Then free struct
}
4. buildTypeWord — read from file, allocate everything:
void *buildTypeWord(FILE *fp)
{
char buffer[256];
if (fgets(buffer, sizeof(buffer), fp) == NULL)
{
return NULL;
}
buffer[strcspn(buffer, "\n")] = '\0';
Word *w = calloc(1, sizeof(Word));
if (w == NULL) return NULL;
w->length = strlen(buffer);
w->letters = calloc(w->length + 1, sizeof(char));
if (w->letters == NULL)
{
free(w);
return NULL;
}
strcpy(w->letters, buffer);
return (void *)w;
}
5. buildTypeWord_Prompt — same but reads from stdin with a prompt.
Implementation Strategy
The Trick: Implement functions in order of difficulty, test after each one:
printTypeWord— just cast and printfcompareWords— cast and strcmpcleanTypeWord— two frees in the right orderbuildTypeWord— the hardest: read, allocate struct, allocate string, copybuildTypeWord_Prompt— variation of build
Get each one compiling and tested before moving to the next. Run Valgrind after implementing clean to verify no leaks.
buildTypeWord, why do you allocate w->length + 1 bytes for w->letters instead of just w->length?'\0'. The word "hello" is 5 characters but needs 6 bytes: 'h','e','l','l','o','\0'. If you only allocate strlen bytes, strcpy writes the null terminator past the end of your allocation — a buffer overflow.
Memory for One Word
Heap:
┌──────────────┐
│ Word struct │
│ letters ──────────→ ┌─────────────┐
│ length = 5 │ │ "hello\0" │
└──────────────┘ └─────────────┘
(calloc #1) (calloc #2)
Two allocations → two frees. cleanTypeWord must free both.
Common Mistakes
| Mistake | Result |
|---|---|
| Free struct before string | Memory leak (lost string pointer) |
Forget +1 for null terminator |
Buffer overflow on strcpy |
| Don’t check calloc return | Crash on out-of-memory |
| Cast to wrong type in callback | Garbage data, silent corruption |
| Skip NULL check at start of callback | Crash on NULL input |
buildTypeWord, if calloc for w->letters returns NULL, what should you do before returning NULL?Word struct successfully. If the second calloc fails, you must free the first allocation before returning — otherwise it leaks. Option A leaks the struct. Option B frees a NULL pointer (harmless but pointless) and still leaks the struct. This pattern of cleaning up partial allocations on failure is critical in C.
Why does this matter?
Lab 8 is where all of Series 4 clicks together. The generic container doesn’t know your type — it just calls your callbacks. Getting those callbacks right (especially the memory management in build and clean) is the difference between a program that works and one that leaks memory or crashes. Valgrind is your best friend here.
Deep dive: Valgrind verification for nested allocations
With Word structs, each word creates two heap allocations. For a file with 5 words, Valgrind should show:
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: 11 allocs, 11 frees, ...
Why 11 allocs for 5 words? 5 struct callocs + 5 string callocs + 1 for the array itself = 11.
If you see a mismatch (e.g., 11 allocs, 6 frees), you’re leaking the strings. Check your cleanTypeWord:
| Valgrind says | Likely cause |
|---|---|
definitely lost: 40 bytes in 5 blocks |
Forgot free(w->letters) in clean — 5 leaked strings |
definitely lost: 80 bytes in 5 blocks |
Forgot to call clean at all — 5 leaked Word structs |
definitely lost: 120 bytes in 10 blocks |
Forgot both — 5 strings + 5 structs |
Invalid read of size 8 in clean |
Freed struct before string (use-after-free on w->letters) |
The golden rule: free(w->letters) then free(w). Inner first, outer second. Think of it like unpacking a box — you take items out of the box before recycling the box.
Quick Check: Why does each Word require two calloc calls?
One for the Word struct itself, one for the character array (letters). The struct contains a char * pointer — the actual string data lives in a separate heap allocation.
Quick Check: Why start with printTypeWord instead of buildTypeWord?
printTypeWord is the simplest — just cast and printf. Getting it working first lets you verify that the generic framework calls your function correctly. Then you can test more complex functions by seeing their output through the working print function.
Quick Check: What does the generic array code know about the Word struct?
Nothing. It stores void * pointers and calls your callbacks. The generic code never includes the Word header, never casts to Word *, and never accesses letters or length. All type-specific knowledge lives in your callback functions.
Series 4 Complete
You’ve gone from basic structs through file I/O, sorting, function pointers, void pointers, generic containers, and linked lists. These are the tools used to build real data structures — hash tables, trees, graphs — in later courses.
In the final series, Systems Programming, you’ll learn fork, exec, pipe, and signal — connecting everything back to Unix. The C programs you write will create other processes, just like the shell does when you type a command.
The CTF Challenges for Weeks 8–9 include struct, file I/O, and function pointer challenges that test the patterns you’ve just learned.
Big Picture: The generic container pattern — void pointers + function pointer callbacks — is how C libraries achieve type-independent behavior. It’s the same approach used in the Linux kernel, database engines, and language runtimes. Java generics and interfaces are a safer, more ergonomic version of the same idea. Understanding the C version means understanding what those higher-level abstractions actually do.