C
Completed
-
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
39: String Manipulation Techniques
Alright, let's get our hands dirty. I've been working on a small system that takes user-submitted tags—you know, like " C Programming ", " Software Engineering ", or " Coding"—and the problem is that users are inconsistent with their spacing and capitalization. If I try to compare "C Programming" with " c programming ", the computer tells me they're different, which is technically true but practically useless for a search feature.
I want to build a "normalization" function. The goal: trim the leading and trailing whitespace and make everything lowercase. Let's see if we can figure this out together.
The "Read-Only" Trap
My first instinct is to just pass a string to a function and start chopping it up. Let's try a quick experiment.
char *tag = " C Programming ";
tag[0] = 'X'; // Let's see if this works
If I run that, I get a segmentation fault. Why? Because " C Programming " is a string literal stored in the read-only section of the binary. I can't modify it in place. I need a buffer. Let's move the input into a character array so we actually have permission to move bytes around.
char tag[] = " C Programming ";
// Now we can actually mutate this.
Hunting for the Real Start
First, I need to get rid of those leading spaces. I could try to create a new string, but that's a waste of memory. I'll just find where the actual text starts and shift everything over to the left.
I'll use a pointer to scan for the first non-space character:
char *start = tag;
while (*start == ' ') {
start++;
}
// 'start' now points to the 'C'
Okay, that's easy. But now my string effectively starts in the middle of the array. To fix this, I'll use memmove. I prefer memmove over memcpy here because the source and destination regions overlap—we're shifting the string within its own memory block. memcpy with overlapping memory is undefined behavior, and that's a rabbit hole we don't want to go down today.
size_t len = strlen(tag);
size_t leading_spaces = start - tag;
memmove(tag, start, len - leading_spaces + 1);
// The +1 ensures we grab that null terminator '\0'
Cutting Off the Tail
Now the front is clean, but we still have those trailing spaces at the end. This is actually easier. I don't need to move any memory; I just need to find the last non-space character and plant a null terminator right after it.
I'll start from the end of the string and walk backward:
int end = strlen(tag) - 1;
while (end >= 0 && tag[end] == ' ') {
tag[end] = '\0';
end--;
}
Wait, I noticed something. If I put the '\0' inside the loop, I'm overwriting every single trailing space with a null terminator. It works, but it's redundant. I only need to do it once at the very end of the search. Let's refine that.
int end = strlen(tag) - 1;
while (end >= 0 && tag[end] == ' ') {
end--;
}
tag[end + 1] = '\0';
Normalizing the Case
Finally, we need to handle the capitalization. I could write my own logic to check if a character is between 'A' and 'Z' and add 32 to it, but ctype.h already provides tolower(). It's cleaner and handles locale edge cases better than I ever would.
I'll just loop through the now-trimmed string and convert each character:
for (int i = 0; tag[i]; i++) {
tag[i] = tolower((unsigned char)tag[i]);
}
One small detail: I cast tag[i] to (unsigned char). This is a habit I've picked up over the years. tolower expects an int that can be represented as an unsigned char or EOF. If you pass a signed char with a negative value (which happens with some extended ASCII characters), you can trigger undefined behavior. It's a tiny detail, but it's the difference between code that works on your machine and code that crashes in production.
So, we've gone from a messy, read-only literal to a cleaned-up, lowercase, in-place modified buffer. We used pointers for scanning, memmove for shifting, and ctype.h for normalization. This is the bread and butter of string manipulation in C: managing the pointers and being obsessive about the null terminator.
📋 Practical Task
Exercise: URL Slug Generator
In web development, a "slug" is a URL-friendly version of a string. For example, "Hello World! This is C" should become "hello-world-this-is-c".
Write a function void make_slug(char *str) that transforms a string in-place according to these rules:
- Convert all uppercase letters to lowercase.
- Replace any sequence of one or more spaces or punctuation marks (non-alphanumeric characters) with a single hyphen
'-'. - Remove any leading or trailing hyphens.
- Ensure the resulting string is properly null-terminated.
Test Case:
Input: " C Programming: The Complete Guide! "
Expected Output: "c-programming-the-complete-guide"
There are no comments for now.