📚 The Standard Library
string.h: the null-terminated toolbox
▶ Open the interactive lesson — free, no signupPart 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
| function | one-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) / strrchr | pointer to first / last occurrence of char c, or NULL |
#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;
}$ 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:
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
| function | one-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,baab- 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.
#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;
}$ 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:
#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;
}$ 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.