Common string pitfalls
Recognise and avoid the most frequent C string bugs — missing null terminators, buffer overflows, comparing with ==, and off-by-one errors.
- Identify the missing null terminator bug and explain why it causes crashes
- Explain why == cannot be used to compare C strings
- Describe a buffer overflow and how it arises in string operations
- Apply off-by-one thinking to allocate and fill string buffers correctly
C string bugs are legendary. They are the root cause of countless security vulnerabilities, including buffer overflows that have allowed attackers to take over systems since the 1980s. Understanding these pitfalls is not just academic — it is essential for writing any C that works reliably.
Pitfall 1: Comparing with ==
char name[] = "Alice";
if (name == "Alice") { /* WRONG */
printf("Hello Alice\n");
}This compares the address of the name array with the address of the string literal "Alice" in read-only memory. They are different addresses, so the condition is always false (or at best unreliably true due to compiler optimisation). Use strcmp:
if (strcmp(name, "Alice") == 0) { /* CORRECT */
printf("Hello Alice\n");
}Pitfall 2: Missing null terminator
char str[5] = {'H', 'e', 'l', 'l', 'o'}; /* no room for '\0' */
printf("%s\n", str); /* prints "Hello" then keeps going until it hits a 0 byte somewhere */If you fill an array with characters but forget the null terminator, printf, strlen, strcpy, and every other string function will read past the end of your array — a classic undefined behaviour. Always allocate length + 1 bytes and ensure the last byte is '\0'.
char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; /* CORRECT */Pitfall 3: Buffer overflow
char buf[5];
strcpy(buf, "Hello, World!"); /* 13 characters + null -- overflows buf */strcpy does not know that buf is only 5 bytes. It writes all 14 bytes, overwriting whatever memory comes after buf. This can corrupt other variables, return addresses, or anything else on the stack.
Buffer overflows are security vulnerabilities. Stack-based buffer overflows can overwrite the function return address, redirecting execution to attacker-controlled code. This is the basis of classic exploits. The defense: always use strncpy, snprintf, and strncat; always know the size of your destination buffer; never trust user-supplied input to be within bounds.
Safe version:
char buf[5];
strncpy(buf, "Hello, World!", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; /* ensure null-termination */
/* buf is "Hell" -- truncated, but safe */Pitfall 4: Off-by-one errors
char buf[5];
char *src = "Hi";
/* Copy 5 characters -- but "Hi\0" is only 3 bytes needed */
/* Problem: if src were "Hello", this writes 6 bytes into a 5-byte buffer */
for (int i = 0; i < 5; i++) {
buf[i] = src[i]; /* runs past '\0' */
}Off-by-one errors in string handling typically involve:
- Allocating
strlen(s)instead ofstrlen(s) + 1(forgetting the null). - Iterating with
i <= leninstead ofi < len. - Using
sizeof(buf)in astrncpywithout subtracting 1 for the null.
Developing the habit of explicitly accounting for the null terminator in every buffer size calculation prevents most of these.
Pitfall 5: Modifying a string literal
char *str = "Hello";
str[0] = 'J'; /* UNDEFINED BEHAVIOUR -- string literal is read-only */String literals are typically stored in a read-only memory segment. On most platforms, this crashes with a segmentation fault. Always use a char array (not a pointer) when you need a mutable string:
char str[] = "Hello"; /* writable copy on the stack */
str[0] = 'J'; /* OK: modifies the copy */Pitfall 6: Dangling pointer to a local string
char *get_message(void) {
char msg[] = "Hello from inside";
return msg; /* returns a pointer to a local array -- WRONG */
}When get_message returns, msg is destroyed (the stack frame is gone). The returned pointer is a dangling pointer — it points to memory that may now hold anything. Use a static array, a global, or dynamically allocated memory to return a string from a function:
/* Option 1: static (but not thread-safe) */
const char *get_message(void) {
static char msg[] = "Hello from inside";
return msg;
}
/* Option 2: string literal (read-only, but valid) */
const char *get_message(void) {
return "Hello from inside"; /* lives forever in read-only memory */
}
/* Option 3: caller-provided buffer (preferred for library functions) */
void get_message(char *buf, size_t size) {
snprintf(buf, size, "Hello from inside");
}Where to go next
Next: the palindrome checker lab — you will put arrays and string manipulation together to check whether a word reads the same forwards and backwards.
String functions from <string.h>
Use the standard C string library — strcpy, strcat, strcmp, strncpy, strstr, and others — to copy, compare, and search strings safely.
Lab: Palindrome checker
Write a C program that checks whether a string is a palindrome — practising array iteration, string manipulation, and careful null-terminator handling.