The C Path — learn C, visually

🎯 Pointers & Memory

Strings: char arrays with a secret handshake

⏱ 13 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Every username, chat message, and filename your programs will ever touch is text — and C stores text as a bare char array with a hidden stop sign at the end. Lose that stop sign and the string functions march through memory unchecked; that exact class of mistake helped the 1988 Morris worm knock out a tenth of the internet. This lesson hands you the convention, the toolbox, and the habits that keep your code off that list.

C has no string type. What it has is a convention: a string is a char array whose end is marked by a zero byte, written '\0' and called the null terminator. Every string function in existence — printf("%s"), the length-counter strlen, all of them — just walks forward from a starting address until it hits that zero.

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

Working with strings

hi.c
#include <stdio.h>
#include <string.h>

int main(void) {
    char s[] = "hi";    /* really {'h','i','\0'} */

    printf("strlen: %zu\n", strlen(s));   /* chars before \0 */
    printf("sizeof: %zu\n", sizeof s);    /* whole array     */

    for (int i = 0; s[i] != '\0'; i++)    /* the string walk  */
        printf("s[%d] = '%c' (%d)\n", i, s[i], s[i]);
    return 0;
}
terminal
$ gcc hi.c -o hi && ./hi
strlen: 2
sizeof: 3
s[0] = 'h' (104)
s[1] = 'i' (105)

Two different questions, two different answers: strlen counts characters before the terminator (2); sizeof measures the whole array including it (3). Forgetting the terminator's byte when sizing a buffer is a classic off-by-one.

🧠 Checkpoint: How many bytes does char s[] = "cat"; occupy?

  • 3
  • 4
  • 8
  • Implementation-defined
Show answer

4 — Three letters plus the hidden '\0' terminator = 4 bytes. Buffers must always budget that extra byte.

String literals are (effectively) read-only

The same-looking declarations below are deeply different. An array initialized from a literal gets its own writable copy of the bytes. A pointer assigned a literal points straight into the program's read-only data segment (.rodata — remember the memory map?):

two_kinds.c — one of these lines is a landmine
char stack_copy[] = "hello"; /* ARRAY: bytes copied onto the
                                stack — yours, writable      */
char *readonly   = "hello";  /* POINTER into .rodata — the
                                literal itself, do NOT write */

stack_copy[0] = 'H';         /* fine: your copy              */
readonly[0]   = 'H';         /* UB — typically SIGSEGV       */
💀

Modifying a string literal is undefined behavior. On typical systems the literal lives in a write-protected page, so readonly[0] = 'H' dies with a segfault. Defend yourself by declaring literal pointers as const char *s = "hello"; — then the compiler rejects the write at build time instead.

🧠 Checkpoint: char *p = "abc"; p[0] = 'x'; — what happens?

  • p becomes "xbc"
  • Compile error
  • Undefined behavior — the literal is (effectively) read-only
  • Nothing at all
Show answer

Undefined behavior — the literal is (effectively) read-only — The pointer aims at the literal in read-only storage; writing through it is UB and usually segfaults. Write const char *p and the compiler will catch the mistake for you.

The <string.h> toolbox

toolbox.c
#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[32] = "Hello";          /* room to grow      */

    strcat(buf, ", world");          /* append (must fit!) */
    printf("%s (len %zu)\n", buf, strlen(buf));

    char copy[32];
    strcpy(copy, buf);               /* copies incl. \0   */

    printf("strcmp(\"apple\",\"banana\") = %d\n",
           strcmp("apple", "banana"));
    printf("copy equals buf? %s\n",
           strcmp(copy, buf) == 0 ? "yes" : "no");
    return 0;
}
terminal
$ gcc toolbox.c -o toolbox && ./toolbox
Hello, world (len 12)
strcmp("apple","banana") = -1
copy equals buf? yes
# strcmp guarantees only the SIGN: any negative value is legal
⚠️

strcmp does not return a boolean! It returns negative / zero / positive for less / equal / greater (think: "a minus b"). So if (strcmp(a, b)) is true when the strings differ — the exact opposite of what it looks like. Always write strcmp(a, b) == 0 for equality.

Buffer overflows: a 40-year-old wound

strcpy and strcat copy until they find '\0' — they never check whether the destination is big enough. Copy 20 bytes into a 8-byte buffer and you overwrite whatever lives next door: other variables, or the function's return address. That's not just a bug; overwriting the return address with attacker-chosen bytes is the classic security exploit.

🤔

The 1988 Morris worm — the first internet worm, which took down ~10% of the internet — spread partly through a buffer overflow in a string routine (gets). gets was finally removed from the C standard in C11, the only function ever expelled. Use bounded functions: snprintf, fgets, strncat.

🧠 Checkpoint: When does if (strcmp(a, b)) take the branch?

  • When the strings are equal
  • When the strings differ
  • When a is longer than b
  • Never — it doesn’t compile
Show answer

When the strings differ — strcmp returns 0 (falsy!) for equal strings and nonzero otherwise. The naked-strcmp condition reads like "if equal" but means "if different" — a beloved interview trap.

This spot has an interactive editor widget — open the interactive lesson to play with it.

One string is a 1-D char array — so what's an array of strings? Time to stack arrays inside arrays.

▶ Practice this lesson interactively (with live gcc)