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

  1. Print a table of squares from 1 to 10 with the numbers right-aligned in a field of six characters.
  2. Read a name and a number from one line with scanf("%31s %d") and print them in reverse order.
  3. 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.

  1. What does scanf return?

    Answer

    The number of successful conversions

    scanf returns how many items it converted, which is why the sum program compares the result with 2.

  2. What does fgets do 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 strcspn before printing the line.

  3. What does the width in %8.2f mean?

    Answer

    At least eight characters are printed

    The width is a minimum; a longer value simply takes more space and pushes the following columns.

  4. What does a minus sign in %-10s do?

    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.

  5. 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.

  6. 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.

  7. This program is built as sum and 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.

  8. After scanf("%d", &number) reads 42 from a line, what does the following fgets return first?

    Answer

    An empty line holding only the newline

    The newline after 42 was not consumed, so fgets returns a line that contains just that newline.

  9. 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 %d against an int instead of int * is a format mismatch, reported under -Wformat=.

  10. What does scanf do 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