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.
| signature | does | the gotcha | learn |
|---|---|---|---|
void *memcpy(void *restrict dst, const void *restrict src, size_t n) | copy n raw bytes | overlapping 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-safe | behaves 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 bytes | does 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)c | fine 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) C23 | copy up to n bytes, stop after byte c | returns 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) C23 | fill that can't be optimized away | use 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.
| signature | does | the 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 |
| signature | does | the gotcha | learn |
|---|---|---|---|
int strcmp(const char *a, const char *b) | lexicographic compare | returns <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 chars | the prefix-test tool: strncmp(s, "pre", 3) == 0 | |
int memcmp(const void *a, const void *b, size_t n) | compare n raw bytes | on 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 locale | for 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 originals | for sorting many strings: transform once, strcmp many times |
| signature | does | the gotcha |
|---|---|---|
char *strchr(const char *s, int c) | first occurrence of char c, or NULL | strchr(s, '\0') legitimately finds the terminator; c is converted to char |
char *strrchr(const char *s, int c) | last occurrence, or NULL | the file-extension / basename tool: strrchr(path, '/') |
void *memchr(const void *s, int c, size_t n) | first byte c in n bytes, or NULL | doesn't stop at '\0' — for raw buffers |
char *strstr(const char *hay, const char *needle) | first occurrence of substring, or NULL | empty 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 set | returns a length, not a pointer |
size_t strcspn(const char *s, const char *set) | length of prefix containing none of set | great for stripping fgets' newline: s[strcspn(s, "\n")] = '\0'; |
char *strtok(char *restrict s, const char *restrict delims) | split into tokens | see 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).
| signature | does | the gotcha | learn |
|---|---|---|---|
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 value | may return a pointer to a static buffer — not thread-safe; POSIX has strerror_r | → lesson |
char *strdup(const char *s) C23 | malloc'd copy of s | POSIX classic, standard since C23. The copy is heap memory: you free it | → lesson |
char *strndup(const char *s, size_t n) C23 | malloc'd copy of at most n chars | unlike strncpy, the copy is always '\0'-terminated | → lesson |
| I want to… | write | why this way |
|---|---|---|
| copy with bounds | snprintf(dst, sizeof dst, "%s", src); | always terminates; return ≥ buffer size means truncated (return is the length it wanted to write) |
| use strncpy anyway | strncpy(dst, src, sizeof dst); dst[sizeof dst - 1] = '\0'; | the ritual terminator — never skip it |
| strip fgets' newline | s[strcspn(s, "\n")] = '\0'; | works whether or not the newline is there (last line of a file) |
| test equality | strcmp(a, b) == 0 | == compares addresses; identical literals may or may not share one — ID |
| test a prefix | strncmp(s, "https:", 6) == 0 | bounded, no scan of the whole string |
| test a suffix | size_t n = strlen(s); n >= 4 && strcmp(s + n - 4, ".txt") == 0 | check the length first or you index before the string |
| duplicate a string | char *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 safely | strtok_r(s, ",", &save) (POSIX) | reentrant; or hand-roll with strcspn/strspn if you need empty fields |
| append with bounds | size_t len = strlen(dst); snprintf(dst + len, size - len, "%s", src); | ensure len < size first; avoids strcat's silent overflow |
| zero a struct | memset(&st, 0, sizeof st); | or struct S st = {0}; which also zeros padding-independent members portably |
| shift data inside one array | memmove(a + 2, a, 4); | same array = overlap = memmove territory, never memcpy |
| wipe a password buffer | memset_explicit(buf, 0, sizeof buf); C23 | pre-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.