The C Path — learn C, visually

📚 The Standard Library

string.h: the null-terminated toolbox

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

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

Part 2 showed you how C stores text; the standard library supplies the tools that work on it, and they all have sharp edges — mishandled text is behind some of the most famous security disasters in computing history, where one too-long input overwrites memory and an attacker walks in. In this lesson you'll copy, compare, search, and split text safely, starting with the trap everyone hits first: comparing two strings with == and silently getting the wrong answer.

C strings are just char arrays ending in '\0' (Part 2), and string.h is the toolbox for working with them. It's small, fast, and full of decades-old traps. Let's tour the whole thing, traps included.

The core five

functionone-liner
strlen(s)characters before the '\0' — an O(n) walk, not a stored field!
strcpy(dst, src)copy including '\0' — dst must be big enough, nothing checks
strcat(dst, src)append src at dst's '\0' — same "trust me" contract
strcmp(a, b)lexicographic compare: <0, 0, >0 — never if (a == b), that compares pointers!
strchr(s, c) / strrchrpointer to first / last occurrence of char c, or NULL
tour.c
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "hello, world";

    printf("strlen: %zu\n", strlen(s));
    printf("strcmp(\"abc\",\"abd\"): %s\n",
           strcmp("abc", "abd") < 0 ? "negative" : "other");

    const char *comma = strchr(s, ',');       /* first ',' */
    printf("strchr: found at index %td\n", comma - s);

    const char *sub = strstr(s, "world");     /* substring */
    printf("strstr: \"%s\"\n", sub);

    char line[] = "value\n";                  /* fgets-style */
    line[strcspn(line, "\n")] = '\0';         /* strip \n    */
    printf("stripped: [%s]\n", line);
    return 0;
}
terminal
$ gcc tour.c -o tour && ./tour
strlen: 12
strcmp("abc","abd"): negative
strchr: found at index 5
strstr: "world"
stripped: [value]

🧠 Checkpoint: Why is if (name == "alice") wrong for comparing strings?

  • It compares lengths only
  • It compares the two pointers, not the characters
  • String literals can’t be compared
  • It’s fine — this works
Show answer

It compares the two pointers, not the characters — == on pointers asks "same address?", not "same text?". Two identical strings at different addresses compare unequal (and identical literals may or may not be merged — implementation-defined). Use strcmp(name, "alice") == 0.

The strncpy trap

"Just use strncpy, it's the safe one" — beware. strncpy(dst, src, n) has a genuinely weird contract: if src is longer than n, it copies n bytes and does not write a '\0'. Your "string" is now an unterminated byte array, and the next strlen reads off the end into the void:

strncpy_trap.c — the missing terminator
char dst[8];
strncpy(dst, "a short fit", 8);
/* src has 11 chars: strncpy copies exactly 8 bytes,
   "a short " — and NO '\0'. dst is not a string!    */

printf("%s\n", dst);      /* 💀 UB: reads past dst[7] */

/* the ritual fix:                                    */
dst[sizeof dst - 1] = '\0';

/* the better tool — always terminated:               */
snprintf(dst, sizeof dst, "%s", "a short fit");
💀

strncpy does not guarantee termination. If you must use it, always follow with dst[n-1] = '\0';. Better: use snprintf(dst, sizeof dst, "%s", src), which always terminates and tells you (via its return value) if truncation happened. (strncat is saner — it always terminates — but its n counts what to append, not the buffer size.)

Search: strstr, strspn & friends

functionone-liner
strstr(hay, needle)pointer to first occurrence of substring, or NULL
strpbrk(s, set)pointer to the first char of s that's in set
strspn(s, set)length of the prefix made only of chars in set
strcspn(s, set)length of the prefix containing none of set — great for stripping fgets' newline: s[strcspn(s, "\n")] = 0;

strtok: useful, stateful, destructive

strtok(str, delims) splits a string into tokens — but it does two surprising things. It mutates your string, stamping '\0' over each delimiter; and it keeps hidden static state, which is why every call after the first passes NULL ("continue where you left off"). Step through it:

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

⚠️

Because of the hidden state, you can't interleave two strtok loops, use it on a string literal (mutating one is UB!), or call it from multiple threads. C11's strtok_s / POSIX strtok_r take the state as an explicit parameter and fix all three.

🧠 Checkpoint: After a full strtok pass over char s[] = "a,b"; what does printf("%s", s) print?

  • a,b
  • a
  • ab
  • nothing — s is freed
Show answer

a — strtok replaced the "," with \0, so the array now holds "a\0b\0…". %s stops at the first \0 and prints just "a". The original string is gone — copy it first if you still need it.

The mem family: raw bytes, no '\0' involved

These work on arbitrary memory and take explicit lengths — they never look for a terminator: memset(p, byte, n) fills, memcmp(a, b, n) compares, memchr(p, byte, n) finds a byte, and memcpy(dst, src, n) copies… with one famous restriction. If the regions overlap, memcpy is undefined behavior — a forward-copying implementation clobbers source bytes before reading them:

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

memmove handles overlap correctly (it copies backwards when needed, as if via a temporary buffer). Rule of thumb: same array, or any doubt at all → memmove.

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

int main(void) {
    char a[] = "ABCDEF";
    memmove(a + 2, a, 4);        /* overlap: memmove is safe */
    printf("after memmove: %s\n", a);

    char b[8];
    memset(b, '-', 7);           /* fill 7 bytes with '-'    */
    b[7] = '\0';
    printf("after memset : %s\n", b);

    printf("memcmp: %d\n", memcmp("abc", "abd", 3) < 0);
    return 0;
}
terminal
$ gcc shift.c -o shift && ./shift
after memmove: ABABCD
after memset : -------
memcmp: 1

🧠 Checkpoint: When must you use memmove instead of memcpy?

  • When copying more than 4 KB
  • When source and destination might overlap
  • When copying structs
  • Always — memcpy is deprecated
Show answer

When source and destination might overlap — memcpy is allowed to assume no overlap (and is often faster because of it); overlapping regions make it UB. memmove behaves as if it copies through a temporary buffer, so shifting data within one array is its home turf.

snprintf: the safe string builder

Building strings from pieces with strcpy+strcat is verbose and risky. snprintf does it in one bounded call, always terminates, and returns the length it wanted to write — so you can detect truncation:

builder.c
#include <stdio.h>

int main(void) {
    char url[32];
    int need = snprintf(url, sizeof url,
                        "https://%s:%d/%s", "api.example.com", 443, "v2/users");

    if (need >= (int)sizeof url)
        printf("truncated! needed %d bytes, had %zu\n", need, sizeof url);

    printf("url = %s\n", url);   /* always '\0'-terminated  */
    return 0;
}
terminal
$ gcc builder.c -o builder && ./builder
truncated! needed 36 bytes, had 32
url = https://api.example.com:443/v2/
🎉

strdup graduates: strdup(s) — malloc a copy of a string — lived in POSIX for decades while portable C had to hand-roll it. C23 finally adopted strdup and strndup into the standard. Remember the copy is malloc'd: you free it.

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

Strings and bytes handled — time for numbers and clocks: math.h and time.h.

▶ Practice this lesson interactively (with live gcc)