Foundations · Module 1 · Lesson 5 of 7
Text in C: Working with Strings
Learn how C stores text as null-terminated character arrays, read and print strings, and avoid buffer overflows when copying text.
By the end you can
- Declare and initialize a character array as a string
- Print and read strings using %s with printf and scanf
- Explain the null terminator \0 and why strings need it
- Copy strings safely without overflowing the destination buffer
Builds on Lists of Data: Arrays.
01 · A question first
You want to store a person's name in a variable. An int holds a number, but how do you hold "Alice" or "Bob"? C has no built-in string type. How do you store text, and what stops your program from reading past the end of a name?
Make a guess, then step through the trace below to check it.
02 · Watch it run
char name[6] = {'H', 'i', '!', '\0', '?', '?'};printf("%s\n", name);- slot 0[0]: 'H'(looked at in this step)
- slot 1[1]: 'i'
- slot 2[2]: '!'
- slot 3[3]: '\0'
- slot 4[4]: '?'
- slot 5[5]: '?'
The array name holds 6 characters: H, i, !, null terminator, then two uninitialized slots.
Machine work: allocate name[6]; fill with {'H','i','!','\0','?','?'}
03 · The idea
Start with the problem
Numbers are only half of what programs handle; the other half is text—names, file paths, command-line flags, HTTP headers, and log messages. You already know that a single char variable holds one character byte (such as 'A', ASCII value 65). And from the previous lesson, you know that an array of char (char name[50];) can hold a sequence of characters in contiguous memory.
But there is a catch: a person's name might be "Al" (2 letters), "Alice" (5 letters), or "Bartholomew" (11 letters), all stored inside the same 50-byte char name[50] array. When you pass name to printf("%s ", name), how does printf know where the letters of the name stop and the unused slots of the 50-byte array begin?
C solves this without storing a separate length field by placing a special sentinel byte—the null terminator ('�', numeric byte value 0)—immediately after the last character of the text.
How the idea works
In C, a string is simply a contiguous array of char elements terminated by the first null byte ('�').
Look at the trace above to watch how printf uses that sentinel byte:
- Memory layout (Step 1): On line 1,
char name[6]is allocated with 6 slots holding'H','i','!','�','?', and'?'. Even though the array is 6 bytes long, the string inside it is only 3 characters long ("Hi!"), followed by'�'at index3. - Reading until
�(Step 2): On line 2,printf("%s ", name)starts at index0and prints'H','i', and'!'. As soon as it sees'�'at index3, it stops immediately—the two'?'bytes at indices4and5are never read or printed.
String literals vs character constants
In C, single quotes and double quotes mean completely different things:
'A'(single quotes) is a singlecharinteger constant (1 byte, ASCII value65)."A"(double quotes) is a string literal: a 2-bytechararray containing{'A', '�'}.
When you initialize a char array with a double-quoted string literal, the compiler automatically appends the trailing '�' for you:
char greeting[] = "Hello";
/* Equivalent to: char greeting[6] = {'H', 'e', 'l', 'l', 'o', '�'}; */Notice the size: "Hello" has 5 visible letters, so strlen(greeting) is 5, but the array occupies 6 bytes in memory (sizeof(greeting) is 6) because of the mandatory null terminator at index 5.
Reading strings safely without buffer overflow
When reading a word from stdin into a char array with scanf, you pass the array name without & (because an array name in an expression already evaluates to the address of its first element &name[0]). However, bare scanf("%s", name) is dangerous: if name is 10 bytes long and the user types a 30-letter word, scanf will write all 30 characters past the end of name, corrupting the stack.
Always specify a maximum field width one less than the array capacity so scanf saves 1 byte for '�':
char name[16];
printf("Enter username (max 15 chars): ");
if (scanf("%15s", name) == 1) {
printf("Welcome, %s!
", name);
}If you want to read an entire line of text including spaces, use fgets(buffer, sizeof(buffer), stdin), which guarantees it will never read more than sizeof(buffer) - 1 characters and always writes the trailing '�'.
Standard string functions in <string.h>
Because strings are arrays, you cannot assign them with b = a or compare their contents with a == b (which only compares whether a and b sit at the same memory address!). Instead, include <string.h> and use standard library functions that walk the array up to '�':
#include <stdio.h>
#include <string.h>
int main(void) {
char word[16] = "runtime";
/* 1. Length (counts characters BEFORE '�') */
printf("Length: %zu
", strlen(word)); /* prints 7 */
/* 2. Content comparison (returns 0 when equal!) */
if (strcmp(word, "runtime") == 0) {
printf("Exact match!
");
}
/* 3. Safe bounded copy into a destination buffer */
char copy[16];
snprintf(copy, sizeof(copy), "%s", word);
printf("Copied: %s
", copy);
return 0;
}Why it is correct
Every function that processes a C string relies on a two-part string invariant:
- Null-termination: Somewhere between index
0andcapacity - 1, there is a'�'byte marking the end of the text. - Capacity bound (
strlen(s) + 1 <= capacity): A buffer ofNbytes can hold at mostN - 1visible characters, because the final byte must hold'�'.
As long as every write into a buffer preserves that trailing '�' within bounds, any reader (printf, strlen, strcmp) can safely walk from index 0 until s[i] == '�' without needing a separate length struct.
What it costs
Because C strings do not store their length in a header field, finding the length of a string with strlen(s) requires walking from index 0 until it hits '�'. That means strlen(s) takes O(n) linear time, not O(1)! Calling strlen(s) inside a loop condition like for (int i = 0; i < strlen(s); i++) re-scans the entire string on every character, turning a linear O(n) loop into a slow O(n²) loop. Instead, compute size_t len = strlen(s); once before the loop, or test for (int i = 0; s[i] != '�'; i++).
Pitfalls and variations
| Mistake | What goes wrong | Fix |
|---|---|---|
char name[3] = "Bob"; | No room for '�'; string functions read out of bounds | Allocate at least 4 bytes: char name[4] = "Bob"; or char name[] = "Bob"; |
Comparing strings with if (a == "yes") | Compares memory addresses instead of character contents | Use if (strcmp(a, "yes") == 0) from <string.h> |
Using unbounded scanf("%s", buf) or strcpy(dst, src) | Input longer than the buffer overwrites adjacent stack memory (buffer overflow) | Use width-limited scanf("%15s", buf), fgets, or snprintf |
Where it is used
Command-line arguments passed to main(int argc, char *argv[]), Unix file paths, environment variables, and C system call parameters (open("/etc/hosts", O_RDONLY)) are all null-terminated C strings. Even higher-level languages like Python and Rust convert strings into null-terminated byte arrays whenever they call into the operating system's C libraries.
In the next lesson, you will demystify the single most important concept in C—pointers—and see how memory addresses and dereferencing connect variables, functions, and arrays together.
04 · Prove it
Question 1 of 3 · answer from memory
What character marks the end of a C string?
05 · Practice
Something wrong with this lesson? Report a problem