The C Path — learn C, visuallyprintable cheatsheet

string.h — the null-terminated toolbox

Facts per ISO C17 · C23 additions marked · UB = undefined behavior, ID = implementation-defined

Styled for paper — hit Ctrl+P and pin it above your desk.

C strings are char arrays ending in '\0' (strings lesson). The str* functions stop at that terminator; the mem* functions take explicit lengths and never look for one. Passing NULL to any of them is UB — none of these functions checks.

Copy & fill

signaturedoesthe gotchalearn
void *memcpy(void *restrict dst, const void *restrict src, size_t n)copy n raw bytesoverlapping regions are UB — a forward copy clobbers source bytes before reading them. Any doubt → memmove→ lesson
void *memmove(void *dst, const void *src, size_t n)copy n bytes, overlap-safebehaves as if via a temporary buffer; the safe default for shifting data within one array→ lesson
char *strcpy(char *restrict dst, const char *restrict src)copy including '\0'dst must be big enough — nothing checks; overflow is UB→ lesson
char *strncpy(char *restrict dst, const char *restrict src, size_t n)copy at most n bytesdoes not write '\0' if src has ≥ n chars — your "string" is now an unterminated byte array. Also zero-fills the remainder when src is shorter (slow for big buffers)→ lesson
void *memset(void *s, int c, size_t n)fill n bytes with (unsigned char)cfine for zeroing byte-wise; note the standard doesn't promise all-bits-zero equals 0.0 or NULL for floats/pointers (it does on every mainstream platform)
void *memccpy(void *restrict dst, const void *restrict src, int c, size_t n) C23copy up to n bytes, stop after byte creturns pointer just past c in dst, or NULL if c wasn't found — handy for bounded string building (long-time POSIX function)
void *memset_explicit(void *s, int c, size_t n) C23fill that can't be optimized awayuse for wiping secrets — a plain memset before free may be deleted by the optimizer
💀

strncpy does not guarantee termination. If you must use it, always follow with dst[n-1] = '\0';. Better: snprintf(dst, sizeof dst, "%s", src), which always terminates and tells you (via its return value) if truncation happened.

Concatenate

signaturedoesthe gotcha
char *strcat(char *restrict dst, const char *restrict src)append src at dst's '\0'same "trust me" contract as strcpy — no bounds. Also O(len(dst)) just to find the end: repeated strcat in a loop is accidentally quadratic
char *strncat(char *restrict dst, const char *restrict src, size_t n)append at most n chars, then '\0'saner than strncpy (always terminates) but n counts what to append, not the buffer size — it may write n+1 bytes past the current end

Compare

signaturedoesthe gotchalearn
int strcmp(const char *a, const char *b)lexicographic comparereturns <0, 0, or >0 — the contract is the sign, not −1/0/+1. Comparison is by unsigned char values. And never if (a == b) — that compares pointers, not text→ lesson
int strncmp(const char *a, const char *b, size_t n)compare at most n charsthe prefix-test tool: strncmp(s, "pre", 3) == 0
int memcmp(const void *a, const void *b, size_t n)compare n raw byteson whole structs it also compares padding bytes, which hold garbage — struct equality needs member-by-member code→ lesson
int strcoll(const char *a, const char *b)compare per current localefor sorting user-visible text (e.g. accented letters); slower than strcmp
size_t strxfrm(char *restrict dst, const char *restrict src, size_t n)transform so strcmp of results == strcoll of originalsfor sorting many strings: transform once, strcmp many times

Search

signaturedoesthe gotcha
char *strchr(const char *s, int c)first occurrence of char c, or NULLstrchr(s, '\0') legitimately finds the terminator; c is converted to char
char *strrchr(const char *s, int c)last occurrence, or NULLthe file-extension / basename tool: strrchr(path, '/')
void *memchr(const void *s, int c, size_t n)first byte c in n bytes, or NULLdoesn't stop at '\0' — for raw buffers
char *strstr(const char *hay, const char *needle)first occurrence of substring, or NULLempty needle matches at the start; case-sensitive (case-insensitive strcasestr is a GNU extension, not ISO C)
char *strpbrk(const char *s, const char *set)first char of s that's in set, or NULL"pointer to break" — think "find any of"
size_t strspn(const char *s, const char *set)length of prefix made only of chars in setreturns a length, not a pointer
size_t strcspn(const char *s, const char *set)length of prefix containing none of setgreat for stripping fgets' newline: s[strcspn(s, "\n")] = '\0';
char *strtok(char *restrict s, const char *restrict delims)split into tokenssee below — the most stateful function in the library
⚠️

strtok: useful, stateful, destructive. It mutates your string, stamping '\0' over each delimiter, and keeps hidden static state — that's why every call after the first passes NULL ("continue where you left off"). Consequences: you can't interleave two strtok loops, can't use it on a string literal (mutating one is UB), and can't call it from multiple threads. C11's strtok_s (optional Annex K) / POSIX strtok_r take the state as an explicit parameter and fix all three. Runs of delimiters are skipped — empty fields are invisible to strtok (bad for CSV).

Length & misc

signaturedoesthe gotchalearn
size_t strlen(const char *s)characters before the '\0'an O(n) walk, not a stored field — don't call it every loop iteration; and sizeof "hi" is 3 (terminator included) while strlen("hi") is 2→ lesson
char *strerror(int errnum)human-readable message for an errno valuemay return a pointer to a static buffer — not thread-safe; POSIX has strerror_r→ lesson
char *strdup(const char *s) C23malloc'd copy of sPOSIX classic, standard since C23. The copy is heap memory: you free it→ lesson
char *strndup(const char *s, size_t n) C23malloc'd copy of at most n charsunlike strncpy, the copy is always '\0'-terminated→ lesson

Safe-usage recipes

I want to…writewhy this way
copy with boundssnprintf(dst, sizeof dst, "%s", src);always terminates; return ≥ buffer size means truncated (return is the length it wanted to write)
use strncpy anywaystrncpy(dst, src, sizeof dst); dst[sizeof dst - 1] = '\0';the ritual terminator — never skip it
strip fgets' newlines[strcspn(s, "\n")] = '\0';works whether or not the newline is there (last line of a file)
test equalitystrcmp(a, b) == 0== compares addresses; identical literals may or may not share one — ID
test a prefixstrncmp(s, "https:", 6) == 0bounded, no scan of the whole string
test a suffixsize_t n = strlen(s); n >= 4 && strcmp(s + n - 4, ".txt") == 0check the length first or you index before the string
duplicate a stringchar *p = malloc(strlen(s) + 1); if (p) memcpy(p, s, strlen(s) + 1);portable pre-C23 strdup; the +1 copies the terminator. free(p) later
tokenize safelystrtok_r(s, ",", &save) (POSIX)reentrant; or hand-roll with strcspn/strspn if you need empty fields
append with boundssize_t len = strlen(dst); snprintf(dst + len, size - len, "%s", src);ensure len < size first; avoids strcat's silent overflow
zero a structmemset(&st, 0, sizeof st);or struct S st = {0}; which also zeros padding-independent members portably
shift data inside one arraymemmove(a + 2, a, 4);same array = overlap = memmove territory, never memcpy
wipe a password buffermemset_explicit(buf, 0, sizeof buf); C23pre-C23: volatile pointer tricks or platform calls (explicit_bzero)
💡

Mental model: str* = "walk until '\0'", strn* = "walk until '\0' or n (semantics vary per function!)", mem* = "exactly n bytes, no terminator involved". The n in strncpy, strncat and snprintf means a different thing in each — that inconsistency is decades old and not going away.