C Strings: Null Terminators, strlen, strcpy vs strncpy, Buffer Safety
What This Concept Is
A "C string" is not a type. It is a contract about a char array:
- a region of
charmemory - containing some sequence of non-zero bytes
- followed by exactly one
\0byte (the null terminator)
Standard library functions in <string.h> assume this contract. strlen(s) walks s until it finds a \0 and returns the number of bytes before it. strcpy(dst, src) copies bytes from src to dst until and including the \0. Neither function checks the size of the destination. If dst is too small, you have just written past the end of a buffer, which is undefined behavior and the root cause of a generation of security vulnerabilities.
The bounded cousins, strncpy, strlcpy, snprintf, take a size limit. Use them.
Why It Matters Here
Most C beginners write their first string bug in week one:
char name[8];
strcpy(name, "Kernighan"); /* writes 10 bytes into 8; corrupts memory */
The compiler accepts this. The runtime on your laptop may look fine for a while. Then something nearby on the stack is silently overwritten, or the program crashes in an unrelated function. The bug is a buffer overrun, and it is C's most common one.
Later modules build on strings for tokenizing, I/O, paths, and protocol parsing. The habit you want now: always know both the buffer size and the intended string length.
Concrete Example
Safe string copy with explicit size:
#include <stdio.h>
#include <string.h>
int safe_copy(char *dst, size_t dst_size, const char *src) {
if (dst_size == 0) return -1;
size_t n = strlen(src);
if (n + 1 > dst_size) return -1; /* does not fit, including the '\0' */
memcpy(dst, src, n + 1); /* copy bytes plus the terminator */
return 0;
}
int main(void) {
char buf[16];
if (safe_copy(buf, sizeof buf, "hello, world") == 0)
puts(buf);
else
puts("buffer too small");
return 0;
}
Why not strncpy? Because strncpy(dst, src, n) does not guarantee a terminator when strlen(src) >= n. It pads the rest with \0 only if there is room. Many codebases now use strlcpy (BSD) or roll their own like the above.
strlen("hello") returns 5. sizeof("hello") is 6 - the terminator counts.
Common Confusion / Misconception
"strncpy is the safe version of strcpy." Partially. It limits writes but may leave the destination un-terminated. That is worse than it sounds: the next strlen or printf("%s", ...) will walk off into whatever is next in memory.
"char s[] = "hello"; s[5] is out of range." s[5] is the '\0'. Valid to read; writing a non-'\0' there breaks the contract for subsequent string functions.
"I know my strings, I do not need bounds." In C, any time you call strcpy, strcat, sprintf, or gets on user-supplied data without size-tracking, you have a potential buffer overrun. Treat all input as untrusted.
How To Use It
- Track buffer size (capacity) and length (bytes used) separately.
- Prefer
snprintf(buf, sizeof buf, "...", ...)oversprintf. It truncates safely and always terminates. - Prefer
fgets(buf, sizeof buf, stdin)overgets(getswas removed in C11 for this reason). - Use
strnlen(s, max)instead ofstrlen(s)whensmight not be terminated. - If you see
strcpy,strcat, orsprintfwith attacker-controllable input, fix it.
Check Yourself
- If
char s[10], what is the maximumstrlen(s)can return for a valid C string stored there? - Why is
strncpy(dst, src, n)not a drop-in safe replacement forstrcpy? - What is wrong with
char *s = malloc(strlen(input)); strcpy(s, input);?
Mini Drill or Application
Without looking them up, write:
size_t my_strlen(const char *s);- no standard-library calls.int my_strcpy_safe(char *dst, size_t dst_size, const char *src);- returns 0 on success, -1 on overflow.int my_strcat_safe(char *dst, size_t dst_size, const char *src);- same contract.- Compile all three with
gcc -Wall -Wextra -std=c11and test with strings of lengths 0, 1, and exactlydst_size - 1.