TRT
Sign in

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.

FreeAbout 15 minutes

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

Tracing a null-terminated string in memorystep 1 of 2
char name[6] = {'H', 'i', '!', '\0', '?', '?'};printf("%s\n", name);
  1. slot 0[0]: 'H'(looked at in this step)
  2. slot 1[1]: 'i'
  3. slot 2[2]: '!'
  4. slot 3[3]: '\0'
  5. slot 4[4]: '?'
  6. 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:

  1. 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 index 3.
  2. Reading until � (Step 2): On line 2, printf("%s ", name) starts at index 0 and prints 'H', 'i', and '!'. As soon as it sees '�' at index 3, it stops immediately—the two '?' bytes at indices 4 and 5 are 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 single char integer constant (1 byte, ASCII value 65).
  • "A" (double quotes) is a string literal: a 2-byte char array containing {'A', '�'}.

When you initialize a char array with a double-quoted string literal, the compiler automatically appends the trailing '�' for you:

c
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 '�':

c
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 '�':

c
#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:

  1. Null-termination: Somewhere between index 0 and capacity - 1, there is a '�' byte marking the end of the text.
  2. Capacity bound (strlen(s) + 1 <= capacity): A buffer of N bytes can hold at most N - 1 visible 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

MistakeWhat goes wrongFix
char name[3] = "Bob";No room for '�'; string functions read out of boundsAllocate at least 4 bytes: char name[4] = "Bob"; or char name[] = "Bob";
Comparing strings with if (a == "yes")Compares memory addresses instead of character contentsUse 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

Not started

Sign in to save your learning progress.

Sign in to save