I came here to post exactly that; I think using snprintf() is the proper solution to the "I need to build a string out of many parts in C" problem pretty much every time.
Especially since these functions' return values make it hard to use them to step forward in the buffer, so subsequent calls need to walk over the already-inserted characters again and again. That kind of redundant work makes me grit my teeth, which I guess is a sign that I'm a C programmer at heart. :)
The latter is given a pointer to the base buffer, so it has to walk past the existing characters in order to find the end where the concatenation should start.
I also find the "size" arguments confusing and weird. I think it's idiomatic in C to (tightly) associate a size with a buffer pointer, and speaking in terms of "there are n bytes available at this location" makes sense to me. So the prototype really should have been
Even better, if you don't mind using the heap, there's also asprintf() which figures out the right length and allocates a buffer itself, then returns it to you. Downside is you have to free the pointer returned of course. And you might want to be careful passing it user input without taking a look at the length first. But you have to do that anyway if you use the stack.
Here's something I've found a useful upgrade to asprintf, as it frees the passed-in buffer after expanding the format string. You can just pass the same char ** repeatedly (and also pass it as one of the string arguments corresponding to a %s!) and it'll update the pointer appropriately each time. Makes many kinds of string manipulation very simple.
By the way, I realized that sprintf can be used to concatenate by using the buffer within the formatting itself, while snprintf prevents it. Is it a safety issue too?
keep in mind that you can't do that, here's a quote that explains it well:
Some programs imprudently rely on code such as the following
sprintf(buf, "%s some further text", buf);
to append text to buf. However, the standards explicitly note that the results
are undefined if source and destination buffers overlap when calling sprintf(),
snprintf(), vsprintf(), and vsnprintf(). Depend‐ ing on the version of gcc(1)
used, and the compiler options employed, calls such as the above will not
produce the expected results.
The glibc implementation of the functions snprintf() and vsnprintf() conforms
to the C99 standard, that is, behaves as described above, since glibc 2.1.
Until glibc 2.0.6, they would return -1 when the output was truncated.
Except snprintf() does not terminate the string if there's no room for the final \0. So technically this should be
snprintf(buf, sizeof buf - 1, "%s%s", "foo", "bar");
buf[sizeof buf - 1] = '\0';
I don't think that is correct, my understanding is that it always terminates when possible. FreeBSD's man page [1] has a pretty clear description:
The snprintf() and vsnprintf() functions will write at most size-1 of the
characters printed into the output string (the size'th character then
gets the terminating `\0'); if the return value is greater than or equal
to the size argument, the string was too short and some of the printed
characters were discarded. The output is always null-terminated, unless
size is 0.
On a more anecdotal front, I did a quick test on Ideone [2] that seems to behave exactly how I would expect it to (and contradicts your description).
In short: always call snprintf() with the exact size of the buffer. No Obi-Wans.
Especially since these functions' return values make it hard to use them to step forward in the buffer, so subsequent calls need to walk over the already-inserted characters again and again. That kind of redundant work makes me grit my teeth, which I guess is a sign that I'm a C programmer at heart. :)
Compare:
and The latter is given a pointer to the base buffer, so it has to walk past the existing characters in order to find the end where the concatenation should start.I also find the "size" arguments confusing and weird. I think it's idiomatic in C to (tightly) associate a size with a buffer pointer, and speaking in terms of "there are n bytes available at this location" makes sense to me. So the prototype really should have been