5.1.20 · D3C Programming

Worked examples — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)

2,812 words13 min readBack to topic

The scenario matrix

Before any example, let us list every distinct situation a C string can be in. If a bug or a surprise exists, it lives in one of these rows.

Cell Case class What is special Covered by
A Normal string, room to spare length + '\0' easily fits buffer Ex 1
B Buffer exactly full len + 1 == size (the boundary) Ex 2
C Off-by-one overflow len + 1 > size by exactly one byte Ex 3
D Empty / degenerate string length , first byte is '\0' Ex 4
E Missing terminator array has no '\0' at all → runaway scan Ex 5
F Concatenation overflow strlen(dst)+strlen(src)+1 > size Ex 6
G Limiting / cost behaviour repeated strcat → quadratic time Ex 7
H sprintf output longer than buffer formatted text overflows silently Ex 8
I sizeof trap across a function boundary char * param vs real array Ex 9
J Bounded snprintf truncation (the safe fix) output too long but no overflow Ex 10

Two colour conventions used in every figure below:


Cell A — normal string with room

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Count src's bytes. "dog" is d o g '\0' = bytes. Why this step? Because the copy must move all bytes including the terminator — see the coral box in the figure.
  2. Copy into dst[8]. We write bytes into an -byte array. , so no overflow. Why this step? Cell A is defined by "room to spare" — this is the safe baseline against which every later bug is measured.
  3. my_strlen(dst) scans d(1) o(2) g(3) '\0'(stop) and returns . Why this step? Length counts characters before '\0', so the count is , not .
Recall Verify

Reveal ::: strlen("dog") == 3, copy needs bytes, ✓ no overflow.


Cell B — buffer exactly full (the boundary)

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Add the terminator. "cat" = c a t '\0' = bytes. Why this step? Every string literal silently appends '\0' — you must budget for it.
  2. Fit into a[4]. bytes into bytes: exactly full, len + 1 == size. Why this step? This is the boundary case — legal, but there is zero slack. One more character and we are in Cell C.
  3. strlen(a) scans c(1) a(2) t(3) '\0' → returns .
Recall Verify

Reveal ::: strlen("cat") == 3 and buffer size (exact fit) ✓.


Cell C — off-by-one overflow

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Count what must be written. "Hello" = H e l l o '\0' = bytes. Why this step? The five letters are visible; the sixth byte ('\0') is invisible but mandatory.
  2. Compare to the buffer. . strcpy writes byte index past the array's last valid index . Why this step? Cell C is exactly len + 1 > size by one — the most common real bug in C.
  3. What C does: nothing to stop you. The dashed coral box in the figure (the neighbour's byte) gets a written into it → buffer overflow, undefined behaviour. Why this step? There is no bounds check (Buffer Overflow & Memory Safety); the corruption may crash now, later, or be exploited.
Recall Verify

Reveal ::: needed bytes , buffer , overflow amount byte ✓.


Cell D — the empty / degenerate string

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Layout of "". The first byte is already '\0': \0 ? ? ?. Why this step? The empty string is not "no bytes" — it is one byte, the terminator, sitting at index .
  2. strlen(e) looks at e[0], sees '\0' immediately, stops with counter . Why this step? This is the degenerate limit of the counting loop: it never runs a single increment.
  3. strcpy(e, "") would copy exactly one byte (the '\0') — always safe, even into a -byte buffer.
Recall Verify

Reveal ::: strlen("") == 0 ✓; it still occupies byte (the terminator).


Cell E — missing terminator (runaway scan)

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Spot the trap. {'a','b','c'} fills all slots. There is no '\0' anywhere. Why this step? Curly-brace init does not append a terminator when the array is exactly full — unlike a string literal.
  2. Run strlen. It counts a(1) b(2) c(3) then reads bad[3]outside the array — and keeps going, counting whatever random bytes lie beyond until it accidentally meets a . Why this step? Without the stop signal the loop cannot terminate at ; it walks into your neighbour's memory (the dashed boxes).
  3. Result: undefined — could print , , or crash. There is no correct answer. Why this step? Cell E shows that "the letters are right" is not enough: the terminator is the contract, not the letters.
Recall Verify

Reveal ::: intended length , but declared bytes with no terminator ⇒ 3 + 1 = 4 > 3, so no '\0' fits ⇒ scan is undefined ✓.


Cell F — concatenation overflow

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Measure the pieces. strlen("Hi") = 2, strlen("there") = 5. Why this step? strcat glues them, so the final string is "Hithere" of length .
  2. Compute bytes needed. result length bytes. Why this step? The rule is strlen(dst) + strlen(src) + 1 — the is the single joined terminator.
  3. Compare to buffer. ⇒ overflow by bytes (dashed boxes in the figure). Why this step? strcat first walks to dst's '\0' at index , then copies t h e r e '\0' starting there — indices , but valid indices stop at .
Recall Verify

Reveal ::: 2 + 5 + 1 = 8 needed, buffer , overflow bytes ✓; strlen("Hithere") == 7.


Cell G — limiting cost behaviour (quadratic strcat)

Figure — String handling — char arrays, null terminator, strcpy, strcat, strlen, sprintf (dangers)
  1. Recall strcat's hidden scan. Before appending, it walks dst from the start to its '\0'. That is every single time (Time Complexity). Why this step? The append itself is cheap ( char), but finding the end is not.
  2. Sum the scans. On iteration the current length is , so the scan costs about reads. Total: Why this step? Adding up uses the arithmetic-series formula — that is the "Shlemiel the painter" curve.
  3. Interpret. To append single characters we performed byte-reads — quadratic in , not linear. Why this step? This is the limiting behaviour cell: correctness is fine, but the cost blows up as grows.
Recall Verify

Reveal ::: ✓.


Cell H — sprintf output longer than the buffer

  1. Format the number. %d turns 12345 into the characters '1' '2' '3' '4' '5'. Why this step? sprintf writes text, not a raw int — five digits (printf format specifiers).
  2. Add the terminator. sprintf always appends '\0' bytes written. Why this step? Even the safe count needs the ; here it makes the overflow worse.
  3. Compare to buffer. ⇒ overflow by bytes. sprintf never saw buf's size, so it cannot stop. Why this step? Cell H is the sprintf-specific danger: the size is simply not one of its arguments.
Recall Verify

Reveal ::: digits of , needed , buffer , overflow ✓.


Cell I — the sizeof trap across a function

  1. Array decays to pointer. When store is passed, the parameter buf is a char *, not an array (Arrays vs Pointers). Why this step? A pointer only holds an address — it forgot the array is bytes long.
  2. Evaluate sizeof buf. On a -bit machine, sizeof a char * is (pointer size), not . Why this step? sizeof on a real array gives its byte count; on a pointer it gives the pointer's own size.
  3. Consequence. snprintf is told the buffer is only bytes, so it truncates output at even though were available — a silent capacity bug (and would be catastrophic if the intent had been a plain sizeof-based strcpy). Why this step? Cell I shows the danger is not overflow but a wrong size number at a function boundary.
Recall Verify

Reveal ::: inside fill, sizeof buf == 8 (pointer), but real array sizeof store == 64 ✓ — they differ.


Cell J — the safe fix (snprintf truncation)

  1. Compute the would-be full length. "log_2024.txt" = characters. snprintf returns the length it would have written (excluding '\0'), so n == 12. Why this step? This return value is how you detect truncation: if n >= sizeof path, output was cut.
  2. What actually lands in path. snprintf writes at most size - 1 = 7 characters plus a guaranteed '\0': l o g _ 2 0 2 '\0'path == "log_202". Why this step? Cell J is the safe counterpart to Cell H: instead of overflowing, snprintf truncates and always terminates.
  3. Check for truncation. Since , the caller knows the name was truncated and can react (allocate more, error out). Why this step? This is the whole point of the n-functions — a seat-belt that reports the crash it prevented.
Recall Verify

Reveal ::: full length of "log_2024.txt" (so n == 12); stored chars , giving "log_202" ✓.


Connections

  • Parent topic — the tools these examples exercise.
  • Buffer Overflow & Memory Safety — Cells C, F, H are all overflows.
  • Undefined Behaviour in C — why Cells C, E have no defined answer.
  • Arrays vs Pointers — Cell I, the sizeof trap.
  • Time Complexity — Cell G, the quadratic strcat.
  • printf format specifiers — Cells H, J, how %d/%s expand.