C / STRINGS AND BUFFER SAFETY
C strings and the NUL terminator contract
Explain what makes a char array a real C string, find its terminating zero byte, and keep capacity, length and the terminator straight.
What you will learn
- Explain why a char array must have capacity of at least its text length plus one
- Tell apart '\0' (byte 0), the digit '0' (byte 48) and the null pointer NULL
- Spot arrays like char c[3] = "abc" that hold characters but are not strings
- Terminate buffers you fill yourself, and use memchr to check bytes you did not write
Understanding C strings and the NUL terminator contract
C has no string type. What the standard calls a string is a run of characters ending at the first byte whose value is zero; arrays, pointers and malloc blocks are just storage that may or may not contain such a byte. The consequence is that a string carries no header and no length field, so strlen cannot look anything up: it starts at your pointer and counts bytes until it meets a zero. Length is a property of the data, rediscovered on every call, while capacity is a property of the storage, fixed when you declared or allocated it.
Because those two numbers are independent, the terminator has to be paid for out of capacity: text of n characters needs n + 1 bytes. Writing char s[] = "abc" sizes the array from the literal, so sizeof s is 4 while strlen(s) is 3, and the fourth byte is the zero the compiler appended. Writing char s[3] = "abc" is also legal C, because a character array initialised from a literal is allowed to drop the terminator when there is no room, and it gives you three characters that are not a string; many compilers accept it without a word, though recent gcc can flag it with -Wunterminated-string-initialization.
Every function whose name begins with str takes the terminator on faith: it reads forward until it finds a zero byte, and if your buffer has none it keeps reading into neighbouring memory, which is undefined behaviour that surfaces as an absurd length, trailing garbage in output, or a crash far from the real bug. The mem functions are the mirror image, taking an explicit byte count and neither looking for nor writing a terminator, so memcpy(dst, src, n) moves exactly n bytes and leaves the invariant entirely to you. The working habit follows from that asymmetry: any time you fill bytes yourself with a loop, memcpy, fread or recv, deciding where the zero byte goes is part of the job.
Treat a pointer handed to a str function as an assertion that a zero byte exists somewhere ahead of it, inside memory you own.
<stdio.h>
<string.h>
int main(void)
{
char greet[8] = "hi"; /* 'h', 'i', then six zero bytes */
char three[3] = { 'h', 'i', '!' }; /* three characters, no room for a terminator */
size_t i;
printf("greet = %s\n", greet);
printf("sizeof greet = %zu, strlen(greet) = %zu\n", sizeof greet, strlen(greet));
printf("greet bytes:");
for (i = 0; i < sizeof greet; i++)
printf(" %d", greet[i]);
putchar('\n');
/* three has no zero byte, so plain %s would keep reading; a precision bounds it */
printf("three = %.3s, sizeof three = %zu\n", three, sizeof three);
greet[2] = '!'; /* the terminator is gone; the next zero byte is at index 3 */
printf("strlen(greet) after overwriting the terminator = %zu\n", strlen(greet));
return 0;
}
A C string is not a type but a promise about bytes: the text is everything up to the first zero byte, and every str function reads forward until it finds one.
Worked examples
Three things called zero
Shows that the terminator, the digit character and the one-character literal are different bytes.
<stdio.h>
<string.h>
int main(void)
{
char digit = '0'; /* the character, byte 48 in ASCII */
char terminator = '\0'; /* the escape '\0' is simply the integer 0 */
char one[] = "0"; /* two bytes: the digit, then the terminator */
printf("digit = %d, terminator = %d\n", digit, terminator);
printf("sizeof one = %zu, strlen(one) = %zu\n", sizeof one, strlen(one));
printf("one[0] = %d, one[1] = %d\n", one[0], one[1]);
return 0;
}
Example explained
Line 1'\0' is an escape for the value 0, so terminator prints as 0, not as a printable character.
Line 2The digit '0' is byte 48 in ASCII, so ending a buffer with buf[n] = '0' inserts a visible zero and no terminator.
Line 3sizeof one is 2 but strlen(one) is 1: the literal contributed one character plus the terminator the compiler appended.
Line 4one[1] prints 0, showing the terminator is an ordinary array element you can index and inspect.
Asking whether bytes are a string yet
Uses memchr to test for a terminator before any str function is allowed to touch the buffer.
<stdio.h>
<string.h>
static void show(const char *label, const char *buf, size_t n)
{
if (memchr(buf, '\0', n) != NULL)
printf("%s: terminated, length %zu\n", label, strlen(buf));
else
printf("%s: %zu raw bytes, not a C string\n", label, n);
}
int main(void)
{
char a[6];
char b[6];
memset(a, 'Z', sizeof a);
memset(b, 'Z', sizeof b);
memcpy(a, "abc", 4); /* four bytes: a, b, c and the terminator */
memcpy(b, "abcdef", 6); /* six characters fill the array completely */
show("a", a, sizeof a);
show("b", b, sizeof b);
return 0;
}
Example explained
Line 1memset fills both arrays with 'Z' so no stray zero byte is lying around to make an unterminated buffer look fine.
Line 2memcpy(a, "abc", 4) copies four bytes, and the fourth is the literal's terminator, which is what turns a into a string.
Line 3memcpy(b, "abcdef", 6) is correct memory-wise but leaves no seventh byte, so b holds characters and no terminator.
Line 4memchr answers "is there a zero in these n bytes?" without trusting the contract, which is why strlen(b) is never called.
Important notes
A string literal already carries its terminator and may live in read-only memory, so with char *p = "abc" you must not write to p[i] at all, not even to move the terminator; declare an array if you need to modify the bytes.
An interior zero ends the string early: a buffer holding 'a', 'b', 0, 'c', 'd' is the string "ab" to every str function, which is why data that can legitimately contain zero bytes must be handled with mem functions and an explicit length.
Common mistakes
Sizing an array by letter count, as in char code[3] = "abc"; it compiles, silently drops the terminator, and the next printf("%s", code) or strlen(code) reads past the array into unrelated bytes.
Ending a buffer with buf[n] = '0' instead of buf[n] = '\0'; that stores byte 48, so the string does not end there and the printed text runs on. Writing buf[n] = "\0" is a different error: that assigns a pointer to a char.
Assuming fresh memory is already zeroed; a local char buf[64] or malloc(64) holds indeterminate bytes, so calling strlen before you write a terminator returns a meaningless number or walks off the end of the object.
Try it yourself
Change, predict, then run
Declare char buf[6], memset it to 'A', store '\0' at buf[3], then print strlen(buf) plus all six bytes as numbers; move the terminator to buf[5], print both again, and explain why the length changed although no letter did.
Open the C workspaceCheck your understanding
A char buf[10] holds "hello" written by an earlier call; bytes buf[6] through buf[9] were never written. You then set buf[5] = '!'. What does strlen(buf) return?
- 5, because the length was fixed when "hello" was stored in the array
- 6, since '!' just extends the text by one character
- Whatever the scan finds: it continues past buf[5] into bytes you never wrote and may read beyond the array
- 0, because removing the terminator leaves the string empty
Show answer
strlen records nothing; it counts bytes from the pointer until it meets a zero, so overwriting the terminator hands the scan the indeterminate bytes that follow, and if none of them is zero it reads past the array, which is undefined behaviour. Option 0 is tempting because we speak of a string's length as though it were stored somewhere, but C keeps only bytes, and the length is recomputed on every call.