C Part I · Module 8 · Strings
C Strings: Character Arrays and Null Termination
Chapter 2.8 introduced char as a small integer type that happens to be used for text, and deliberately stopped there — arrays of char, and the convention that turns one into something you can print, were held back until arrays and pointers both existed. That point has arrived, and the reason for the wait becomes obvious immediately: everything useful you can do with text in C is a pointer operation wearing a familiar notation. This chapter establishes the one convention the whole subject rests on. It is not a feature of the language, there is no string type, and the compiler does almost nothing to help you maintain it — which is precisely why it is worth seeing the individual bytes before writing a single line that depends on them.
Foundation18 min readCPart IFundamentalsStrings
Part I · Module 8 · Chapter 8.1 · C Strings: Character Arrays and Null Termination
1. The Problem C Had to Solve
Text is a sequence of characters, and Chapter 2.8 gave you the type for one character. A sequence of same-typed objects is an array — Chapter 6.1 — so an array of char is the obvious storage.
That gets you the characters and immediately runs into the result Chapter 6.1 proved with sizeof: an array stores no length. There are sixteen bytes and all sixteen are elements; there is nowhere for a count to live.
So anything working with text needs the extent from somewhere, and there are exactly two ways to supply it:
Carry a count alongside. This is what Chapter 7.7 required for arrays in general — pass the pointer and the length as two arguments.
Mark the end. Put a value in the data itself that cannot occur in real text, and agree that it means stop here.
C chose the second for text, and the choice shapes everything in this module.
A C string is a
chararray containing a sequence of characters followed by a byte with the value 0. That byte is called the null character, written'\0', and it is the terminator.
The payoff is that a string needs only one thing passed — the address of its first character. The cost is that the length is not known until something walks the string looking for the terminator, and that nothing checks the terminator is there. Both halves of that trade appear throughout this module.
2. Six Bytes for Five Letters
#include <stdio.h>
int main(void)
{
char greeting[] = "hello";
printf("sizeof greeting %zu\n", sizeof greeting);
printf("elements %zu\n", sizeof greeting / sizeof greeting[0]);
printf("as text %s\n", greeting);
for (size_t i = 0; i < sizeof greeting; i++) {
printf(" [%zu] = %3d '%c'\n", i, greeting[i],
greeting[i] == 0 ? '.' : greeting[i]);
}
return 0;
}Predict the first number before reading on. Most people say 5.
Output:
sizeof greeting 6
elements 6
as text hello
[0] = 104 'h'
[1] = 101 'e'
[2] = 108 'l'
[3] = 108 'l'
[4] = 111 'o'
[5] = 0 '.'Six. char greeting[] = "hello"; creates an array of six char objects, because the initialiser "hello" supplies six values — five characters and a terminating zero. Chapter 6.2's rule that int a[] = {...} deduces its size from the initialiser is doing the work; the only new part is what the initialiser contains.
Element [5] is an ordinary array element holding the ordinary value 0. There is no marker bit, no hidden header, and nothing distinguishing it from any other char in the array except its value.
%s is the printf conversion for a string. It takes the address of the first character and prints characters until it reaches a zero byte. It is not told a length, because there is no length to tell it.
3. '\0' Is Not '0'
This confusion is worth settling with numbers before it can take hold.
#include <stdio.h>
int main(void)
{
printf("'\\0' as a number %d\n", '\0');
printf("'0' as a number %d\n", '0');
printf("are they equal? %d\n", '\0' == '0');
printf("sizeof \"hello\" %zu\n", sizeof "hello");
printf("sizeof \"\" %zu\n", sizeof "");
return 0;
}Output:
'\0' as a number 0
'0' as a number 48
are they equal? 0
sizeof "hello" 6
sizeof "" 1'\0' is the value zero. '0' is the character you would print to show a digit, which has the value 48 on an ASCII-compatible system — Chapter 2.8's point that a character is a small integer, applied.
Two notations for the same zero are in common use:
greeting[5] = '\0'; /* conventional: says "this is a terminator" */
greeting[5] = 0; /* identical in every way */Both store the value 0. Prefer '\0' in a string context, because it tells a reader what the zero is for.
Note the last line of the output too. sizeof "" is 1 — the empty string is not zero bytes, it is one byte holding the terminator. An empty string still has to be terminated.
4. Escape Sequences
Chapter 2.8 used \n without explaining it and said the rest belonged here. A escape sequence is a backslash followed by one or more characters, which together stand for a single character that would otherwise be impossible or awkward to write.
| Written | Means | Value |
|---|---|---|
\n | newline | 10 |
\t | horizontal tab | 9 |
\\ | a single backslash | 92 |
\" | a double quote inside a string | 34 |
\' | a single quote inside a character constant | 39 |
\0 | the null character | 0 |
\r | carriage return | 13 |
Each of these is one character, not two, which matters for counting.
char tabbed[] = "a\tb";That array has four elements: 'a', a tab, 'b', and the terminator. The backslash is not stored.
5. The Terminator Is Not Guaranteed
Everything above depends on the terminator being present. Nothing in the language enforces it, and the easiest way to lose it is to size the array exactly.
char exact[5] = "hello"; /* no room for the terminator */
char roomy[6] = "hello";
char padded[8] = "hello";The first produces a diagnostic that describes the problem precisely:
warning: initializer-string for character array is too long, array size is 5
but initializer has size 6 (including the null terminating character)
[-Wunterminated-string-initialization]It is a warning. The program builds. What you get is a five-element char array holding h e l l o and nothing else:
sizeof exact 5
sizeof roomy 6
sizeof padded 8
padded bytes: 104 101 108 108 111 0 0 0
exact bytes: 104 101 108 108 111Two observations from that output.
padded is zero-filled past the text. Chapter 6.2's rule — every element an initialiser does not reach is set to zero — gives you the terminator and two more zeros for free.
exact has no zero anywhere. It is a perfectly valid char array. It is not a string, and the distinction is the whole chapter:
Every C string is a
chararray. Not everychararray is a C string. The difference is whether a terminator is present within the array's bounds.
6. The Array and the String Are Different Extents
Once the terminator is just an element with the value 0, a consequence follows that surprises people: there can be bytes after it, and they are still part of the array.
#include <stdio.h>
int main(void)
{
char with_escapes[] = "tab:\there\nquote:\"q\"\nbackslash:\\\nnul-in-middle:a\0b";
printf("%s", with_escapes);
printf("\n--- bytes ---\n");
for (size_t i = 0; i < sizeof with_escapes; i++) {
printf("%d ", with_escapes[i]);
}
printf("\nsizeof with_escapes %zu\n", sizeof with_escapes);
return 0;
}Output:
tab: here
quote:"q"
backslash:\
nul-in-middle:a
--- bytes ---
116 97 98 58 9 104 101 114 101 10 113 117 111 116 101 58 34 113 34 10 98 97 99
107 115 108 97 115 104 58 92 10 110 117 108 45 105 110 45 109 105 100 100 108
101 58 97 0 98 0
sizeof with_escapes 50The %s output stops after nul-in-middle:a. The byte dump shows why — there is a 0 at that point — and it also shows a 98 ('b') sitting after it, with the real terminator at the very end.
So the array is 50 bytes and the string is 47 characters plus a terminator. The 'b' is in the array and is not in the string. Printing with %s can never reveal it; only looking at the bytes can.
This is also a useful way to hold the whole convention:
The array is the storage you own. The string is the part of it up to the first zero. They are not the same thing, and only one of them is visible to anything that takes a
char *.
7. The Terminator Decides Where the String Ends
Because the terminator is just a value, moving it changes the string without touching anything else.
#include <stdio.h>
int main(void)
{
char buffer[8] = {0};
buffer[0] = 'h';
buffer[1] = 'i';
printf("[%s]\n", buffer);
buffer[2] = 'X';
printf("[%s]\n", buffer);
buffer[3] = '\0';
printf("[%s]\n", buffer);
return 0;
}Output:
[hi]
[hiX]
[hiX]The first line works because = {0} filled all eight bytes with zero — Chapter 6.2 again — so the terminator was already sitting at [2].
Writing 'X' into [2] overwrote the terminator, and the string became hiX only because [3] was already zero. Had the array held leftover data there, the string would have continued into it.
The third write is then redundant: [3] was already '\0'.
Declaring a buffer with
= {0}is the cheapest insurance in C string handling. Every byte is a terminator until you overwrite it, so a partially filled buffer is always a valid string.
8. Counting Characters
The length of a string is not stored, so it has to be computed — by walking until the terminator.
#include <stdio.h>
#include <stddef.h>
int main(void)
{
char greeting[] = "hello";
size_t characters = 0;
while (greeting[characters] != '\0') {
characters++;
}
printf("sizeof greeting %zu bytes\n", sizeof greeting);
printf("characters %zu\n", characters);
printf("difference %zu (the terminator)\n",
(sizeof greeting) - characters);
return 0;
}Output:
sizeof greeting 6 bytes
characters 5
difference 1 (the terminator)Nothing in that loop is new — it is Chapter 6.3's traversal with a different stopping condition. Instead of i < count, the condition is the element is not the terminator, which is exactly Chapter 7.8 §6's observation that the pointer-walking form belongs where the end is marked rather than counted.
Two facts worth separating permanently:
| What it gives you | Known when | |
|---|---|---|
sizeof array | the array's size in bytes, terminator included | compile time |
| the counting loop | the string's length, terminator excluded | run time |
sizeof works only where the array is in scope — Chapter 7.7 established that it reports a pointer's size inside a function. The counting loop works anywhere you have the address, which is why it is what the standard library provides. Chapter 8.4 covers that function.
9. What You Should Be Able to Say Now
- C has no string type. A string is a
chararray with a convention. - An array stores no length (Chapter 6.1), so text needs either a count carried alongside or a marked end. C marks the end.
- The marker is the null character
'\0', an ordinary element holding 0. char greeting[] = "hello";makes six elements — five letters and a terminator.'\0'is 0;'0'is 48. They are different characters.sizeof ""is 1 — an empty string is still terminated.%sprints until it reaches a zero byte and is never told a length.- Every C string is a
chararray; not everychararray is a C string.char exact[5] = "hello"has no terminator and warns. - The array and the string are different extents — bytes can exist after the terminator.
- Length is computed at run time by walking to the terminator;
sizeofis a compile-time property of the array.
10. Exercises
Exercise 1 — give the sizes. For each, state sizeof and how many characters the string has.
char a[] = "cat";
char b[] = "";
char c[] = "a\tb";
char d[10] = "cat";
char e[] = "\\";Exercise 2 — predict the bytes. Write out every element of char s[] = "hi\n"; with its numeric value, and say how many elements the array has.
Exercise 3 — settle the confusion. Give the numeric value of '\0', '0', 'a' and 'A', say which two are equal if any, and say which one is a terminator.
Exercise 4 — spot the missing terminator. For each, say whether the result is a valid C string and why.
char p[4] = "abc";
char q[3] = "abc";
char r[5] = {'a', 'b', 'c'};
char s[3] = {'a', 'b', 'c'};Exercise 5 — predict the output. Do not compile yet.
char buf[6] = {0};
buf[0] = 'o'; buf[1] = 'n'; buf[2] = 'e';
printf("[%s]\n", buf);
buf[1] = '\0';
printf("[%s]\n", buf);
buf[1] = 'n';
printf("[%s]\n", buf);Explain why the third line can recover the original text.
Exercise 6 — count it by hand. Write the loop that counts the characters in a char array, and say what it does if the array has no terminator. Relate your answer to Chapter 6.4.
Exercise 7 — explain the two extents. For §6's 50-byte array, say how many bytes the array has, how many characters %s prints, and where the 'b' is. Then say what tool revealed it and why %s could not.
Exercise 8 — size a buffer. You must store a label of at most 12 characters. Give the declaration, say why your size is what it is, and give the initialiser you would use so the buffer is a valid string before anything is written into it.
11. Summary
- There is no string type in C. A string is a
chararray plus a convention. - The convention exists because an array stores no length — C chose to mark the end.
- The marker is
'\0', an ordinary element with the value 0;'0'is 48 and unrelated. char g[] = "hello"is six elements.sizeof ""is 1.%sprints to the first zero byte and is never told a length.- Not every
chararray is a string — an exactly sized one has no room for the terminator and warns. - A
chararray with no terminator passed to%sis Chapter 6.4's undefined behaviour. - The array and the string are different extents; bytes may follow the terminator.
= {0}makes a buffer a valid string before you write to it — the cheapest insurance there is.- Length is computed at run time;
sizeofis a compile-time property of the array.
Every string in this chapter has been a char array that the program owns, declared with char name[] = "..." and modified freely. There is a second way to write what looks like almost the same thing — a char * initialised from a literal — and it produces something that prints identically, compares identically, and behaves completely differently the moment you try to change it. The distinction is one character of syntax and it is the single most consequential thing to get right about strings in C, which is why the next chapter is about nothing else.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
