Arrays, Strings, and <string.h> Foot-Cannons¶
C strings are not a type. They are a convention: a pointer to a byte buffer terminated by a '\0'. The standard library has a whole family of functions to manipulate them, and about half of those functions are unsafe by default — they were designed in the 1970s for programs that fit in 64 KB of RAM and had no adversarial input. This file makes you fluent in what’s safe, what’s not, and what to reach for instead.
The ML-engineer angle: you’ll be parsing config files, tokenizing input, formatting output, and shuttling strings between C and Python (via ctypes or CFFI). Every one of these operations has a safe form and an unsafe form, and the compiler will happily let you write the unsafe form.
Arrays Decay to Pointers (the single most confusing C fact)¶
An array is not a pointer. But in almost every expression, an array decays to a pointer to its first element. Function parameters especially: writing void f(int a[10]) is exactly the same as void f(int *a). The [10] is documentation, not a check.
void f(int a[10]) { printf("%zu\n", sizeof(a)); } // prints 8, not 40.
// a is a pointer, sizeof(ptr)=8.
int main(void) {
int arr[10];
printf("%zu\n", sizeof(arr)); // prints 40 (10 * 4 bytes). Real array.
f(arr); // arr decays to int* here.
}
Consequences you must internalize:
Inside a function that takes
T arr[], you cannot get the length fromsizeof. Pass asize_t nalongside every array parameter.&arr(address of array) is a different type fromarr(decayed pointer).&arrhas typeint(*)[10], useful in rare cases;arrhas typeint*. Both point to the same memory but with different pointer arithmetic.You cannot copy an array with
=.int a[10] = b;is a compile error. Usememcpyor a loop.
C99 gave us VLAs (variable-length arrays: int arr[n]; where n is a runtime value). They’re allocated on the stack, so a large n blows your stack. C11 made VLAs optional; C23 keeps them optional. Do not use VLAs. Allocate with malloc for runtime-sized buffers. This is Linux kernel policy, and it’s the pragmatic default.
The <string.h> Foot-Cannons¶
Here are the standard functions, ranked by how likely they are to hurt you:
Do Not Use¶
Function |
Why it’s dangerous |
|---|---|
|
No bounds check. Removed from C in C11. If you see it in old code, delete on sight. |
|
No length limit. Overflows if |
|
Same problem, plus requires walking |
|
No bounds check. Same as |
|
No width limit → overflow. Use |
Use With Care (they have footguns)¶
Function |
Footgun |
|---|---|
|
Does not guarantee null-termination. If |
|
The |
|
Not thread-safe — uses a static internal state. Also destructively modifies the input string. |
|
No error signaling. |
Safe Defaults¶
Function |
Why it’s the good one |
|---|---|
|
Bounded. Always null-terminates (when |
|
Simple, fast, no null-termination concerns. Requires you to know sizes. |
|
Like |
|
Reentrant version of |
|
Robust integer parsing with error detection via |
|
Bounded, always null-terminates, returns intended length. Not in the C standard but ubiquitous now. |
The One Idiom That Replaces Half of <string.h>¶
Whenever you’d reach for strcpy, strcat, or sprintf, reach for snprintf instead:
char path[PATH_MAX];
int r = snprintf(path, sizeof(path), "%s/%s.log", dir, name);
if (r < 0 || (size_t)r >= sizeof(path)) {
// handle error or truncation
return -1;
}
// path is guaranteed null-terminated here.
Memorize this pattern. It’s your workhorse for string building. Ninety percent of the buffer-overflow CVEs in C code from the last 30 years would not exist if this had been the default.
strncpy — Why It’s the Wrong Tool for Almost Every Job¶
People reach for strncpy thinking “the n makes it safe.” It does not do what you think:
char dst[8];
strncpy(dst, "hello world", sizeof(dst));
// dst now contains 'h','e','l','l','o',' ','w','o' — NO null terminator.
// If you then printf("%s", dst) → reads past the buffer → UB.
And if the source is shorter:
char dst[1024];
strncpy(dst, "hi", sizeof(dst));
// dst[0]='h', dst[1]='i', dst[2..1023] all set to '\0'.
// You just wrote 1024 bytes for a 2-char string.
strncpy was designed for a specific use case (fixed-size records in old Unix, padded with nulls). It is not a safe strcpy. Reach for snprintf or strlcpy instead.
strtok_r — The Reentrant Tokenizer¶
strtok fails in threaded code because it stashes the parse state in a static variable. strtok_r (POSIX; equivalent strtok_s in C11 Annex K if your compiler supports it) takes an explicit state pointer:
char text[] = "one,two,three";
char *saveptr = NULL;
char *tok = strtok_r(text, ",", &saveptr);
while (tok) {
puts(tok);
tok = strtok_r(NULL, ",", &saveptr);
}
// Note: strtok_r modifies `text` in place, replacing delimiters with '\0'.
// The input must be mutable — it cannot be a string literal.
When writing your own tokenizer for anything nontrivial, seriously consider not using strtok at all — write a small strsplit that returns a char ** array. You’ll implement one in Project 1 of this phase.
What Most People Get Wrong About Strings¶
They conflate “has an n” with “safe.” The C99-era strn* family is not the safe family; strncpy and strncat both have footguns worse than their unbounded siblings in some cases. The real safe family is: snprintf, memcpy/memmove, strlcpy/strlcat where available, and hand-rolled bounded copies where they aren’t. If your project can require POSIX, strtok_r, strdup, getline (bounded stdin reader) are your friends.
The second thing: they treat the buffer size and the string length as the same thing. They are not. sizeof(buf) includes the null terminator; strlen(s) does not. Off-by-one errors here account for a lot of embarrassing crashes. Rule: snprintf takes the buffer size (including null), returns the string length (excluding null). Memorize the asymmetry.
Exercises¶
Write
size_t safe_strlcpy(char *dst, const char *src, size_t dsz);from scratch. Test against edge cases: empty src, dsz==0, dsz==1, src longer than dsz.Rewrite a use of
sprintfin an existing codebase (your own or an open-source project) assnprintfwith truncation handling.Write a
char **strsplit(const char *s, char delim, size_t *out_n);that allocates. Free every allocation. Run under ASan. Do not usestrtok.Read the man page for
snprintfend to end.man 3 snprintf. Yes, the whole thing.
Return to README.md · Next: 03_structs_unions_bitfields.md