C Strings and the Null Terminator
A string is a char array ending in a null byte: string literals, iterating to the terminator, and what becomes undefined when the terminator is missing.
A String Is a Convention, Not a Type
Chapter 4 ended on a promise, and this lesson cashes it in the first paragraph. A C string is a char array whose end is marked by one byte with the value zero. That is the entire definition. There is no string type in C, no object that knows its own length, no hidden size field the compiler maintains for you. There is a run of char objects, exactly the contiguous run chapter 4 described, plus a convention about how it ends. Every operation you will ever perform on a string works by looking for that final byte, which is why the byte has a name of its own: the null terminator.
It is written '\0', and it is a character constant like chapter 2's 'A'. 'A' is the int 65, and '\0' is the character whose value is 0. That makes it a completely different thing from '0', the digit, whose value is 48. The two look nearly identical in source and the compiler will never question a swap, because to C both are just small integers.
A string literal in your source is already an array in this shape. "hi" looks like two characters and is three, because the compiler appends the terminator itself:
"hi" index: 0 1 2
value: 'h' 'i' '\0'
as int: 104 105 0
Which gives you the rule the rest of this chapter keeps paying for. A buffer that holds an n-character string needs n + 1 bytes, one for each character and one for the terminator. Nearly every buffer overrun a beginner writes is that + 1 missing.
Writing char greeting[] = "hi"; declares an array and copies the literal's three bytes into it. This is the third and last of the decay exceptions chapter 4 named and deferred: a string literal initializing an array does not decay to a pointer, it initializes the array element by element. You leave the brackets empty because the initializer already knows the size, and what you get is an ordinary writable array of char that obeys chapter 4's rules and needs no new ones.
Counting to the Terminator
Since nothing records the length, finding it means walking from the start until you meet the zero byte. That loop is the most important shape in this chapter, and you are going to write it before the library that does it for you arrives next lesson.
#include <stdio.h>
int count_characters(const char *text)
{
int count = 0;
while (text[count] != '\0')
{
++count;
}
return count;
}
int main(void)
{
char greeting[] = "hi";
char word[] = "memory";
printf("greeting[0] is %c with value %d\n", greeting[0], greeting[0]);
printf("greeting[2] has value %d\n", greeting[2]);
printf("%s: %d characters, sizeof %zu\n", greeting, count_characters(greeting), sizeof(greeting));
printf("%s: %d characters, sizeof %zu\n", word, count_characters(word), sizeof(word));
return 0;
}
greeting[0] is h with value 104
greeting[2] has value 0
hi: 2 characters, sizeof 3
memory: 6 characters, sizeof 7
The first two lines are chapter 2's dual reading of char doing real work, since %c shows the character and %d shows the number, and the terminator is the one element where only %d has anything to show. The last two lines carry the point. Two numbers describe one array and they differ by one every time, because the character count stops at the terminator and excludes it, while sizeof on the array counts every byte and includes it. Both are true and they answer different questions, so hold them apart now: next lesson hands you a library function that computes the first one, and beginners reach for it when they wanted the second.
Notice the signature. count_characters takes a const char * and no count, because unlike every other array in chapter 4, a string carries its own end marker and needs no length parameter beside it. That is the one thing the convention buys you. The parameter is const because the function only reads, and the loop could equally have been written with a moving pointer, while (*text != '\0') { ++text; }, since text[i] is still *(text + i).
%s is the other walker. Handed a char *, printf starts at that address and writes bytes out until it meets the terminator, which is how one specifier prints a string of any length. It is also chapter 1's security rule made concrete, so write printf("%s", text) and never printf(text), because a string you did not write is data and must never be read as a format.
Two Declarations That Look Alike
Here is the distinction the syntax hides. char copy[] = "hi"; is an array that owns a copy of the literal. const char *literal = "hi"; is a pointer aimed at the literal itself, which is an object of its own with static storage duration, alive for the whole run and living in a part of memory the program does not get to write.
#include <stdio.h>
int main(void)
{
char copy[] = "hi";
const char *literal = "hi";
printf("both print as %s and %s\n", copy, literal);
printf("sizeof(copy) is %zu but sizeof(literal) is %zu\n", sizeof(copy), sizeof(literal));
copy[0] = 'H';
printf("copy is now %s and literal is still %s\n", copy, literal);
return 0;
}
both print as hi and hi
sizeof(copy) is 3 but sizeof(literal) is 8
copy is now Hi and literal is still hi
Reading gives no hint that they differ, since %s prints the same two characters through both. sizeof does: 3 is the array, 8 is a pointer on this machine, chapter 4's decay-exception measurement applied to a case where it finally matters. Writing to the array is fine, as the third line shows. And the pointer is declared const for an honest reason. The next program is wrong on purpose.
#include <stdio.h>
int main(void)
{
char *greeting = "hi";
greeting[0] = 'H';
printf("%s\n", greeting);
return 0;
}
char *greeting = "hi"; is legal C and gcc compiles it at -Wall -Wextra without a word of complaint. Modifying a string literal is undefined behaviour, and on this platform the run ends before anything prints:
AddressSanitizer:DEADLYSIGNAL
==12==ERROR: AddressSanitizer: SEGV on unknown address 0x000000400980 (pc 0x0000004007d0 bp 0xffffdc5cd250 sp 0xffffdc5cd250 T0)
==12==The signal is caused by a WRITE memory access.
#0 0x0000004007d0 in main /tmp/e.c:7
SUMMARY: AddressSanitizer: SEGV /tmp/e.c:7 in main
That is chapter 3's SEGV again and it reads the same way, except the cause is neither a null nor a dangling address. The address is a perfectly real one holding the literal, on a page the operating system mapped read-only, and the WRITE line names the operation that was refused. Do not let the crash teach the wrong lesson, though. Undefined behaviour is not a promise of a crash: the standard permits anything at all here, and another compiler or platform may quietly let the write through.
The rule falls out with no room for judgement. If you intend to modify a string, declare it as an array so you get a copy. If you only intend to read it, declare the pointer const char *, so chapter 3's read-only promise turns the accident into a compile error you can see rather than a run you have to survive.
The Byte That Is Not There
Everything above depends on the terminator being present, so the last question is what happens when it is not. Nothing stops you filling a char array to the brim. The next program is wrong on purpose.
#include <stdio.h>
int main(void)
{
char word[2] = {'h', 'i'};
printf("the word is %s\n", word);
return 0;
}
That array is legal, complete and correctly initialized. It is two char objects holding 'h' and 'i', and every one of chapter 4's initializer-list rules is satisfied. What it is not is a string, because there is no room left for the terminator and the initializer did not supply one. printf cannot know that. Handed the address, %s walks forward looking for a zero byte, and having passed the end of the array it keeps walking:
==12==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xfbff888f0022 at pc 0xffff8b32ee94
READ of size 3 at 0xfbff888f0022 thread T0
#2 0xffff8b343450 in printf (/usr/local/lib64/libasan.so.8+0xb3450)
#3 0x0000004008f4 in main /tmp/e.c:7
Address 0xfbff888f0022 is located in stack of thread T0 at offset 34 in frame
#0 0x00000040086c in main /tmp/e.c:4
This frame has 1 object(s):
[32, 34) 'word' (line 5) <== Memory access at offset 34 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/e.c:7 in main
Read it with chapter 4's four lookups and it is a familiar report. The kind is stack-buffer-overflow, so an automatic array was touched outside itself. The operation is a READ rather than a write, which is the part worth sitting with, because a program that only prints a string can still be an out-of-bounds program. The location is the first frame naming your file, #3, the frames above it being inside printf walking on your behalf. And the last field solves the case: [32, 34) 'word' is a two-byte object and the access is at offset 34, the very first byte past its end. Nothing about the array is wrong. What is wrong is calling two bytes a string.
The fix is the n + 1 rule and it has two spellings. Write char word[3] = {'h', 'i', '\0'}; and terminate the array by hand, or write char word[] = "hi"; and let the literal do it, which is why the literal form is the one to reach for. When you do size a buffer yourself, size it for the characters and for the byte that ends them.
Key Takeaways
- A C string is a convention, not a type: a
chararray whose end is marked by the null terminator'\0'. Nothing records a string's length, so every operation on one finds the end by walking to that byte. '\0'is the character with value 0 and'0'is the digit with value 48. They are different characters and the compiler will not warn you about swapping them.- A buffer for an
n-character string needsn + 1bytes."hi"is threechars, andchar s[] = "hi";hassizeof3 while a walk to the terminator counts 2, because the count excludes the terminator andsizeofincludes it. char s[] = "hi";copies the literal into a writable array;char *p = "hi";points at the literal itself. The literal has static storage duration and modifying it is undefined behaviour, which this platform reports as aSEGVcaused by aWRITE. Declare an array when you mean to modify, andconst char *when you only mean to read.printf's%stakes achar *and walks to the terminator. Always writeprintf("%s", text)rather thanprintf(text), because a string that came from outside your program must never be used as a format.- A
chararray with no terminator is not a string, and handing one to%sor to a counting loop reads past its end. AddressSanitizer reports it as astack-buffer-overflowwith aREAD, naming the array and the offset one byte beyond it.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
C Strings and the Null Terminator - Quiz
Test your understanding of the lesson.
Practice Exercises
Count and Rewrite Strings Without a Library
The string library arrives next lesson. Before it does, write the two loops it is built out of, so that nothing about it can look like magic later. main is written for you and needs no changes. It declares three arrays, char word[] = "memory", char phrase[] = "banana bread" and char tag[] = "cs", each of them a writable copy of its literal with the terminator already in place, and each of them printed twice: once as a character count beside its sizeof, and once after a character has been replaced in it. There is no input to read, so every run produces the same six lines. Your job is the two functions. int count_characters(const char *text) returns how many characters come before the null terminator, which means starting a counter at 0 and walking forward while text[count] is not '\0', stopping the moment it is. The terminator is the end marker rather than a character of the string, so it is never counted, which is why the printed count is always one less than the sizeof printed beside it: sizeof measures the whole array in bytes and the terminator is one of them. Look at the tag line to see that in miniature, since "cs" is two characters in an array of three. void replace_character(char *text, char target, char replacement) walks the same way and assigns replacement to every position holding target, leaving everything else alone. Two things about its signature are deliberate. There is no count parameter on either function, because unlike every other array in chapter 4 a string carries its own end marker and needs no length beside it; the terminator is the bound. And text is const char * in the first function and plain char * in the second, because the first only reads while the second writes, and the write is legal only because main declared arrays rather than pointers to literals. Had main written char *word = "memory", the same assignment would be modifying a string literal, which is undefined behaviour. Do not use any function from string.h, which the next lesson introduces properly, and note that phrase contains three a characters plus a fourth inside bread, so a loop that stops at the first match or at the space will not produce the expected line. main returns 0 on its only path, since the checker treats a nonzero exit status as a failure however correct the output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!