Skip to Content
Course content

41: Practice Exercise: Building a Simple String Utility Library

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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_trim returns a pointer that might be shifted forward. If the user passed in a pointer they got from malloc, they can no longer simply call free(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 strdup it 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 strtol instead of atoi to 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);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.