Exercises — Safe alternatives — strncpy, snprintf, strlcpy
Throughout, remember the single mental picture that governs everything: a buffer is a row of numbered boxes. char buf[8] is eight boxes, numbered 0 through 7. A C string is only a valid string when one of those boxes holds the byte '\0' (the "null terminator" — see C strings and the null terminator); that byte is the fence that tells every reader "the text ends here." Overflow means writing into a box that does not exist (box 8 and beyond).

Level 1 — Recognition
(Can you tell the functions apart by their contract alone?)
Exercise L1.1
Match each function to its behaviour at the boundary (when the source is too long to fit):
- (a) copies exactly
nbytes, leaves no'\0' - (b) writes at most
sizebytes total and always leaves a'\0' - (c) writes past the end of the buffer with no check at all
Functions: strcpy, strncpy, snprintf.
Recall Solution
strcpy→ (c). It only stops at the source's own'\0'; it never looks at the destination size. This is the unsafe function from strcpy and sprintf (unsafe).strncpy→ (a). Capped atn, but on a full/overflowing source it writes exactlynbytes and adds no terminator — the trap.snprintf→ (b). Total bytes written ≤size, and it reserves the last box for'\0', so the result is always a valid string whensize > 0.
Exercise L1.2
For each function, what does the return value mean?
strncpy, snprintf, strlcpy.
Recall Solution
strncpyreturnsdest(the pointer you passed in) — useless for detecting truncation.snprintfreturns the number of characters it would have written, excluding the'\0'. Truncation happened iffreturn >= size.strlcpyreturnsstrlen(src)— the full length it tried to copy. Truncation happened iffreturn >= size.
The pattern: snprintf and strlcpy both hand you a number you can compare against size. strncpy hands you nothing useful.
Level 2 — Application
(Now compute exact bytes. Draw the boxes.)
Exercise L2.1
char buf[6];
strncpy(buf, "abc", sizeof(buf));Write out all 6 bytes of buf. Is it a valid C string?
Recall Solution
sizeof(buf) is 6 (six boxes — see sizeof vs strlen for why sizeof gives the array's box count, not the text length). The source "abc" is 3 visible chars plus its own '\0' = 4 bytes, which is shorter than n = 6.
strncpy copies a b c \0, then pads the remaining boxes with '\0':
Valid string? Yes — box 3 holds a '\0', so strlen(buf) == 3. Safe here purely because the source was short.
Exercise L2.2
char buf[6];
strncpy(buf, "abcdef", sizeof(buf));Write out all 6 bytes. Is it a valid C string? What is the fix?
Recall Solution
"abcdef" is 6 visible chars. n = 6. strncpy copies exactly 6 bytes of content:
No '\0' anywhere — the source filled the entire byte budget, so there was no box left for a fence.
Valid? No. strlen(buf) would run off box 5 into adjacent memory (undefined behaviour — see Memory safety and undefined behavior).
Fix:
strncpy(buf, "abcdef", sizeof(buf) - 1); // cap at 5
buf[sizeof(buf) - 1] = '\0'; // box 5 = fenceNow buf = a b c d e '\0', giving the truncated string "abcde" (strlen == 5).
Exercise L2.3
char buf[6];
int r = snprintf(buf, sizeof(buf), "val=%d", 42);What is in buf? What is r? Was there truncation?
Recall Solution
"val=42" is 6 visible chars, needing 7 bytes (6 + the '\0'). Only 6 boxes exist, and snprintf always reserves the last box for '\0'. So it writes 5 content chars + terminator:
giving buf = "val=4".
r is the length it would have produced = strlen("val=42") = 6.
Truncation check: → true, truncation happened. (See Format strings and printf family for how %d expands.)
Level 3 — Analysis
(Subtle cases: zero, exact-fit, comparing tools on the same input.)
Exercise L3.1
char buf[8];
size_t r = strlcpy(buf, "0123456789", sizeof(buf));Give buf and r. Then compare: what would strncpy(buf, "0123456789", sizeof(buf)) leave instead?
Recall Solution
strlcpy caps content at size - 1 = 7 and always terminates:
so buf = "0123456". It returns strlen(src) = 10 (the full length it tried). → truncation detected, in one line, for free.
Contrast strncpy with the same call: it copies exactly 8 content bytes 0 1 2 3 4 5 6 7 with no terminator — an invalid string. That single difference (fence vs no fence) is the whole reason strlcpy is "the one strncpy should have been."
Exercise L3.2 (exact-fit boundary)
char buf[6];
int r = snprintf(buf, sizeof(buf), "%s", "hello");"hello" is exactly 5 chars. Does it fit? Give buf, r, and whether the truncation check fires.
Recall Solution
5 content chars + 1 terminator = 6 bytes, which is exactly sizeof(buf) = 6. Perfect fit:
buf = "hello". Return r = 5.
Truncation check: → false. No truncation — correct, because everything (including the fence) fit. This is the important boundary: r == size - 1 is the largest value that means "everything fit."
Exercise L3.3 (degenerate: size 0)
char buf[8];
buf[0] = 'X';
int r = snprintf(buf, 0, "hi");What happens to buf[0]? What is r?
Recall Solution
When size == 0, snprintf is forbidden from writing any byte at all — not even a '\0'. So buf[0] stays 'X'; the buffer is untouched.
But it still computes what it would have written and returns that: strlen("hi") = 2.
This is a common idiom — call snprintf(NULL, 0, fmt, ...) first to measure the needed length, then allocate exactly r + 1 bytes and call again. The measure-then-write pattern relies on this rule.
Level 4 — Synthesis
(Write correct code from scratch.)
Exercise L4.1
Write a function copy_safe(char *dst, size_t dstsize, const char *src) using only strncpy** that behaves like strlcpy`: never overflows, always terminates. (Return value not required.)
Recall Solution
void copy_safe(char *dst, size_t dstsize, const char *src) {
if (dstsize == 0) return; // no room even for a fence
strncpy(dst, src, dstsize - 1); // cap content at C-1
dst[dstsize - 1] = '\0'; // force the fence into the last box
}Why each line:
- The
dstsize == 0guard: with zero boxes we cannot even write a terminator; writingdst[-1]would be an out-of-bounds write (Buffer overflow and stack smashing). strncpy(..., dstsize - 1): reserves the last box, sostrncpycan never touch boxdstsize - 1.dst[dstsize - 1] = '\0': unconditionally places the fence. If the source was long, this truncates; if short, this box was already'\0'from padding — either way it's correct.
Exercise L4.2
Build the string "user=<name>" into char out[16], and set a flag truncated if the name didn't fit. Use snprintf.
Recall Solution
char out[16];
const char *name = "alexandra"; // 9 chars
int r = snprintf(out, sizeof(out), "user=%s", name);
int truncated = (r >= (int)sizeof(out));"user=alexandra" is 5 + 9 = 14 chars, needing 15 bytes. sizeof(out) = 16, so it fits with room to spare:
truncated = (14 >= 16) = false (0). Correct — no data lost.
Note the (int) cast: sizeof yields an unsigned size_t; comparing a signed r against it without the cast can misbehave if r were ever negative (an encoding error). Casting makes the comparison honest.
Level 5 — Mastery
(Combine everything, including portability and measurement idioms.)
Exercise L5.1
You must join two strings a = "hello" and b = "world" into char out[9] as "hello-world" (11 chars). Use the snprintf measure-then-detect idiom. What is r, what is in out, and how many bytes would a correct buffer need?
Recall Solution
char out[9];
int r = snprintf(out, sizeof(out), "%s-%s", "hello", "world");The full result "hello-world" is 5 + 1 + 5 = 11 chars, needing 12 bytes. Only 9 boxes exist, and snprintf reserves the last for '\0', so it writes 8 content chars + fence:
out = "hello-wo". Return r = 11 (the wanted length). Truncation: → true.
A correct buffer needs r + 1 = 12 bytes. This is the measure trick: r tells you exactly how big to make it next time.
Exercise L5.2 (portability + full pipeline)
Write portable code that copies src into a heap buffer of exactly the right size, terminated, without ever relying on strlcpy (which may be missing — glibc only shipped it in 2.38). Assume src and malloc succeed.
Recall Solution
size_t need = strlen(src) + 1; // +1 for the '\0' fence
char *dst = malloc(need);
memcpy(dst, src, need); // copy content AND the terminatorWhy this is bulletproof:
strlen(src)counts visible chars (stops atsrc's own'\0'), soneedis exactly content + fence.malloc(need)allocates precisely that many boxes — no overflow possible because the buffer was sized from the data.memcpy(dst, src, need)copiesneedbytes, and since boxneed - 1ofsrcis its'\0', the fence is copied too. No manual termination needed.- Uses only standard functions → portable everywhere, no
strlcpydependency. This is the "size the cup to the water" strategy — the inverse of "pour into a fixed cup," and it never truncates.
Exercise L5.3 (interaction quiz)
For char b[8] and source "1234567" (7 chars), fill this table — the exact b contents and return/validity for each function.
| Call | b bytes |
Valid string? | Return |
|---|---|---|---|
strncpy(b, "1234567", 8) |
? | ? | — |
snprintf(b, 8, "%s", "1234567") |
? | ? | ? |
strlcpy(b, "1234567", 8) |
? | ? | ? |
Recall Solution
Source "1234567" is 7 content chars, needing 8 bytes total (7 + fence). b has exactly 8 boxes — an exact fit.
| Call | b bytes |
Valid? | Return |
|---|---|---|---|
strncpy(b, "1234567", 8) |
1 2 3 4 5 6 7 \0 |
Yes | b (ptr) |
snprintf(b, 8, "%s", "1234567") |
1 2 3 4 5 6 7 \0 |
Yes | 7 |
strlcpy(b, "1234567", 8) |
1 2 3 4 5 6 7 \0 |
Yes | 7 |
The subtle point: here even strncpy produces a valid string! The source "1234567" is 7 chars; strncpy copies 7 content bytes and has 1 byte left, which it fills with the source's own '\0', then pads (nothing left to pad). It succeeds only because the content was n - 1 bytes, not n. Push the source to 8 chars and strncpy breaks while the other two stay safe. snprintf/strlcpy return 7; since , no truncation flagged — correct, everything fit.
Recall Master mnemonic recap
"n forgets the knot; printf and l always tie it." One extra argument — the destination size — is the whole safety story. The size answers "how big is the cup?" Never skip it. And the fence rule: content ≤ capacity − 1, always.
Connections
- Parent: Safe alternatives overview
- 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