C / STRINGS AND BUFFER SAFETY
strlen, strcpy and the functions that trust you
Predict exactly how many bytes strcpy writes, check a destination's capacity before calling it, and know why strlen costs a full pass over the string.
What you will learn
- Read the strlen and strcpy signatures and see that no parameter describes the destination
- Compute the cost of a copy as strlen(src) + 1, terminator included
- Guard strcpy with sizeof taken where the array is declared, never from a char *
- Hoist strlen out of loop conditions so a linear scan does not become quadratic
Understanding strlen, strcpy and the functions that trust you
Look at the two declarations: size_t strlen(const char *s) and char *strcpy(char *dst, const char *src). Three pointers and not one size. A pointer value is an address and nothing more, and it does not carry the extent of the object it points into, so the only boundary either function can perceive is a NUL byte in the data it reads: strlen stops at the one in s, strcpy stops at the one in src. Neither has any way to ask how much room dst has, which is why that question is answered by you or by nobody.
The number that matters is strlen(src) + 1, because strcpy copies the characters and then the terminator: a five-character source needs six bytes. That single extra byte is behind most strcpy accidents, since a buffer sized to the length rather than length plus one gets its last write landing one position past the end. Nothing at compile time contradicts you either: sizeof reports an array's capacity only where the array declaration is in scope, and inside a function whose parameter is char *s it quietly reports the size of a pointer instead, so a check written that way approves every string that fits in eight bytes.
Keeping the length as the position of the first NUL has a running cost as well. strlen must read every byte, so it is O(n) and remembers nothing between calls; put it in a loop condition and you re-walk the whole string on each iteration, turning a linear pass into a quadratic one. Both halves of the lesson point at the same habit: derive the byte count once with strlen, compare it against a capacity you genuinely know, and carry that capacity next to the pointer whenever the array declaration is not visible at the point of the copy.
<stdio.h>
<string.h>
int main(void)
{
char src[] = "portmanteau";
char dst[16];
printf("strlen(src) = %zu\n", strlen(src));
printf("sizeof src = %zu\n", sizeof src);
printf("sizeof dst = %zu\n", sizeof dst);
/* strcpy will write strlen(src) + 1 bytes and cannot verify that they
fit, so the check happens here, at the only place that knows. */
if (strlen(src) + 1 <= sizeof dst) {
char *ret = strcpy(dst, src);
printf("wrote %zu bytes, ret == dst: %d\n", strlen(src) + 1, ret == dst);
printf("dst = [%s]\n", dst);
} else {
printf("dst holds %zu bytes, %zu needed: refused\n",
sizeof dst, strlen(src) + 1);
}
return 0;
}
strlen and strcpy see only addresses and the source's NUL byte, so the destination's capacity is knowledge that exists nowhere except in the caller.
Worked examples
Where the capacity goes missing
Shows that an array's size is known in the function that declares it and lost in any function that receives it as a pointer.
<stdio.h>
<string.h>
static void show(const char *s)
{
printf("inside show: strlen = %zu, sizeof s = %zu\n", strlen(s), sizeof s);
printf(" sizeof s == sizeof(char *)? %d\n",
sizeof s == sizeof(char *));
}
int main(void)
{
char buf[32] = "hi";
printf("inside main: strlen = %zu, sizeof buf = %zu\n", strlen(buf), sizeof buf);
show(buf);
return 0;
}
Example explained
Line 1In main the declaration char buf[32] is in scope, so sizeof buf is the whole 32-byte array.
Line 2Passing buf to show converts it to an address; the 32 is not part of that value and does not travel with it.
Line 3sizeof s therefore measures a pointer, 8 bytes on a 64-bit build, so a guard written as strlen(s) < sizeof s protects nothing.
Line 4strlen keeps working in both places because the length is stored in the bytes themselves, and it finds the NUL after h and i.
The price of asking for the length
Counts the bytes actually read when strlen sits in a loop condition versus when its result is stored once.
<stddef.h>
<stdio.h>
static long inspected;
static size_t my_strlen(const char *s)
{
const char *p = s;
while (*p != '\0') {
inspected++;
p++;
}
inspected++; /* the terminator is read too */
return (size_t)(p - s);
}
int main(void)
{
char s[] = "0123456789";
size_t i, n;
inspected = 0;
for (i = 0; i < my_strlen(s); i++)
;
printf("strlen in the condition: %ld bytes read\n", inspected);
inspected = 0;
n = my_strlen(s);
for (i = 0; i < n; i++)
;
printf("strlen hoisted out: %ld bytes read\n", inspected);
return 0;
}
Example explained
Line 1my_strlen counts every byte it touches, so one call over a 10-character string reads 11 bytes: ten characters plus the NUL.
Line 2The first loop evaluates its condition 11 times, once per successful iteration plus the failing test, giving 11 x 11 = 121 bytes read for a loop with an empty body.
Line 3The second loop reads 11 bytes in total because the length is computed once into n, which is all the information the loop needed.
Line 4A real strlen is sometimes hoisted by the optimiser, but only when it can prove the string is untouched; a call that might modify the buffer, like this counter, stops that.
Exactly strlen(src) + 1 bytes, no more
Demonstrates that strcpy writes only up to the source's terminator and leaves the rest of the destination as it was.
<stdio.h>
<string.h>
int main(void)
{
char buf[8];
size_t i;
memset(buf, 'X', sizeof buf);
strcpy(buf, "ab");
printf("as a string: %s\n", buf);
printf("all 8 bytes: ");
for (i = 0; i < sizeof buf; i++)
putchar(buf[i] == '\0' ? '.' : buf[i]);
putchar('\n');
printf("strlen = %zu, sizeof = %zu\n", strlen(buf), sizeof buf);
return 0;
}
Example explained
Line 1memset gives all eight bytes a defined value first, so the untouched ones can be printed and inspected.
Line 2strcpy writes three bytes, a and b and the terminator, because the source's NUL is the only stopping point it has.
Line 3Bytes 3 through 7 still hold X: strcpy neither clears nor pads the remainder of the destination.
Line 4The dot inside ab.XXXXX is that terminator, and it is why strlen reports 2 while sizeof reports 8.
Important notes
strcpy returns dst, the pointer you already had; the position where it wrote the terminator is discarded, so getting it back costs another strlen or a length you tracked yourself.
strlen returns unsigned size_t, so strlen(a) - strlen(b) becomes an enormous positive value when b is the longer string; compare the two lengths with < rather than subtracting them.
Common mistakes
Sizing the destination with strlen(src) instead of strlen(src) + 1, as in malloc(strlen(src)) followed by strcpy: the terminator is written one byte past the end, usually damaging heap bookkeeping or a neighbouring variable and crashing much later somewhere unrelated.
Checking capacity with sizeof on a char * parameter: it measures the pointer, so on a 64-bit build the test waves through every string of seven characters or fewer and rejects nothing that matters. A compiler warning may catch the constant-size case but cannot see a pointer whose target size is only known at run time.
Calling strlen(s) in the condition of a loop over an unchanging string: the code is correct but re-scans the whole string every iteration, so a function that feels instant on 100 bytes takes minutes on a few megabytes.
Try it yourself
Change, predict, then run
Declare char dst[8] and try to copy first "cat" and then "hippopotamus" into it, guarding each strcpy with an if that compares strlen(src) + 1 against sizeof dst. Print the required and available byte counts in both cases so you can see which copy is refused and by how much.
Open the C workspaceCheck your understanding
Why can no implementation of strcpy, however carefully written, detect on its own that the destination is too small?
- Because strcpy cannot know the destination's length until the destination has been NUL-terminated.
- Because the destination is not declared const, so the compiler discards its size information.
- Because a char * value is only an address, and the size of the object it points into is not part of that value.
- Because strcpy is written in assembly in most C libraries, so it has no access to C type information.
Show answer
The arguments give strcpy an address and a type but no extent, so dst's capacity simply is not derivable inside the function; that is why every size-aware alternative takes the room available as an extra parameter. The first option confuses length with capacity: even a dst already holding a terminated string would only reveal where its current contents stop, not how many bytes were reserved for it.