snprintf is essentially a function that redirects the output of printf
to a buffer. This is particularly useful for avoiding repetition of a formatted
string. You can build a string once and use printf("%s", mystr)
instead of
print("%d,%s,%f,%d", x,y,z,a)
every time, which gets cumbersome with actual
variable names.
snprintf vs sprintf
snprintf is extremely similar to sprintf, which can be found on the same manpage. After all, the names of the functions only differ by the character 'n'! This is actually a pretty common convention in C: the function with the 'n' will require an upper bound or maximum size as an argument. For the most part, the 'n' version of functions are safer and less susceptible to buffer overflows. A significant exception to this rule is strncpy.
Using snprintf
The function header for snprintf is
int snprintf(char *str, size_t size, const char *format, ...);
*str
is the buffer where printf output will be redirected to. size
is the
maximum number of bytes(characters) that will be written to the buffer.
The format
and the optional ...
arguments are just the string formats like
"%d", myint
as seen in printf.
Two very important things to note here:
- snprintf automatically appends a null character to the character sequence resulting from format substitution
- This automatically appended character is not exempt from the
size
check.
This means "%d", 100
will occupy 4 bytes went 'redirected' to the *str
buffer
[ 1 | 0 | 0 | \0 ]
In this example, the buffer must be of at least size 4, and the size
parameter
must be at least 4. If the buffer size is <4
but the size
argument is
>=4
, you will write past the bounds of the buffer (undefined behavior!). If
the buffer size >=4
but the size
argument is <4
, you won't
experience undefined behavior, but your resulting string will be truncated.
Consequently, it's very common to have the size of the buffer passed as the
size
argument.
Understanding the effect of size
In the following example, we take a buffer, initialize all its elements, and
run some experiments. We'll alter both the size
portion of the call to
snprintf as well as the size of the string we're attempting to place into
the buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #include<stdio.h> #define BUFSIZE 9 void init_buf(char *buf, size_t size); void print_buf(char *buf); int main(){ char buf[BUFSIZE]; init_buf(buf, BUFSIZE); print_buf(buf); // hello there! == 12 characters, > BUFSIZE init_buf(buf, BUFSIZE); snprintf(buf, BUFSIZE, "hello there!"); print_buf(buf); // turtle == 6 charaters, < BUFSIZE init_buf(buf, BUFSIZE); snprintf(buf, BUFSIZE, "turtle"); print_buf(buf); // 2222220 == 7 charaters, > 5 init_buf(buf, BUFSIZE); snprintf(buf, 5, "%d", 222222 * 10); print_buf(buf); return 0; } void init_buf(char *buf, size_t size){ int i; for(i=0; i<size; i++){ buf[i] = i + '0'; // int to char conversion } } void print_buf(char *buf){ int i; char c; for(i=0; i<BUFSIZE; i++){ c = buf[i]; if(c == '\0'){ printf("\\0"); } else{ printf("%c", buf[i]); } } printf("\n"); } |
The output:
012345678 hello th\0 turtle\078 2222\05678
Let's say n
is the index at which snprintf places the null character. n
can be formulated as
n = min(size of format string after substitution, size-1)
(By "after substitution", I mean if we had "%d",1000
, the format string
after substitution is "1000" and has a size of 4.)
You should take a few minutes to verify this formula for all the examples above :)
The return value
Now that we understand how the format string and the size
argument influence
where snprintf places the null character, we can now discuss the return value
of snprintf and its usefulness.
snprintf returns the size of the format string after substitution. If the
size of the format string after substitution is greater than or equal to
size
, that means the string was truncated. If your buffer size is also
equal to size
(which it should be when using this function), then this
truncation implies the string was too big to fit in the buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include<stdio.h> #define BUFSIZE 10 int main(){ char buf[BUFSIZE]; if(snprintf(buf, BUFSIZE, "hello") >= BUFSIZE){ printf("%s\n", buf); } if(snprintf(buf, BUFSIZE, "An extremely long string") >= BUFSIZE){ printf("buf: %s\n", buf); } if(snprintf(buf, BUFSIZE, "0%d", 123456789) >= BUFSIZE){ printf("buf: %s\n", buf); } return 0; } |
The output:
buf: An extrem buf: 012345678
The first if statement evaluates to false since the size of the string "hello"
is 6, which is less than the BUFSIZE
of 10. The second and third statements
evaluate to true since the strings after substitution have a size greater than
or equal to BUFSIZE.
A conservative buffer
A good way to demonstrate snprintf is to implement a "conservative buffer". We will allocate a small amount of memory for a buffer, attempt to place a string into it, and create a larger buffer if necessary. If the string still doesn't fit after we create the larger buffer, we'll exit the program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include<stdio.h> #include<stdlib.h> int main(){ int bufSize = 10; char *mystr = "This is my string!"; char *buf = malloc(bufSize); if(snprintf(buf, bufSize, "%s", mystr) >= bufSize){ bufSize *= 2; printf("Not enough space. Trying %d bytes\n", bufSize); free(buf); buf = malloc(bufSize); if(snprintf(buf, bufSize, "%s", mystr) >= bufSize){ printf("Still not enough space. Aborting\n"); exit(1); } } printf("There was enough space!\n"); printf("buf: %s\n", buf); return 0; } |
The output:
Not enough space. Trying 20 bytes There was enough space! buf: This is my string!
However, if bufSize
was initially set to 5, the output would be the
following:
Not enough space. Trying 10 bytes Still not enough space. Aborting