From last week’s Lesson, the task is to code a safer, better version of the strcpy() function. The goals are to check buffer size, report an overflow or underflow (buffer is too big or too small), and potentially confirm whether data is overwritten. Such a program is often used as a test when applying for a C programming job.
My solution is the stringcopy() function with this prototype:
int stringcopy(char *dest, size_t dsize, const char *src, size_t ssize)
This function uses two arguments found in strcpy(): the address of the string to copy, pointer src, and the address of the buffer where the string is to be copied, pointer dest. Two additional arguments are the size of each buffer: dsize for the destination buffer’s size and ssize for the source buffer’s size. This approach parallels the many alternative, or “safe,” strcpy() function variations.
I’ve also added a return value to my stringcopy() function. The value is negative for an overflow, meaning that not every character is copied. It’s positive for an underflow, meaning that unused space (or characters) remain in the destination buffer. The return value is zero when both buffers match.
Here is my code:
2025_10_04-Lesson.c
#include <stdio.h> /* arguments determine buffer sizes return value: negative for overflow, chars cut positive for underflow, chars left in the buffer zero for a perfect copy, no wasted space */ int stringcopy(char *dest, size_t dsize, const char *src, size_t ssize) { int flow,x; /* test the buffer fit */ flow = dsize-ssize; /* if overflow, copy only dsize characters */ if( flow<0 ) { for( x=0; x<dsize-1; x++ ) *(dest+x) = *(src+x); *(dest+x) = '\0'; } /* if underflow, copy the entire string */ /* same condition apples to equal buffer size */ else { for( x=0; x<ssize; x++ ) *(dest+x) = *(src+x); *(dest+x) = '\0'; } return(flow); } int main() { const char size=16; char dest[size]; char src[] = "The string is the thing"; int r; /* copy the string */ r = stringcopy(dest,size,src,sizeof(src)-1); /* test result */ if( r<0 ) { puts("String overflow"); printf("'%s' copied to '%s'\n",src,dest); } else if( r>0 ) { puts("String underflow"); printf("'%s' copied to '%s'\n",src,dest); } else { puts("String fully copied"); printf("'%s' copied to '%s'\n",src,dest); } return 0; }
The stringcopy() function first calculates integer variable flow as the difference between the destination buffer’s size and the source buffer’s size. The result determines overflow or underflow.
An if-else decision is made depending on the value of flow. When flow is less than zero, meaning that the buffer would otherwise overflow, only dsize-1 characters are copied into buffer dest. The final character is the null character, creating the string.
The else condition handles underflow, in which case the entire string is copied. This condition also handles when the dest buffer is larger than necessary, with flow containing the number of characters remaining in the buffer.
No matter which decision is made, the value of flow is returned, indicating underflow or overflow as well as reporting characters cut off or remaining in the buffer.
The main() function copies a source string that’s too long to fit in the destination buffer. Variable r captures the return value from the stringcopy() function, with an if else-if else structure handling the various conditions possible.
Here’s output from a sample run:
String overflow
'The string is the thing' copied to 'The string is t'
I’m not certain whether this code will land me a job, but it doesn’t accomplish the task at hand. In the future I may explore the variety of string copying functions and how they deal with various conditions. Just about anything is better than the standard strcpy() function, which can easily be exploited.
Good exercise for any beginner of the language for sure!
Also, late to the party… but: while strncpy() is useless as an alternative to strcpy() because it does not guarantee that the result is null-terminated, glibc nowadays does ship with a function that is safe (i.e. truncates dest if necessary, always places a ‘\0’ at the end, and returns the length of the source string):
if (strlcpy (dst, src, sizeof(dst)) >= sizeof(dst)) {
… too long …
}
At least, if puts(gnu_get_libc_version()); outputs “2.38” or higher.
In “real applications” it therefore doesn’t really make sense to try and implement strlcpy() oneself—my attempt here is about a factor of 3-4 slower than glibcʼs strlcpy.
One other thing if I may:
With ‘const char size=16; char dest[size];’ the above main() function does something I would avoid if in a coding interview.
That’s because this raises the question of what ‘dest’ actually is… the problem being that ‘const char size=16;’, despite the
const, is not actually a compile-time constant⁽¹⁾. That is, the above defines a variable-length array (VLA) whose size is—at least in principle—not known until run-time⁽²⁾. (Nowadays, the consensus seems to be that VLAs were a mistake⁽³⁾, while VMTs, Variably-Modified Types, were made mandatory in C23.)For ‘size’ to be a compile-time constant, it would have to be something the standard refers to as a “constant expression”:
“A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.”
All in all, the language offers the following mechanisms to define a “constant expression”:
⒈ #define SIZE 16 /* preprocessor, since ~1973 */
⒉ enum { size = 16 }; /* “enum hack”, since C89 */
⒊ constexpr int size = 16; /* “constant expression”, since C23 */
For my feeling, ⒊ is still a bit too new. Personally, I would probably go with ⒈ or ⒉…
⁽¹⁾ In hindsight, it might have been better, if const had been named readonly instead.
⁽²⁾ GCC or Clang will optimize any overhead away… but a lesser compiler might not.
⁽³⁾ If
-Wvlais passed on the command-line, the compiler will issue a warning should a VLA be declared by accident.I like 1 as well, but mostly when the file contains multiple functions that use the constant. My issue, however, is avoiding using a literal, especially one that’s repeated a few times in the code. I’m with you on VLAs, which I’ve covered previous on this blog.
Thanks again for the insights!
«My issue, however, is avoiding using a literal, especially one that’s repeated a few times in the code.»
I realize that. I only wrote what I wrote because beginners of C reading this blog are likely to write such code in their own projects as well… possibly without realizing that they are not dealing with static array declarations but with VLAs.
Despite what I wrote earlier, I think in cases such as the above it may, after all, be best to use
constexpr int size = 16;(in order to get a true compile-time constant). Even if this necessitates invoking GCC with -std=c23 (or -std=c2x for GCC 14 or even earlier).I’ll to a lesson on that new keyword soon. Thank you!
Ah, okay. Looking forward to it!
Regrettably (or fortunately, depending on oneʼs point of view), and unlike C++,
constexpronly works for objects in C23, not for functions…