Intuition What this page is
On the parent note you learned the rules . Here we stress-test every rule against every case : short source, exact-fit source, oversized source, empty source, size = 0, a real-world path-join, and an exam trap. By the end you will have watched each function behave at every boundary — no scenario left unseen.
Before we start, one tiny picture that every example leans on. A char array of capacity C is just C numbered boxes. A valid C string uses one box for the invisible end-marker '\0' (the "knot"). So the most visible characters you can store is C − 1 . See C strings and the null terminator .
Every case a copy function can hit is one of these cells. Each column is a length relationship between the source text and the destination capacity C ; each degenerate row is an edge input.
Cell
Situation
What must happen
Which example
A
src shorter than C − 1
copies fully + terminates; spare room
Ex 1
B
src length exactly C − 1
just fits, terminates, no truncation
Ex 2
C
src length exactly C
the trap — strncpy drops the knot
Ex 3
D
src much longer than C
truncation, detect it
Ex 4
E
src empty ("")
writes only '\0'
Ex 5
F
size = 0 passed
function must touch nothing
Ex 6
G
real-world: build a path, detect overflow
snprintf + return check
Ex 7
H
exam twist: chained copies, off-by-one
reason about the knot
Ex 8
We use char buf[8] (C = 8 ) almost everywhere, so index 7 is the last box and C − 1 = 7 is our content cap.
char buf [ 8 ];
strncpy (buf, "hi" , sizeof (buf)); // src = "hi", 2 chars
Forecast: guess the 8 bytes of buf before reading on. Where does the '\0' land?
Step 1 — copy the 2 real characters. 'h' → box 0, 'i' → box 1.
Why this step? strncpy copies from src until either src runs out or n bytes are done. src runs out first (2 < 8).
Step 2 — pad the rest with '\0'. Boxes 2–7 all become '\0'.
Why this step? This is strncpy's special short-source behaviour: it fills the entire n with zeros. That is why here we get lucky — a valid, terminated string with no manual fix.
Result bytes: 'h' 'i' '\0' '\0' '\0' '\0' '\0' '\0'.
Verify: strlen(buf) walks boxes until the first '\0' → stops at box 2 → length 2 . printf("%s", buf) prints hi. Terminated, safe. See sizeof vs strlen for why strlen counts to the knot, not the capacity.
char buf [ 8 ];
strncpy (buf, "1234567" , sizeof (buf)); // 7 chars, C-1 = 7
Forecast: does box 7 hold a real character or the '\0'?
Step 1 — copy all 7 characters. '1'..'7' → boxes 0–6.
Why this step? 7 chars is exactly n-1, so all fit inside the first 7 boxes with box 7 still free.
Step 2 — one padding byte. Only box 7 remains, so strncpy writes '\0' there.
Why this step? The source (7) is still shorter than n (8), so padding kicks in for the single leftover box. Result is terminated by luck again — the source happened to be C − 1 , not C .
Result: "1234567" + '\0', 8 bytes exactly.
Verify: strlen(buf) == 7, sizeof(buf) == 8. This is the largest source that strncpy terminates on its own. One character more (Ex 3) breaks it.
char buf [ 8 ];
strncpy (buf, "01234567" , sizeof (buf)); // 8 chars, exactly C
Forecast: how many '\0' bytes are in buf after this line? (Answer may shock you.)
Step 1 — copy exactly n = 8 characters. '0'..'7' fill boxes 0–7. That is all 8 boxes used .
Why this step? strncpy copies at most n ; here src supplies 8 and n is 8, so it copies all 8 and stops. There is no leftover box.
Step 2 — no padding, so NO '\0'. Because every one of the n boxes got a real character, the padding rule never fires.
Why this step matters: buf is now not a string — it has no end-marker. strlen(buf) walks off box 7 into neighbouring memory (undefined behaviour, see Memory safety and undefined behavior and Buffer overflow and stack smashing ).
Step 3 — the mandatory fix.
buf [ sizeof (buf) - 1 ] = ' \0 ' ; // buf[7] = '\0'
Why this step? We sacrifice box 7's '7' to plant the knot, turning garbage into a valid, truncated string "0123456".
Verify: before the fix, buf[7] == '7' (not '\0'). After the fix, strlen(buf) == 7 and the last char is '6'. The '7' was lost — that is the price of truncation, but the program is now safe.
char a [ 8 ], b [ 8 ], c [ 8 ];
strncpy (a, "0123456789" , sizeof (a)); // 10 chars
a [ 7 ] = ' \0 ' ; // manual fix
int rb = snprintf (b, sizeof (b), " %s " , "0123456789" );
size_t rc = strlcpy (c, "0123456789" , sizeof (c)); // BSD/glibc>=2.38
Forecast: three buffers, three return values. Predict the contents of a, b, c and the numbers rb, rc.
Step 1 — strncpy path. Copies boxes 0–7 = "01234567", no knot; the manual a[7]='\0' gives "0123456".
Why? Same trap as Ex 3, and we already applied the fix.
Step 2 — snprintf path. It reserves the last byte for '\0' up front, so it writes 7 chars "0123456" + '\0', then returns the length it wanted : strlen("0123456789") == 10, so rb == 10.
Why this step? snprintf never lets content touch box 7 — that box is pre-booked for the knot. See Format strings and printf family .
Step 3 — strlcpy path. Copies up to size-1 = 7 chars "0123456", always terminates, returns strlen(src) == 10, so rc == 10.
Why? Same "always tie the knot" contract as snprintf, but no format string.
Verify — all three destinations are identical strings "0123456" (7 chars):
strlen(a) == strlen(b) == strlen(c) == 7.
Truncation detection: rb (10) >= sizeof(b) (8) ✔ and rc (10) >= sizeof(c) (8) ✔.
strncpy gives no built-in truncation flag — that is exactly why it needs babysitting.
char buf [ 8 ];
strlcpy (buf, "" , sizeof (buf)); // src = "" (0 visible chars)
Forecast: how many bytes get written and what does strlcpy return?
Step 1 — copy zero characters. There is nothing before src's '\0', so no real char is copied.
Why? The copy loop ends immediately on the first byte, which is already '\0'.
Step 2 — terminate. strlcpy still writes '\0' into box 0 (its "always terminate" promise, since size = 8 > 0).
Why this step? Even an empty result must be a valid string; box 0 must hold the knot so strlen reads 0 .
Verify: strlen(buf) == 0, return value == strlen("") == 0. And 0 >= 8 is false → no truncation, correctly.
char buf [ 8 ];
buf [ 0 ] = 'X' ; // sentinel we can watch
int r = snprintf (buf, 0 , "hello" ); // size = 0!
Forecast: does buf[0] change? What is r?
Step 1 — with size = 0, write nothing. The contract says "at most size bytes including the '\0'". With size = 0 there is not even room for the knot, so snprintf touches no memory .
Why this step? This is the guard that lets you call snprintf(NULL, 0, fmt, ...) first just to measure the needed length — a very common two-pass trick.
Step 2 — still report the would-be length. It returns strlen("hello") == 5.
Why? The return value is independent of how much fit; it always reports what a full write would need.
Verify: buf[0] is still 'X' (untouched), and r == 5. Note: strncpy(dest, src, 0) also copies nothing and, dangerously, terminates nothing — never assume n=0 gives you a valid string.
A program builds a filename "<dir>/<name>" into a fixed buffer and must refuse to proceed if it would truncate.
char path [ 16 ];
int r = snprintf (path, sizeof (path), " %s / %s " , "logs" , "output.txt" );
if (r >= ( int ) sizeof (path)) {
/* truncated — handle the error, do NOT use path */
}
Forecast: "logs/output.txt" — count its characters. Does it fit in 16 boxes? What is r, and does the if fire?
Step 1 — compute the intended text. "logs" (4) + "/" (1) + "output.txt" (10) = 15 visible chars .
Why this step? We need the content length to reason about the knot: 15 content chars need 16 bytes total (15 + 1). See C strings and the null terminator .
Step 2 — does it fit? Capacity is 16, needed is 16 → it fits exactly . snprintf writes all 15 chars into boxes 0–14 and '\0' into box 15.
Why? snprintf's cap is size including the knot; 15 + 1 = 16 = size , the tightest possible fit.
Step 3 — the return value and the guard. r == 15 (would-be length, excluding '\0'). The test r >= sizeof(path) is 15 >= 16 → false , so the if does not fire: no truncation, use path.
Why this step? This is the professional pattern — build, then compare return against capacity to catch overflow before trusting the result.
Verify: strlen(path) == 15; path equals "logs/output.txt"; 15 >= 16 is false. If the directory had one more character ("logs2"), needed would be 16 content + 1 = 17 > 16, so r == 16, 16 >= 16 true, guard fires — try that mentally as a mini-drill.
Exam prompt: "After the following runs, what does printf("%s", buf) print, and why is line 3 essential?"
char buf [ 5 ];
strncpy (buf, "abcde" , sizeof (buf)); // 1: 5 chars into C=5
buf [ sizeof (buf) - 1 ] = ' \0 ' ; // 2
strncpy (buf, "ab" , sizeof (buf)); // 3
Forecast: don't compute yet — guess the final visible string.
Step 1 — line 1 is Cell C again. 5 chars into 5 boxes = "abcde", no '\0' (trap).
Why? Source length equals n, padding never fires. (This is exactly Ex 3 at C = 5 .)
Step 2 — line 2 plants the knot. buf[4] = '\0' → "abcd" (the 'e' is overwritten).
Why? Without it, buf is not a string. After it, strlen == 4.
Step 3 — line 3 overwrites with a short source. "ab" is 2 chars < n = 5, so strncpy copies 'a','b' then pads boxes 2,3,4 with '\0' .
Why this matters: the padding wipes the leftover 'c','d' from before — a short source cleans up because it zero-fills to n. So the final buffer is fully re-terminated even without another manual fix.
Verify: final bytes 'a' 'b' '\0' '\0' '\0'; strlen(buf) == 2; printf prints ab. Line 2 is essential only for the intermediate state — but if the trailing garbage from line 1 were read by any code between lines 1 and 3, that would be undefined behaviour, which is why you never leave a strncpy un-terminated even momentarily.
Recall One-line summary of every cell
Short src pads safely; exact-C-1 fits and terminates; exact-C is the trap (strncpy drops the knot); oversized truncates (detect with r >= size); empty writes just a knot; size=0 touches nothing; real code compares the return against capacity; and a short source re-copy zero-fills away old garbage.
Which cell is the one that silently produces an unterminated string? Cell C — source length exactly equals capacity C , so strncpy copies all C bytes and adds no '\0'.
In Ex 7, why does the guard r >= sizeof(path) NOT fire? The intended text is 15 chars needing 16 bytes, capacity is 16, so it fits exactly; r == 15 and 15 >= 16 is false.
In Ex 8, why does line 3 restore termination without a manual fix? "ab" is shorter than n, so strncpy pads boxes 2–4 with '\0', cleaning up and terminating automatically.
Parent · Safe alternatives
C strings and the null terminator
Buffer overflow and stack smashing
strcpy and sprintf (unsafe)
sizeof vs strlen
Format strings and printf family
Memory safety and undefined behavior