Strings

C has no string type. A string is a char array whose last used byte is a zero, and every function that handles text from lesson 25 onward finds the end by scanning for that zero. The array and the terminator are the whole convention; the compiler enforces neither.

Literals and arrays

A string literal in double quotes is stored with a terminating zero, and a char array initialised from it gets a copy of those bytes. sizeof counts the terminator, so the size is one more than the visible text. Printing the bytes confirms the zero at the end.

#include <stdio.h> int main(void) { char word[] = "cat"; printf("%s %zu\n", word, sizeof word); printf("bytes: %d %d %d %d\n", word[0], word[1], word[2], word[3]); return 0; }

gcc -Wall -Wextra literal.c -o literal
./literal

cat 4
bytes: 99 97 116 0

The four bytes are the character codes for c, a and t followed by the zero. %s printed only three characters because it stopped at the zero, while sizeof counted all four. A literal is read-only: writing through a pointer to it is undefined behaviour, so text that must change goes into an array like word.

Walking to the terminator

The length is the number of bytes before the zero, which a loop can count without knowing the array size. The same walk can change each character, as long as the array is writable. The example measures "hello, world" and turns its lower-case letters into upper case.

#include <stdio.h> int main(void) { char text[] = "hello, world"; int length = 0; while (text[length] != '\0') { length++; } for (int i = 0; text[i] != '\0'; i++) { if (text[i] >= 'a' && text[i] <= 'z') { text[i] = (char) (text[i] - ('a' - 'A')); } } printf("length: %d\n", length); printf("upper: %s\n", text); return 0; }

gcc -Wall -Wextra traverse.c -o traverse
./traverse

length: 12
upper: HELLO, WORLD

The difference 'a' - 'A' is 32 on an ASCII system, and the cast keeps the assignment in char arithmetic. Lesson 24 replaces the two comparisons with the functions from ctype.h.

Building a string by hand

A copy is a loop over the source that writes the same bytes to the destination, followed by a terminator at the new end. The destination must have room for every byte plus the zero. The example appends an exclamation mark while copying, which a plain byte copy could not do, and leaves the rest of the buffer at zero through its initialiser.

#include <stdio.h> int main(void) { char source[] = "tea"; char target[8] = {0}; int i = 0; while (source[i] != '\0') { target[i] = source[i]; i++; } target[i] = '!'; target[i + 1] = '\0'; printf("%s (%d chars)\n", target, i + 1); return 0; }

gcc -Wall -Wextra copy.c -o copy
./copy

tea! (4 chars)

The count is i + 1 because the loop stopped on the terminator of the source, and the exclamation mark took its place in the destination. Four characters plus the terminator fit in eight bytes, with room to spare; a buffer of three would have written past its end.

Comparing two strings

== on two arrays compares their addresses, not their contents, so a hand-written comparison walks both strings until the bytes differ or both end. The result here is the numeric difference of the first differing characters.

#include <stdio.h> int main(void) { char a[] = "apple"; char b[] = "apricot"; int i = 0; while (a[i] != '\0' && a[i] == b[i]) { i++; } printf("first difference: %d\n", a[i] - b[i]); return 0; }

gcc -Wall -Wextra compare.c -o compare
./compare

first difference: -2

The first three letters match, then 'p' minus 'r' is -2, so "apple" sorts before "apricot". A comparison function usually returns -1, 0 or 1 instead of the raw difference, and lesson 25 shows the library version that does exactly that.

A missing terminator, a wrong format

The first mistake gives the array exactly as many bytes as the visible text. The initialiser drops the terminator, and GCC 15 warns about the truncated string. The program builds and %s then reads past the array until it happens to find a zero.

#include <stdio.h> int main(void) { char word[3] = "cat"; printf("%s\n", word); return 0; }

gcc -Wall -Wextra badterm.c -o badterm

badterm.c: In function 'main':
badterm.c:5:20: warning: initializer-string for array of 'char' truncates NUL terminator but destination lacks 'nonstring' attribute (4 chars into 3 available) [-Wunterminated-string-initialization]
5 | char word[3] = "cat";
| ^~~~~

The second mistake passes a single character to %s, which wants the address of a string. The compiler reports the type mismatch, and the program would treat the character code as a pointer if it ran:

#include <stdio.h> int main(void) { char letter = 'x'; printf("%s\n", letter); return 0; }

gcc -Wall -Wextra badfmt.c -o badfmt

badfmt.c: In function 'main':
badfmt.c:7:14: warning: format '%s' expects argument of type 'char *', but argument 2 has type 'int' [-Wformat=]
7 | printf("%s\n", letter);
| ~^ ~~~~~~
| | |
| | int
| char *
| %d

The suggestion at the bottom is %d, because the argument was promoted to int. To print one character use %c; to print a string, pass an array as in the sections above.

Exercises

  1. Count the vowels of a string with a loop and a switch, without using string.h.
  2. Reverse a string in place by swapping the first and last characters and moving inward. Check an even and an odd length.
  3. Write a function that appends one string to another by hand and refuses to write past a given capacity. Return the number of characters written.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What marks the end of a C string?

    Answer

    A zero byte

    Functions that handle strings scan for the terminating zero; the array size alone does not tell them where the text ends.

  2. Why is sizeof "cat" one more than the visible length?

    Answer

    The terminator is part of the array

    The literal stores three characters plus the terminating zero, so its array type has four elements.

  3. What does == compare for two char arrays?

    Answer

    Their addresses

    Array names decay to pointers, so == compares where the arrays live, not the bytes they contain; use strcmp for contents.

  4. Which string can be modified in place?

    Answer

    A char array initialised from a literal

    The array is a writable copy; a literal is read-only and writing through a pointer to it is undefined behaviour.

  5. What does this program print? #include <stdio.h> int main(void) { char word[] = "cat"; printf("%zu\n", sizeof word); return 0; }

    Answer

    4

    Three visible characters plus the terminating zero make four bytes.

  6. What does this program print? #include <stdio.h> int main(void) { char word[] = "cat"; printf("%d %d %d %d\n", word[0], word[1], word[2], word[3]); return 0; }

    Answer

    99 97 116 0

    The bytes are the ASCII codes of c, a and t, followed by the zero terminator.

  7. What does this program print? #include <stdio.h> int main(void) { char text[] = "hello, world"; int length = 0; while (text[length] != '\0') length++; printf("%d\n", length); return 0; }

    Answer

    12

    The loop counts the bytes before the zero, and the comma and space are part of the twelve characters.

  8. What does this program print? #include <stdio.h> int main(void) { char a[] = "apple"; char b[] = "apricot"; int i = 0; while (a[i] != '\0' && a[i] == b[i]) i++; printf("%d\n", a[i] - b[i]); return 0; }

    Answer

    -2

    The first differing characters are p and r, and their code difference is -2.

  9. This program is built with gcc -std=c17 -Wall -Wextra badterm.c -o badterm. Which warning flag does GCC print? #include <stdio.h> int main(void) { char word[3] = "cat"; printf("%s\n", word); return 0; }

    Answer

    -Wunterminated-string-initialization

    The initialiser drops the terminator, which -Wunterminated-string-initialization reports.

  10. This program is built with gcc -std=c17 -Wall -Wextra badfmt.c -o badfmt. Which warning flag does GCC print? #include <stdio.h> int main(void) { char letter = 'x'; printf("%s\n", letter); return 0; }

    Answer

    -Wformat=

    %s expects char *, so the mismatch is a format warning under -Wformat=.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev