-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
41: Practice Exercise: Building a Simple String Utility Library
By this point in the course, you've probably realized that C treats strings with a level of trust that would be terrifying in any other language. We don't have a string object; we have pointers to bytes and a prayer that there's a null terminator somewhere before we hit a protected memory page. When you start building a utility library to handle common tasks—like trimming whitespace or splitting strings—you'll find yourself at a crossroads regarding memory ownership.
The Trap of Constant Reallocation
When I first started writing string utilities, my instinct was to make every function "safe" by returning a brand new string. If I wanted to trim the whitespace off the ends of a string, I'd calculate the new length, call malloc, copy the trimmed portion over, and return that new pointer. It feels intuitive because it preserves the original input, which seems like the "correct" functional approach.
// The "Naive" way: Always allocate new memory char* naive_trim(const char* str) { while (isspace((unsigned char)*str)) str++; if (*str == 0) return strdup(""); char* end = (char*)(str + strlen(str) - 1); while (end > str && isspace((unsigned char)*end)) end--; size_t len = (end - str) + 1; char* trimmed = malloc(len + 1); memcpy(trimmed, str, len); trimmed[len] = '\0'; return trimmed; }The problem here isn't that the code is "wrong"—it works. The problem is the hidden cost. If you're processing a CSV file with a million rows and trimming every field, you're hitting the heap millions of times. You're also forcing the caller to remember to
free()every single result. In a large project, this is how you end up with a memory leak that only shows up after the program has been running for six hours. I've spent far too many weekends chasing leaks caused by "convenience" functions like this.The Elegance (and Danger) of In-Place Trimming
The professional approach in C is usually to modify the string in place or return a pointer into the existing buffer. Since we're already holding the memory, why move it? We can just shift the start pointer forward and drop a null terminator at the end of the string. It's an O(1) operation in terms of extra memory.
// The "Better" way: Modify in-place char* better_trim(char* str) { if (!str) return NULL; // Trim leading space while (isspace((unsigned char)*str)) str++; if (*str == 0) return str; // Trim trailing space char* end = str + strlen(str) - 1; while (end > str && isspace((unsigned char)*end)) end--; // Write new null terminator end[1] = '\0'; return str; }This is orders of magnitude faster, but it introduces a new contract that you have to communicate to anyone using your library. Notice that
better_trimreturns a pointer that might be shifted forward. If the user passed in a pointer they got frommalloc, they can no longer simply callfree(returned_pointer)because that pointer is no longer the start of the allocated block. This will crash your program immediately.This is the fundamental trade-off in C library design: do you prioritize "ease of use" (new allocations) or "performance and control" (in-place modification)? In a real-world utility library, I almost always choose the latter. I'd rather the user be responsible for keeping track of the original base pointer than force a performance penalty on every single string operation. If you really need to keep the original string, it's the caller's job to
strdupit before passing it to the trim function.
📋 Practical Task
Exercise: Implementing a Robust String-to-Integer Array Splitter
Your task is to add a str_split_int function to your utility library. This function should take a string of numbers separated by commas (e.g., "10, 20, 30, 40") and convert them into a dynamically allocated array of integers.
Requirements:
- The function must return a pointer to an array of integers.
- Since C arrays don't know their own length, you must also return the total count of integers found (use a pointer argument for this, e.g.,
int *out_count). - The function should be robust: it must ignore any extra whitespace around the commas.
- You must use
strtolinstead ofatoito ensure you can handle potential conversion errors. - The memory for the integer array should be allocated exactly once you know the count, or resized efficiently.
Example usage:
int count = 0;
int* nums = str_split_int(" 1, 22, 333 ", &count);
// nums should be [1, 22, 333], count should be 3
free(nums);There are no comments for now.