Formatted input and output
Standard input and standard output are two streams of bytes. Formatted functions turn numbers into text and back, and almost every bug in this area is a mismatch between the format and the argument, or a check that was skipped. This lesson reads real input through a pipe so the behaviour is reproducible.
Fields and widths
A conversion in printf can carry a width and a precision: %8.2f prints at least eight characters with two decimals, and a minus sign before the width left-aligns. %-10s pads a string to ten characters on the right, which is enough to build a table from a loop.
#include <stdio.h>
int main(void)
{
printf("%-10s %6s %8s\n", "item", "qty", "price");
printf("%-10s %6d %8.2f\n", "tea", 2, 4.5);
printf("%-10s %6d %8.2f\n", "coffee", 1, 12.75);
printf("%-10s %6d %8.2f\n", "biscuits", 10, 0.35);
return 0;
}
gcc -Wall -Wextra table.c -o table
./table
item qty price
tea 2 4.50
coffee 1 12.75
biscuits 10 0.35
printf returns the number of characters written, or a negative value on error; a program that writes to a pipe can use that value. The width is a minimum, so a longer name simply pushes the following columns to the right.
Reading numbers
scanf parses the input according to the format and stores the results through pointers. It returns how many items it converted, which is the only reliable way to know that the input matched. Here two conversions must succeed, so the program compares the result with 2.
#include <stdio.h>
int main(void)
{
int a;
int b;
if (scanf("%d %d", &a, &b) != 2) {
fprintf(stderr, "expected two integers\n");
return 1;
}
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
Feed it a good line and a bad one. The second run stops at the word and the program reports the failure:
gcc -Wall -Wextra sum.c -o sum
printf '4 7\n' | ./sum
printf '4 seven\n' | ./sum; echo "status: $?"
4 + 7 = 11
expected two integers
status: 1
When a conversion fails, scanf leaves the offending text in the stream. That is useful for a retry loop and confusing when the next read gets the same characters again.
Lines with fgets
For whole lines, fgets is easier to control: it reads at most the size of the buffer minus one, always terminates the string, and stops at a newline. The newline stays in the buffer, so the program removes it before printing.
#include <stdio.h>
#include <string.h>
int main(void)
{
char line[64];
while (fgets(line, sizeof line, stdin) != NULL) {
line[strcspn(line, "\n")] = 0;
printf("[%s] %zu chars\n", line, strlen(line));
}
return 0;
}
gcc -Wall -Wextra lines.c -o lines
printf 'first line\nsecond\n' | ./lines
[first line] 10 chars
[second] 6 chars
The length is measured after the newline is removed. A line longer than 63 characters arrives in pieces, which the loop treats as separate lines; lesson 58 shows how getline avoids that limit.
The newline scanf leaves
Mixing the two is a classic trap. After scanf("%d") the newline is still unread, so the next fgets returns an empty line and the real text stays in the stream. The first program below shows the bug.
#include <stdio.h>
int main(void)
{
int number;
char line[64];
printf("number: ");
if (scanf("%d", &number) != 1) {
return 1;
}
printf("number is %d\n", number);
if (fgets(line, sizeof line, stdin) == NULL) {
printf("nothing left\n");
return 0;
}
printf("next line: [%s]\n", line);
return 0;
}
gcc -Wall -Wextra mix.c -o mix
printf '42\nnext text\n' | ./mix
number: number is 42
next line: [
]
The fix is to drop everything up to and including the newline before reading the line. A loop with getchar does that without guessing how many characters are left:
#include <stdio.h>
int main(void)
{
int number;
char line[64];
int c;
printf("number: ");
if (scanf("%d", &number) != 1) {
return 1;
}
while ((c = getchar()) != '\n' && c != EOF) {
/* drop the rest of the number line */
}
printf("number is %d\n", number);
if (fgets(line, sizeof line, stdin) == NULL) {
printf("nothing left\n");
return 0;
}
printf("next line: [%s]", line);
return 0;
}
gcc -Wall -Wextra mix_fixed.c -o mix_fixed
printf '42\nnext text\n' | ./mix_fixed
number: number is 42
next line: [next text
]
The brackets show that the newline belongs to the line fgets returned, which is why the closing bracket starts on the next line. Strip it when the line is printed, as in the previous section.
The missing ampersand
scanf needs the address of the object, so number without an ampersand passes an integer where a pointer is expected. GCC reports the format mismatch and the uninitialised variable, but the program still builds:
#include <stdio.h>
int main(void)
{
int number;
if (scanf("%d", number) != 1) {
return 1;
}
printf("%d\n", number);
return 0;
}
gcc -Wall -Wextra forget_amp.c -o forget_amp
forget_amp.c: In function 'main':
forget_amp.c:7:17: warning: format '%d' expects argument of type 'int *', but argument 2 has type 'int' [-Wformat=]
7 | if (scanf("%d", number) != 1) {
| ~^ ~~~~~~
| | |
| | int
| int *
forget_amp.c:7:9: warning: 'number' is used uninitialized [-Wuninitialized]
7 | if (scanf("%d", number) != 1) {
| ^~~~~~~~~~~~~~~~~~~
forget_amp.c:5:9: note: 'number' was declared here
5 | int number;
| ^~~~~~
Run, this program treats the integer as an address and writes to it, which usually ends in a segmentation fault. The warning is the whole protection: the compiler knows the format, so it knows the argument is wrong.
Exercises
- Print a table of squares from 1 to 10 with the numbers right-aligned in a field of six characters.
- Read a name and a number from one line with scanf("%31s %d") and print them in reverse order.
- Make the sum program ask again after bad input instead of exiting. Use the return value of scanf and a loop.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
scanfreturn?Answer
The number of successful conversions
scanfreturns how many items it converted, which is why the sum program compares the result with 2. -
What does
fgetsdo with a newline in the input?Answer
It keeps it in the buffer
The newline stays in the buffer when there is room, so programs strip it with
strcspnbefore printing the line. -
What does the width in
%8.2fmean?Answer
At least eight characters are printed
The width is a minimum; a longer value simply takes more space and pushes the following columns.
-
What does a minus sign in
%-10sdo?Answer
Left-aligns the string in the field
The minus flag left-aligns, so the padding is added on the right instead of the left.
-
What does this program print?
#include <stdio.h> int main(void) { printf("[%6.2f]", 4.5); return 0; }Answer
[ 4.50]
The field is six characters wide, the value is right-aligned, and the precision adds two decimals.
-
What does this program print?
#include <stdio.h> #include <string.h> int main(void) { char line[64] = "first line\n"; line[strcspn(line, "\n")] = 0; printf("%zu chars\n", strlen(line)); return 0; }Answer
10 chars
The newline is removed before
strlen, so the ten characters of the words remain. -
This program is built as
sumand run with./sum; echo "status: $?". What does the shell print?#include <stdio.h> int main(void) { int a, b; if (sscanf("4 seven", "%d %d", &a, &b) != 2) { fprintf(stderr, "expected two integers\n"); return 1; } printf("%d + %d = %d\n", a, b, a + b); return 0; }Answer
expected two integers / status: 1
The second conversion fails, the function returns 1 instead of 2, and the error branch exits with status 1.
-
After
scanf("%d", &number)reads42from a line, what does the followingfgetsreturn first?Answer
An empty line holding only the newline
The newline after 42 was not consumed, so
fgetsreturns a line that contains just that newline. -
This program is built with
gcc -std=c17 -Wall -Wextra forget_amp.c -o forget_amp. Which format warning flag does GCC print?#include <stdio.h> int main(void) { int number; if (scanf("%d", number) != 1) return 1; printf("%d\n", number); return 0; }Answer
-Wformat=
The
%dagainst anintinstead ofint *is a format mismatch, reported under-Wformat=. -
What does
scanfdo with text it could not convert?Answer
It leaves the text in the stream
The offending characters stay in the input stream, so a retry loop must consume them before the next attempt.
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