Null-Terminated Strings in C Explained
In the C programming language, strings are not a distinct primitive
data type, but rather contiguous sequences of characters stored in
memory and terminated by a special character called the null terminator
('\0'). This article examines the internal representation
of null-terminated strings in C, explaining how memory is allocated for
them, how standard library functions interact with the null byte, and
the operational trade-offs associated with this design choice.
Internal Memory Layout
A C string is represented as a contiguous one-dimensional array of
char values. The end of the string is explicitly marked by
a null character—a byte with an ASCII/binary value of zero
(0x00), denoted in code as '\0'.
When you declare a string literal such as:
char greeting[] = "Hello";The compiler reserves six consecutive bytes in memory:
| Index | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| Character | 'H' | 'e' | 'l' | 'l' | 'o' | '\0' |
| Hex Value | 0x48 | 0x65 | 0x6C | 0x6C | 0x6F | 0x00 |
Even though the human-readable text contains five characters, the
internal representation requires N + 1 bytes, where
N is the length of the string.
Purpose of the Sentinel Byte
The null byte acts as an in-band sentinel value. Because the array
does not store explicit metadata regarding its length, functions must
scan the memory sequentially, element by element, until they encounter
the '\0' byte.
Standard library functions in <string.h> rely
entirely on this contract:
strlen(s): Increments a counter while iterating through memory addresses starting atsuntil the current byte equals0.strcpy(dest, src): Copies bytes sequentially from the source address to the destination address, halting immediately after copying the null byte.printf("%s", s): Outputs characters sequentially until reaching the null character.
Performance and Safety Implications
Using a null-terminated representation introduces specific performance characteristics and safety considerations:
- Linear Time Complexity for Length: Determining string length requires an \(O(n)\) traversal. Languages that use length-prefixed strings (like Pascal or modern languages such as Rust and Go) store the size separately, allowing \(O(1)\) length lookups.
- Buffer Overruns: If a string array is not properly
terminated—either due to insufficient buffer allocation or improper
manual manipulation—functions like
strlenandstrcpywill read past the allocated buffer bounds until an arbitrary zero byte is found in memory. This leads to undefined behavior, data corruption, or segmentation faults. - Binary Data Restrictions: Because the byte
0x00is reserved to signify the end of the string, null-terminated strings cannot contain raw binary data or embedded null characters. Handling binary data in C requires a separate byte array (unsigned char*) paired with an explicit length variable.