Safe string handling
The functions of lesson 25 write as much as the source holds. The safe versions take the size of the destination and stop before its end, and they return enough information to tell whether the result fits. This lesson uses those two facts together.
snprintf and the size
snprintf takes the buffer, its capacity and a format, and writes at most capacity - 1 characters plus the terminator. It returns the number of characters the full result would need, without the terminator, so a value smaller than the capacity means the whole text is there.
#include <stdio.h>
int main(void)
{
char buffer[16];
int written = snprintf(buffer, sizeof buffer, "%s-%d", "item", 42);
printf("%s (%d)\n", buffer, written);
return 0;
}
gcc -Wall -Wextra snprintf.c -o snprintf
./snprintf
item-42 (7)
Seven characters fit in sixteen bytes, so the return value counts what was written. When the value equals or exceeds the capacity, the text was cut, which is the subject of the next section. Reading the return is the whole difference between using the function safely and hoping.
What truncation looks like
With a smaller buffer the function still terminates the string, and the return value grows past the capacity. The program below formats an identifier read from standard input, so the compiler cannot know the length in advance and stays quiet.
#include <stdio.h>
#include <string.h>
int main(void)
{
char input[64];
char label[8];
int needed;
if (fgets(input, sizeof input, stdin) == NULL) {
return 1;
}
input[strcspn(input, "\n")] = 0;
needed = snprintf(label, sizeof label, "id:%s", input);
printf("[%s] %d\n", label, needed);
return 0;
}
gcc -Wall -Wextra runtime.c -o runtime
printf 'alpha\n' | ./runtime
[id:alph] 8
The buffer holds seven characters and the terminator, and the return value eight says that one more byte was needed. A caller that checks needed >= sizeof label knows the identifier was cut and can reject the input or allocate a larger buffer.
A bounded copy
Without a format there is nothing to interpret, and a loop with a capacity is enough. The condition reserves one byte for the terminator, so the destination is always a valid string, even when the source is longer than the buffer. The count of copied characters is the index the loop stopped at.
#include <stdio.h>
int main(void)
{
char source[] = "hello world";
char target[8];
size_t capacity = sizeof target;
size_t i = 0;
while (i + 1 < capacity && source[i] != '\0') {
target[i] = source[i];
i++;
}
target[i] = '\0';
printf("[%s] copied %zu\n", target, i);
return 0;
}
gcc -Wall -Wextra bounded.c -o bounded
./bounded
[hello w] copied 7
The same loop works with any capacity, including zero if the check runs before the first write. strncpy from lesson 25 does not give this guarantee: it writes the terminator only when the source is shorter than the limit.
Building a line
Field widths are how a report keeps its columns, and snprintf composes the whole row in one call. The destination here is large enough by construction: ten characters for the name, one for the bar and five for the number, plus the terminator, which is well under thirty-two.
#include <stdio.h>
int main(void)
{
char row[32];
int used = snprintf(row, sizeof row, "%-10s|%5d", "apples", 12);
printf("%s\n", row);
printf("used: %d\n", used);
return 0;
}
gcc -Wall -Wextra row.c -o row
./row
apples | 12
used: 16
The name is left-aligned in ten characters, then the bar, then the number right-aligned in five. Sixteen characters were used, so the row would also fit in a buffer of seventeen bytes; anything smaller would return a larger number and a truncated row.
Ignoring the return value
When both the format and the source are literals, the compiler can compute the result and warn that it will not fit. The program still builds and writes a properly terminated, shortened string, so a caller that does not read the return value never notices the loss:
#include <stdio.h>
int main(void)
{
char buffer[8];
snprintf(buffer, sizeof buffer, "%s", "truncated");
printf("[%s]\n", buffer);
return 0;
}
gcc -Wall -Wextra mist_trunc.c -o mist_trunc
./mist_trunc
mist_trunc.c: In function 'main':
mist_trunc.c:7:38: warning: '%s' directive output truncated writing 9 bytes into a region of size 8 [-Wformat-truncation=]
7 | snprintf(buffer, sizeof buffer, "%s", "truncated");
| ~^ ~~~~~~~~~~~
| |
| const char[10]
mist_trunc.c:7:5: note: 'snprintf' output 10 bytes into a destination of size 8
7 | snprintf(buffer, sizeof buffer, "%s", "truncated");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[truncat]
The second program is the same mistake with input the compiler cannot see. It warns about a possible truncation and then prints the cut label as if it were complete, because the return value is discarded:
#include <stdio.h>
#include <string.h>
int main(void)
{
char input[64];
char label[8];
if (fgets(input, sizeof input, stdin) == NULL) {
return 1;
}
input[strcspn(input, "\n")] = 0;
snprintf(label, sizeof label, "id:%s", input);
printf("label: %s\n", label);
return 0;
}
gcc -Wall -Wextra unchecked.c -o unchecked
printf 'alpha\n' | ./unchecked
unchecked.c: In function 'main':
unchecked.c:13:39: warning: '%s' directive output may be truncated writing up to 63 bytes into a region of size 5 [-Wformat-truncation=]
13 | snprintf(label, sizeof label, "id:%s", input);
| ~^ ~~~~~
| |
| char *
unchecked.c:13:5: note: 'snprintf' output between 4 and 67 bytes into a destination of size 8
13 | snprintf(label, sizeof label, "id:%s", input);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
label: id:alph
The fix is one comparison: keep the return value and act on needed >= (int) sizeof label. Rejecting the input is usually better than using a truncated identifier, because the two different names alpha and alphabet would otherwise share a label.
Exercises
- Write a function that copies a string into a buffer of a given capacity and returns how many characters were copied. Test it with a source longer and shorter than the buffer.
- Build a two-column table of ten rows with snprintf into one row buffer, and print each row as soon as it is built.
- Read a path with fgets and copy its last eight characters into a buffer with snprintf. Check the return value before printing the result.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
snprintfreturn?Answer
The length the full text needs
The return value is the length the complete text needs without the terminator, so it can exceed the capacity and reveal truncation.
-
What does
snprintfguarantee about the terminator?Answer
It always writes one when the capacity is above zero
The function reserves the last byte for the zero whenever the capacity is positive, even when the text is cut.
-
How does a caller detect that
snprintftruncated the text?Answer
When the return value is at least the capacity
A return value greater than or equal to the capacity means the full result did not fit and the buffer holds a cut version.
-
Why does the bounded copy loop use
i + 1 < capacity?Answer
To leave room for the terminating zero
The loop stops one byte before the end so the terminator can always be written and the result stays a valid string.
-
What does this program print?
#include <stdio.h> int main(void) { char buffer[16]; int written = snprintf(buffer, sizeof buffer, "%s-%d", "item", 42); printf("%s (%d)\n", buffer, written); return 0; }Answer
item-42 (7)
The formatted text has seven characters, and the return value reports that length.
-
What does this program print?
#include <stdio.h> int main(void) { char label[8]; int needed = snprintf(label, sizeof label, "id:%s", "alpha"); printf("[%s] %d\n", label, needed); return 0; }Answer
[id:alph] 8
The eight-byte buffer keeps seven characters plus the terminator, and the return value 8 says one more byte was needed.
-
What does this program print?
#include <stdio.h> int main(void) { char source[] = "hello world"; char target[8]; size_t capacity = sizeof target; size_t i = 0; while (i + 1 < capacity && source[i] != '\0') { target[i] = source[i]; i++; } target[i] = '\0'; printf("[%s] copied %zu\n", target, i); return 0; }Answer
[hello w] copied 7
Seven characters fit before the terminator, so the result is
hello wand the count is 7. -
What does this program print?
#include <stdio.h> int main(void) { char row[32]; int used = snprintf(row, sizeof row, "%-10s|%5d", "apples", 12); printf("%s\nused: %d\n", row, used); return 0; }Answer
apples | 12 / used: 16
Ten characters for the name, one bar and five for the number make sixteen.
-
This program is built with
gcc -std=c17 -Wall -Wextra mist_trunc.c -o mist_trunc. Which warning flag does GCC print?#include <stdio.h> int main(void) { char buffer[8]; snprintf(buffer, sizeof buffer, "%s", "truncated"); printf("[%s]\n", buffer); return 0; }Answer
-Wformat-truncation=
The known output does not fit the buffer, which
-Wformat-truncation=reports. -
Why is a truncated identifier such as
id:alphdangerous?Answer
Different names can share the same truncated label
Both
alphaandalphabetcan becomeid:alph, so rejecting the input is safer than using the cut label.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev