Why this matters
Text is everywhere: names, file paths, commands, log lines, HTTP headers. Most languages have a built-in string type that knows its own length and grows as needed. C does not. In C a string is just an array of char with a special marker at the end, and every string function in the library, every printf("%s"), every file name you pass to the operating system relies on that marker being there. Understanding exactly what a C string is, byte by byte, is what separates code that works from code that prints garbage or gets exploited.
A string is a char array ending in '\0'
A string in C is a sequence of characters followed by a null character, the char with value 0, written '\0'. The null character is called the terminator. It is how every function knows where the text stops.
#include <stdio.h>
int main(void)
{
char word[] = "cat";
printf("as a string: %s\n", word);
printf("sizeof word = %zu\n", sizeof word);
for (int i = 0; i < (int)sizeof word; i++) {
printf("word[%d] = %3d", i, word[i]);
if (word[i] != '\0') {
printf(" '%c'", word[i]);
}
printf("\n");
}
return 0;
}
as a string: cat
sizeof word = 4
word[0] = 99 'c'
word[1] = 97 'a'
word[2] = 116 't'
word[3] = 0
The string literal "cat" in double quotes stands for 4 bytes: the three letters (their ASCII codes, as you learned in the types module) and a 0. The array is sized to fit all four.
word
+------+------+------+------+
| 'c' | 'a' | 't' | '\0' |
| 99 | 97 | 116 | 0 |
+------+------+------+------+
0 1 2 3
Do not confuse these three:
| written | type | meaning |
|---|---|---|
'\0' | char | the null character, value 0: the terminator |
'0' | char | the digit zero, value 48 |
"0" | string | two bytes: 48 and 0 |
and 'a' (one char, single quotes) versus "a" (a string of two bytes, double quotes).
Length versus size
The length of a string is the number of characters before the terminator. The size of the array is how many bytes it can hold. They are different numbers, and the array can be much bigger than the string in it:
#include <stdio.h>
int length(const char s[])
{
int n = 0;
while (s[n] != '\0') {
n++;
}
return n;
}
int main(void)
{
char buf[20] = "hello";
printf("%s has %d characters, buffer holds %zu bytes\n", buf, length(buf), sizeof buf);
buf[0] = 'j';
printf("%s\n", buf);
buf[2] = '\0';
printf("%s (length %d)\n", buf, length(buf));
return 0;
}
hello has 5 characters, buffer holds 20 bytes
jello
je (length 2)
buf after buf[2] = '\0':
+---+---+----+---+---+----+----+-- ... --+----+
| j | e | \0 | l | o | \0 | \0 | | \0 |
+---+---+----+---+---+----+----+-- ... --+----+
0 1 2 3 4 5 6 19
^ the string ends here; "lo" is still in memory but no longer part of it
The length function is a hand-written version of the library's strlen. Notice that it has to walk the whole string to find the end: finding the length of a C string takes time proportional to its length. That is the price of not storing the length.
Since strings are arrays, everything from the previous lessons applies: a function receives the address of the first char, can modify the caller's string unless the parameter is const, and needs no length parameter here only because the terminator marks the end.
Initializing char arrays
char a[10] = "hi"; /* 'h' 'i' '\0' and seven more '\0' */
char b[] = "hi"; /* exactly 3 bytes */
char c[] = {'h', 'i', '\0'}; /* same as b, the long way */
char d[2] = "hi"; /* legal, but NO terminator: not a string! */
char e[1] = "hi"; /* warning: initializer-string for array of 'char' is too long */
Case d is a trap: C allows a literal to exactly fill the array, silently dropping the '\0', and gcc does not warn. Leave the brackets empty, or make them at least one larger than the text.
When the terminator is missing
Every string function keeps reading until it finds a 0 byte. If there is none inside the array, it carries on into whatever memory comes next:
#include <stdio.h>
int main(void)
{
char before[4] = "zzz";
char letters[3] = {'a', 'b', 'c'}; /* no room for '\0' */
printf("[%s]\n", letters);
return 0;
}
In the sandbox this printed [abczzz]: printf ran off the end of letters and into the neighboring array before, stopping only at its terminator. On another system it might print other garbage or crash. It compiled without any warning about the problem. This is undefined behavior, and in real programs it leaks secrets (printing memory that holds a password) or crashes far from the actual mistake.
Printing and reading strings
%s prints a string: give it the array name (no index, no &). printf prints characters until the terminator. Width and precision work: %-10s pads to 10, %.3s prints at most 3 chars.
scanf("%s", buf) reads one word: it skips leading whitespace, then copies characters until the next whitespace, and adds the terminator. Two important details:
- No
&: the array name already turns into the address of its first char. - Always give a maximum width, one less than the array size, leaving room for the terminator:
scanf("%49s", word)forchar word[50]. Plain%scopies as many characters as the input contains, which overflows the array when the input is long. That is the classic buffer overflow, and the next lesson is about avoiding it.
#include <stdio.h>
#include <ctype.h>
void shout(char s[])
{
for (int i = 0; s[i] != '\0'; i++) {
s[i] = toupper((unsigned char)s[i]);
}
}
int main(void)
{
char word[50];
while (scanf("%49s", word) == 1) {
shout(word);
printf("%s!\n", word);
}
return 0;
}
$ echo "hello world of C" | ./prog
HELLO!
WORLD!
OF!
C!
The loop for (int i = 0; s[i] != '\0'; i++) is the idiom for walking a string. toupper from <ctype.h> returns the uppercase version of a letter and leaves other characters alone; the (unsigned char) cast is there because the ctype functions are only defined for non-negative char values, and char may be signed.
String literals are read-only
The text of a string literal lives in a read-only part of the program. char s[] = "cat"; makes a copy in an array you own, which you may modify. You will later see char *p = "cat";, which does not copy, and writing through such a p crashes. That difference is covered in the pointers module.
Common mistakes
- Array one too small:
char name[5] = "Alice";has no room for the terminator (and no warning). Size arrays as "longest text + 1". - Comparing strings with
==:if (word == "yes")does not compare the text (gcc warnscomparison with string literal results in unspecified behavior). Usestrcmp, next lesson. - Assigning with
=:word = "yes";fails withassignment to expression with array type; strings are arrays. Usestrcpyorsnprintf, next lesson. scanf("%s", ...)without a width: overflows on long input.- Confusing
'\0','0'and"0". - Forgetting the terminator when building a string by hand: if you fill
buf[0]tobuf[n-1]yourself, writebuf[n] = '\0';afterwards.
Recap
- A C string is a char array whose text ends with the null character
'\0'(value 0). - A literal like
"cat"is 4 bytes;sizeofcounts the terminator, the length does not. - Length means "chars before the terminator" and takes a walk through the string to compute.
- Missing terminators make string functions read past the array: undefined behavior.
- Print with
%s; read a word withscanf("%49s", buf), always with a width. - Walk a string with
for (i = 0; s[i] != '\0'; i++).
Going further: run man ascii in the sandbox and find NUL, the name of the terminator byte. Then write a function that counts the words in a string (runs of non-space characters).