Learn C Programming

Lesson 3 of 7 · Constants, Qualifiers, Input and Output

Module 3 · Constants, Qualifiers, Input and Output

printf: Format Specifiers, Width and Precision

FreeReading

In this lesson

  • Read any format specifier as four separate parts, and write one from scratch.
  • Print a table whose columns line up, using width, the minus flag and the zero flag.
  • Use what printf gives back, and say what a mismatched specifier does on the Playground.

Amara is printing a contest leaderboard. Her names are different lengths, so the solved column starts in a different place on every row. The whole thing looks like a ransom note. She has been adding spaces by hand for twenty minutes.

The fix is four characters. printf has had a small layout language inside it since 1972, and almost nobody is taught it properly. This lesson teaches it properly.

A format string is text with holes in it

The first argument to printf is a template. Everything in it is printed as it stands, except a run starting with %, which is a conversion specification: a hole, plus instructions for filling it.

The values after the format string fill the holes in order. There is no checking at run time, and that fact is the source of every mistake in this lesson.

One specifier, four optional parts and one required one

%[flags][width][.precision]conversion

flags        -  left align inside the field
             0  pad the field with zeros instead of spaces
             +  always print a sign, even for a positive number
                a space, print a space where the plus sign would go

width        a whole number: the smallest number of characters to use

.precision   for a float, the digits after the point
             for a string, the most characters to print

conversion   d  i  a signed int              u  an unsigned int
             ld    a long                    lld   a long long
             f     a double, fixed point      e  a double, scientific
             g     a double, whichever is shorter
             c     one character              s   a string
             x  X  hexadecimal, lower or upper case
             o     octal                      p   a pointer
             zu    a size_t                   %%  a literal percent sign
  • Only the % and the conversion are required. %d is a complete specifier.
  • A width is a minimum, never a maximum. A value too big for its field pushes past it.
  • The letter before the conversion is a length modifier, and it has to match the type or the reading goes wrong.
The five parts of the specifier %-8.2f One specifier, read left to right % - 8 .2 f start flag width precision conversion always left align at least 8 2 decimals a double Only the first box and the last are required. The three in between are layout, and each one may be left out. printf("[%-8.2f]", 3.14159) prints [3.14 ], because the value is rounded to two decimals, then padded on the right until the field is eight characters wide. Read the parts in that order and no specifier is ever a surprise.
Figure 1. The anatomy of one specifier. The order of the parts is fixed and the middle three are optional.

Width is the smallest field a value may occupy

A width pads the value with spaces until the field is that wide. By default the padding goes on the left, so the value sits to the right of its field.

Two flags change that. The minus sign moves the padding to the right, so the value sits on the left. The zero pads with zeros instead of spaces, which only makes sense for a number.

#include <stdio.h>

int main(void)
{
    printf("[%d]\n", 42);
    printf("[%8d]\n", 42);
    printf("[%-8d]\n", 42);
    printf("[%08d]\n", 42);
    printf("[%+d]\n", 42);
    printf("[% d]\n", 42);
    printf("[%8.3f]\n", 3.14159);
    printf("[%-8.3f]\n", 3.14159);
    printf("[%.3s]\n", "Progsity");
    printf("[%%]\n");
    return 0;
}
[42]
[      42]
[42      ]
[00000042]
[+42]
[ 42]
[   3.142]
[3.142   ]
[Pro]
[%]

Every square bracket in that output is exactly where the field starts and ends. Count the characters between them and each rule explains itself.

The ninth line is the surprising one. On a string, a precision is a maximum rather than a minimum, so %.3s printed three letters of eight.

The last line is how you print a percent sign. A lone % starts a specifier, so you write two.

Precision means two different things

On a float, the precision is the number of digits after the point, and the value is rounded to fit. %.2f is the shape almost every money and measurement problem asks for.

On a string, the precision is the largest number of characters to print, and the rest is dropped.

On an integer, a precision is the smallest number of digits, padded with zeros, which is a rare thing to want and worth recognising.

So the same two characters mean round to, cut to, or pad to, depending on one letter three places to the right.

A lined-up table is width and nothing else

Amara's leaderboard needs one width per column, and the text column needs the minus flag so that names start at the same place.

#include <stdio.h>

int main(void)
{
    printf("%-14s%8s%10s\n", "PLAYER", "SOLVED", "PENALTY");
    printf("%-14s%8d%10d\n", "Amara", 9, 412);
    printf("%-14s%8d%10d\n", "Kenji", 9, 1207);
    printf("%-14s%8d%10d\n", "Zara", 7, 388);
    printf("%-14s%8d%10d\n", "Bob", 2, 44);
    return 0;
}
PLAYER          SOLVED   PENALTY
Amara                9       412
Kenji                9      1207
Zara                 7       388
Bob                  2        44

One format string, used five times. The header row uses %s for the numbers' column titles, which is why the widths match across all five lines.

Notice there is no space between the specifiers. The widths already provide the gaps, and adding spaces as well is how a table ends up one character out.

printf hands you a number back

Almost nobody knows this. printf returns the number of characters it wrote, or a negative number if the write failed.

Most programs ignore it, which is fine. It is the honest way to answer "how wide was that line". It also matters because a returned value is how scanf is used properly in the next lesson.

#include <stdio.h>

int main(void)
{
    int written = printf("Amara scored %d\n", 97);

    printf("that line was %d characters long\n", written);
    printf("a %%d holds %d, a %%x holds %x, a %%o holds %o\n", 255, 255, 255);
    printf("a size_t needs %%zu: %zu\n", sizeof(int));
    return 0;
}
Amara scored 97
that line was 16 characters long
a %d holds 255, a %x holds ff, a %o holds 377
a size_t needs %zu: 4

Sixteen characters: fourteen visible, plus the newline, plus the second digit of 97. Count them yourself, because that is the point of the line.

The third line prints one value three times in three bases. Module 3 lesson 1 wrote those bases as literals; here they are being read back out.

The fourth uses %zu, the specifier for the type sizeof gives you. Module 2 lesson 5 introduced it, and it is in this list because %d there is a mismatch that nothing will report.

When the specifier and the value disagree

A specifier tells printf how many bytes to take and how to read them. If it is wrong, printf still takes those bytes and still reads them that way.

The format checking that catches this lives behind -Wall, and the Playground compiles with gcc -O2 -std=c17 -lm and no -Wall. So on the Playground this class of mistake is completely silent, at compile time and at run time.

That is not a reason to avoid the Playground. It is a reason to know the four mismatches below by sight, because you will get no help finding them.

Example 1: the smallest formatted line

One value, three widths, so you can see what a field actually is.

#include <stdio.h>

int main(void)
{
    int marks = 7;

    printf("|%d|\n", marks);
    printf("|%4d|\n", marks);
    printf("|%-4d|\n", marks);
    return 0;
}
|7|
|   7|
|7   |

Three lines, one value. The bars are only there so that you can count the spaces.

Run in Compiler
Example 2: a receipt, where precision is the whole job

Money to two decimals, a quantity right aligned, and a name on the left. This is the shape of half the problems you will ever be set.

#include <stdio.h>

int main(void)
{
    printf("%-10s%5s%10s\n", "ITEM", "QTY", "TAKA");
    printf("%-10s%5d%10.2f\n", "rice", 12, 45.00);
    printf("%-10s%5d%10.2f\n", "oil", 3, 120.50);
    printf("%-10s%5d%10.2f\n", "salt", 7, 2.50);
    printf("%-10s%5d%10.2f\n", "TOTAL", 22, 1083.50);
    return 0;
}
ITEM        QTY      TAKA
rice         12     45.00
oil           3    120.50
salt          7      2.50
TOTAL        22   1083.50

The decimal points line up because every number in that column has the same width and the same precision. Drop the .2 and the column collapses.

Real money is counted in whole paisa, the Module 2 lesson 3 rule. This is a layout example, and the graded problems of this module all use integers.

Run in Compiler
Example 3: the same number in four bases, the one you would actually write

A value read out of a device, printed the four ways different people will ask for it.

#include <stdio.h>

int main(void)
{
    unsigned int reg = 48879;

    printf("dec:%10u\n", reg);
    printf("hex:%10x\n", reg);
    printf("HEX:%10X\n", reg);
    printf("oct:%10o\n", reg);
    printf("padded hex: 0x%08X\n", reg);
    return 0;
}
dec:     48879
hex:      beef
HEX:      BEEF
oct:    137357
padded hex: 0x0000BEEF

The last line is how a hex dump is written everywhere. The 0x is text you typed, and the 08 keeps eight digits even when the value is small.

Run in Compiler

Where this is used

  • Every judge on this platform. The Progsity judge compares your output with the expected file character by character. A missing zero in a paisa column is a wrong answer, and the specifier is the only thing standing between you and that.
  • The ls -l listing on any Linux machine. The columns of size, date and name are printed with exactly these widths, by C code, in a program written in 1971 and still running.
  • Five other languages. Python, Java, Go, Rust and PHP all carry a direct descendant of this format language. Learning C's version teaches you theirs by accident.
  • A CSV writer in a bank's export job. Each field is printed with a fixed precision so that two systems reading the file agree on the number, down to the last paisa.

Common mistakes

1. A specifier that does not match its value.

double price = 12.5;
printf("%d\n", price);

Silent on the Playground, because format checking lives behind -Wall and the Playground does not pass it. A local gcc -Wall says warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' and helpfully suggests %f. The printed number is meaningless.

2. Fewer values than holes.

printf("marks %d and %d\n", 90);

Silent on the Playground. The first hole gets 90. The second reads whatever happens to be where an argument would have been, so a meaningless number appears in it. With -Wall the message is warning: format '%d' expects a matching 'int' argument.

3. A lone percent sign in the text.

printf("VAT is 15% of the bill\n");

Silent on the Playground, and the output is mangled in a way that looks nothing like the cause. The % plus the space plus the o of of are read as one specifier, so those characters vanish and a number appears in their place. Write %% for a percent sign.

4. %d for a sizeof or a long long.

long long total = 5000000000;
printf("%d\n", total);
printf("%d\n", sizeof(int));

Silent on the Playground, at both lines. %d takes four bytes and both of those values are eight, so the number printed is built from the wrong half of them. It is %lld for a long long and %zu for a sizeof, and Module 2 lesson 5 is where those came from.

Brain teaser

Zara is printing a measurement and expects five characters of output.

#include <stdio.h>

int main(void)
{
    printf("[%5.2f]\n", 3.14159);
    printf("[%5.2f]\n", 1234.5678);
    printf("[%.2f]\n", 3.14159);
    return 0;
}

The first line prints [ 3.14], which is five characters wide with a space at the front. Say what the 5 counts, given that the value has only four characters of digits and a point.

Then the harder half. Predict the second line exactly, and say what a width promises when the value will not fit inside it.

Write out the four characters of 3.14 and ask how many more are needed to reach five. Then ask whether a field can ever be made smaller than its contents.

Exercise 1: hex-octal-dumpMedium

Kenji is reading a device register and needs it in three bases, each pushed into the same ten wide field.

Input. One line with one integer n in decimal.

Output. Four lines: dec:, hex:, HEX: and oct:, each followed by n in a field of width 10.

Constraints. 0 <= n <= 4294967295, so the value needs an unsigned int.

Sample. Input 255 gives dec: 255 and three more lines.

#include <stdio.h>

int main(void)
{
    unsigned int n = 0;
    scanf("%u", &n);

    /* Four lines, four bases, one width. */

    return 0;
}

Graded as hex-octal-dump. At the top of the range one of the four lines is eleven characters wide, and that is correct.

Run in Compiler
Exercise 2: the leaderboardEasy

Rebuild Amara's table from the example above, with every column three characters wider. Add a row at the end for Maria, with 5 solved and 91 penalty.

Output. Six lines, the header and five players, every column lined up.

No input. Type the whole thing rather than copying it, and change one width to see the table move.

#include <stdio.h>

int main(void)
{
    /* One format string, used six times. Start from the example above. */

    return 0;
}

Not graded in this module: a program with no input has one possible output, and one test is not a judge.

Run in Compiler
Exercise 3: the price listMedium

Read three prices as decimals and print each one to exactly two decimal places, in a field ten wide. Put the word taka after each.

Input. One line with three decimal numbers.

Output. Three lines, each a price then one space then taka.

Sample. Input 45 120.5 2.5 gives 45.00 taka and two more lines.

#include <stdio.h>

int main(void)
{
    double a = 0.0;
    double b = 0.0;
    double c = 0.0;
    scanf("%lf %lf %lf", &a, &b, &c);

    /* Three printf calls, one width and one precision each. */

    return 0;
}

Not graded in this module. The graded version of this idea is invoice-exact in the problems lesson, which counts in whole paisa instead.

Run in Compiler
Exercise 4: the character counterHard

Print a line of your own, then print how many characters that line was, using the value printf gave you and not a number you counted.

Output. Two lines. The second says how long the first was.

Then change the first line's text and check that the second line follows it without you touching it.

#include <stdio.h>

int main(void)
{
    int written = printf("Progsity Learn, C track\n");

    /* One more printf, using written. */

    return 0;
}

Not graded in this module. Then answer this on paper: does the count include the newline, and how would you prove it without trusting your memory?

Run in Compiler

Common doubts

  • Can a width be cut off if the value is too big?

    No. A width is a minimum only. The field grows to fit the value and the columns after it shift. That is why a table breaks on one long row rather than everywhere.

  • Why is it %f for both float and double in a printf?

    Because a float argument is widened to a double on the way into the call. In a scanf nothing is widened, which is why there it really is %lf. That is the next lesson.

  • Does %.2f round or cut?

    It rounds. What it rounds is the value stored in the double, which Module 2 lesson 3 showed is not always the value you typed.

  • Can I put the width in a variable?

    Yes, with a star: printf("%*d", 8, marks) takes the width from the first argument. It is rare, and worth recognising rather than reaching for.

  • Why does my number appear in a strange place?

    Almost always a lone % earlier in the same string. Everything after a bad specifier shifts, so look left of the damage rather than at it.

Key takeaways

  • A specifier is a percent sign, then flags, width and precision, then one conversion letter.
  • Width is a minimum: the field grows for a bigger value and never cuts one.
  • Precision rounds a float, caps a string, and pads an integer with zeros.
  • The minus flag aligns left and the zero flag pads with zeros; both go before the width.
  • printf returns the number of characters it wrote, and %% prints one percent sign.
  • A mismatched specifier is silent on the Playground, because format checking needs -Wall.

Next you turn the whole thing around and read values in, where the same format language has one extra rule and one famous trap.

End of lesson 3

Mark it done, and your progress moves with you.

Next: scanf: Reading Input Without Losing Your Mind