Streams and the standard streams

The standard I/O library, <stdio.h>, reads and writes through streams. A FILE stream is an object of type FILE, always handled through a FILE * pointer, that stands for an open file, terminal or pipe. It holds the stream's buffer, its position and its error and end-of-file flags. You never look inside it; you pass the pointer to functions such as fprintf, fgets and fclose.

Three streams are already open when main starts: stdin (standard input), stdout (standard output) and stderr (standard error). printf(…) is short for fprintf(stdout, …). Results go to stdout; error messages and diagnostics go to stderr, so that a user who redirects the output to a file still sees the errors, and a script that reads the output does not get the errors mixed in.

#include <stdio.h>

int main(void)
{
    printf("E-001,350\n");                              /* data: stdout */
    fprintf(stderr, "warning: E-002 has no reading\n"); /* diagnostics: stderr */
    fprintf(stdout, "E-003,95\n");
    return 0;
}
E-001,350
warning: E-002 has no reading
E-003,95

Here both streams appear together, as in a terminal. Run as ./export > readings.csv, the file gets the two data lines and the warning stays on the screen. Underneath, the streams are file descriptors 0, 1 and 2 of the process, and every write ends in a system call such as write. The lower-level POSIX functions open, read and write work with those descriptors directly, without stdio's buffering.

printf and conversion specifications

The first argument of printf is a format string: text that is copied to the output, with placeholders that say how to print the remaining arguments. Each placeholder is a conversion specification: a %, optional flags, a width, a precision and a length modifier, and a conversion character that names the type, as in %-8.2f or %zu.

#include <stdio.h>

int main(void)
{
    int kwh = 42;
    printf("[%5d][%-5d][%05d]\n", kwh, kwh, kwh);      /* width, left, zeros */
    printf("[%x][%X][%o][%#x]\n", 255, 255, 8, 255);   /* hex, octal, prefix */
    printf("[%c][%%]\n", 'K');                         /* a character, a percent sign */
    printf("[%8.3f][%-8s][%.3s]\n", 3.14159, "kWh", "Kestrel");
    printf("%e %g %g\n", 12345.678, 0.0001, 1e20);
    printf("%zu %ld %lld\n", sizeof(double), 400000L, 3504000000LL);
    return 0;
}
[   42][42   ][00042]
[ff][FF][10][0xff]
[K][%]
[   3.142][kWh     ][Kes]
1.234568e+04 0.0001 1e+20
8 400000 3504000000
Conversion Argument type Prints
%d, %i int Signed decimal
%u, %x, %o unsigned int Unsigned decimal, hexadecimal, octal
%ld, %lld long, long long The length modifier l or ll before the conversion
%zu, %td size_t, ptrdiff_t Sizes and pointer differences
"%" PRId64 int64_t Macro from <inttypes.h> that expands to the right modifier
%f, %e, %g double (a float is converted to it) Fixed, scientific, or %g: fixed for moderate exponents, scientific for very small or large ones, trailing zeros removed
%c, %s int, char * One character, a null-terminated string
%p void * An address (different on every run, because of address space layout randomization)

printf cannot check its arguments itself: it trusts the format string and reads whatever is on the argument list as the type the format says. A mismatch is undefined behavior. gcc checks literal format strings at compile time (-Wformat, part of -Wall), so with -Werror a mismatch does not build:

#include <stdio.h>

int main(void)
{
    long meters = 400000;
    printf("%d meters\n", meters);
    return 0;
}
example.c:6:14: error: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long int’ [-Werror=format=]

Without -Wall, gcc prints nothing at all and the program prints a number that may or may not be right, depending on the platform. This is one of the best reasons to always compile with warnings on.

Rounding in printf

%.2f rounds the exact binary value that a double holds, which is often slightly below or above the decimal number in the source (see rounding errors). Python does the same; Java's String.format gives different results. For money, print integers:

#include <stdio.h>

int main(void)
{
    printf("%.2f %.2f %.2f\n", 2.675, 0.125, 1.005);

    long cents = 9450;              /* EUR 94.50: 350 kWh x EUR 0.2700 */
    printf("EUR %ld.%02ld\n", cents / 100, cents % 100);
    return 0;
}
2.67 0.12 1.00
EUR 94.50

2.675 is stored as 2.67499999…, so it rounds down; 0.125 is exact and lies halfway, and glibc rounds such ties to the even digit. Keeping the amount in whole cents makes the output exact by construction. (This way of splitting is for non-negative amounts; for negative ones, print the sign separately.)

Format-string bugs

Never pass text from outside the program as the format string. printf(note) treats every % in note as a conversion and reads arguments that do not exist: a note such as "100% sure" is enough for undefined behavior, and crafted input can read or even write memory. gcc 12 does not warn about this with -Wall -Wextra; the extra flag -Wformat-security does:

notes.c:6:5: warning: format not a string literal and no format arguments [-Wformat-security]

The fix is always the same: a literal format with %s, printf("%s", note), or a function without formatting, fputs(note, stdout). The SEI CERT C rule for this is FIO30-C, "Exclude user input from format strings"; FIO47-C, "Use valid format strings", covers the type mismatches above. Many distributions build their packages with -Wformat -Wformat-security, and it is worth adding to your own flags.

Reading input safely

scanf reads and converts in one step, which makes it look convenient and makes it hard to use safely. It returns the number of values it converted, or EOF when input ended before the first one, and it stops at the first character that does not fit, leaving that character in the input:

#include <stdio.h>

int main(void)
{
    int kwh = 0;
    int meter = 0;

    int got = scanf("%d %d", &kwh, &meter);
    printf("scanf converted %d value(s): kwh=%d meter=%d\n", got, kwh, meter);

    got = scanf("%d", &meter);     /* "abc" is still waiting */
    printf("second scanf converted %d\n", got);
    return 0;
}
scanf converted 1 value(s): kwh=350 meter=0
second scanf converted 0

A program that ignores the return value continues with meter unset; one that retries in a loop reads the same abc forever. And scanf("%s", name) without a width is the same overflow as gets.

The robust pattern is to separate reading from parsing. Read one whole line with fgets(), which takes the buffer size and never writes more: it stops after a newline (which it keeps), at the end of the input, or when the buffer is full, and always adds a terminator. It returns NULL at the end of the input. Then parse the line in memory, where a bad line can simply be rejected:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char line[32];
    int total = 0;

    while (fgets(line, sizeof line, stdin) != NULL) {
        if (strchr(line, '\n') == NULL && !feof(stdin)) {
            printf("line too long, skipped\n");
            int c;
            while ((c = getchar()) != '\n' && c != EOF) {
                /* throw away the rest of the line */
            }
            continue;
        }
        line[strcspn(line, "\n")] = '\0';   /* remove the newline, if any */

        char meter[16];
        int kwh;
        if (sscanf(line, "%15[^,],%d", meter, &kwh) == 2) {
            printf("%s: %d kWh\n", meter, kwh);
            total += kwh;
        } else {
            printf("bad line: \"%s\"\n", line);
        }
    }
    printf("total %d kWh\n", total);
    return 0;
}
E-001: 350 kWh
bad line: "E-002,abc"
E-003: 95 kWh
line too long, skipped
total 445 kWh

%15[^,] reads up to 15 characters that are not a comma into the 16-byte meter array, so it cannot overflow. line[strcspn(line, "\n")] = '\0' is the usual idiom to remove the newline: strcspn returns the position of the first newline, or the length when there is none. For strict number checks, parse the fields with strtol as shown on Strings and Buffer Overflows; sscanf's %d accepts "350abc" and gives undefined behavior when the number does not fit in an int.

EOF and getchar

EOF is a negative int constant (-1 with glibc) that the stdio functions return to signal the end of the input or an error. To tell the two apart afterward, call feof(stream) or ferror(stream). Because EOF must differ from every possible byte, getchar returns an int: a byte value from 0 to 255, or EOF.

#include <stdio.h>

int main(void)
{
    int c;                          /* int, not char: it must hold EOF */
    long bytes = 0;
    long lines = 0;

    while ((c = getchar()) != EOF) {
        bytes++;
        if (c == '\n') {
            lines++;
        }
    }
    printf("%ld bytes, %ld lines\n", bytes, lines);
    return 0;
}
53 bytes, 3 lines

Storing the result in a char is a classic portability bug. On a 64-bit Linux machine (ARM64), char is unsigned: -1 becomes 255, the comparison with EOF is never true and the loop never ends. On most x86-64 systems char is signed, so the loop seems to work but stops early at the first byte 0xFF in the input. gcc's -Wextra catches it on ARM64:

count.c:7:28: error: comparison is always true due to limited range of data type [-Werror=type-limits]
    7 |     while ((c = getchar()) != EOF) {
      |                            ^~

Reading and writing files

fopen() opens a file and returns a new FILE * for it, or NULL with errno set when it cannot (the file does not exist, a permission is missing). The second argument is the mode:

Mode Meaning If the file exists If it does not
"r" Read Read from the start Fails
"w" Write Emptied first Created
"a" Append Every write goes to the end Created
"r+", "w+", "a+" Read and write As above, with both directions allowed
"wx" (C11) Create, never overwrite Fails Created

A b in the mode ("rb", "wb") opens a file in binary mode. On Linux and other POSIX systems text and binary mode are the same; on Windows, text mode translates line endings, so use b for anything that is not text. The example writes a small CSV file, reads it back line by line and removes it:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    FILE *out = fopen("readings.csv", "w");
    if (out == NULL) {
        perror("readings.csv");
        return EXIT_FAILURE;
    }
    fprintf(out, "meter,kwh\n");
    fprintf(out, "%s,%d\n", "E-001", 350);
    fprintf(out, "%s,%d\n", "E-002", 1500);
    if (fclose(out) != 0) {         /* the last data is written here */
        perror("readings.csv");
        return EXIT_FAILURE;
    }

    FILE *in = fopen("readings.csv", "r");
    if (in == NULL) {
        perror("readings.csv");
        return EXIT_FAILURE;
    }
    char line[64];
    int total = 0;
    if (fgets(line, sizeof line, in) == NULL) {     /* skip the header */
        fclose(in);
        return EXIT_FAILURE;
    }
    while (fgets(line, sizeof line, in) != NULL) {
        char meter[16];
        int kwh;
        if (sscanf(line, "%15[^,],%d", meter, &kwh) == 2) {
            printf("%s -> %d\n", meter, kwh);
            total += kwh;
        }
    }
    if (ferror(in)) {
        perror("reading readings.csv");
    }
    fclose(in);
    remove("readings.csv");
    printf("total %d kWh\n", total);
    return 0;
}
E-001 -> 350
E-002 -> 1500
total 1850 kWh

Every fopen needs exactly one fclose, on every path out of the function, including error paths; otherwise the program leaks file descriptors (a process may only have a limited number open) and may lose buffered data. Checking fclose on a file you wrote matters, because the data is often only really written when the buffer is flushed during the close; a full disk shows up there, not in fprintf. Patterns for cleaning up on every path, such as goto cleanup, are on Error Handling.

When fopen fails

perror prints your text, a colon and the system's description of the current errno to stderr. It is the quickest way to give a useful message: "No such file or directory" and "Permission denied" are what a service desk can act on.

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *path = "meters-2026-10.csv";
    FILE *in = fopen(path, "r");
    if (in == NULL) {
        perror(path);
        return EXIT_FAILURE;
    }
    printf("opened %s\n", path);
    fclose(in);
    return 0;
}
meters-2026-10.csv: No such file or directory

The program also ended with exit status 1 (EXIT_FAILURE), so a script or a scheduler that runs it can see that it failed. Always include the file name in the message: "No such file or directory" alone does not say which file.

Parsing a P1 telegram line

A P1 telegram (see Anatomy of a telegram) is a block of text lines such as 1-0:1.7.0(01.234*kW), the power currently drawn. Firmware usually stores 1.234 kW as the integer 1234 W, to avoid floating point. sscanf with a literal prefix, two integer conversions with a maximum width and %n (which stores how many characters were consumed) does most of the work; a length check and a digit check make it strict:

#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

/* Parses "1-0:1.7.0(kk.www*kW)" into watts. */
static bool parse_power(const char *line, int *watts)
{
    int kw = 0;
    int fraction = 0;
    int used = 0;
    if (sscanf(line, "1-0:1.7.0(%2d.%3d*kW)%n", &kw, &fraction, &used) != 2) {
        return false;
    }
    if (used == 0 || line[used] != '\0' || strlen(line) != 20) {
        return false;               /* not the whole line */
    }
    for (size_t i = 10; i < 16; i++) {      /* "kk.www" must be digits */
        if (i != 12 && !isdigit((unsigned char)line[i])) {
            return false;           /* a sign or a space instead of a digit */
        }
    }
    *watts = kw * 1000 + fraction;
    return true;
}

int main(void)
{
    const char *lines[] = {
        "1-0:1.7.0(01.234*kW)",
        "1-0:1.7.0(00.045*kW)",
        "1-0:1.7.0(01.234*kWh)",
        "1-0:2.7.0(00.000*kW)",
        "1-0:1.7.0(1.2*kW)",
        "1-0:1.7.0(-1.234*kW)",
    };
    for (int i = 0; i < 6; i++) {
        int watts;
        if (parse_power(lines[i], &watts)) {
            printf("%-22s %5d W\n", lines[i], watts);
        } else {
            printf("%-22s rejected\n", lines[i]);
        }
    }
    return 0;
}
1-0:1.7.0(01.234*kW)    1234 W
1-0:1.7.0(00.045*kW)      45 W
1-0:1.7.0(01.234*kWh)  rejected
1-0:2.7.0(00.000*kW)   rejected
1-0:1.7.0(1.2*kW)      rejected
1-0:1.7.0(-1.234*kW)   rejected

%n is only stored when the literal ) before it matched, so used stays 0 for a line that ends differently. The length check and the digit check make sure both numbers had their full number of digits: %2d also accepts a single digit, and every %d also accepts a sign or leading spaces, so (-1.234*kW) or ( 1.234*kW) would otherwise get through. (1-0:2.7.0 is the power returned to the grid, a different line.) The same rule as for the CRC field on Strings and Buffer Overflows applies: accept exactly the format you expect and reject the rest.

Binary files

fwrite and fread copy raw bytes between memory and a file: fast and compact, but only readable by a program that agrees on the exact layout. Write fixed-width types (uint32_t, not int or a whole struct, whose padding and byte order depend on the platform) and check the returned count, which is the number of complete elements:

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    uint32_t quarter_wh[4] = {312, 298, 1250, 1302};

    FILE *f = fopen("day.bin", "wb");
    if (f == NULL) {
        perror("day.bin");
        return 1;
    }
    size_t written = fwrite(quarter_wh, sizeof quarter_wh[0], 4, f);
    if (fclose(f) != 0 || written != 4) {
        perror("day.bin");
        return 1;
    }

    uint32_t back[8];
    f = fopen("day.bin", "rb");
    if (f == NULL) {
        perror("day.bin");
        return 1;
    }
    size_t got = fread(back, sizeof back[0], 8, f);   /* asks for 8, file has 4 */
    printf("read %zu values, end of file: %s\n", got, feof(f) ? "yes" : "no");
    for (size_t i = 0; i < got; i++) {
        printf("%" PRIu32 " ", back[i]);
    }
    printf("\n");
    fclose(f);
    remove("day.bin");
    return 0;
}
read 4 values, end of file: yes
312 298 1250 1302

The file is 16 bytes, in this machine's byte order (little-endian). Another system that reads it must use the same order; formats meant for exchange specify one. Endianness and other portability traps are covered on The Standard Library and Portability.

Buffering

Each system call to write data costs time, so stdio collects output in a buffer and writes it in larger pieces. Buffered output is output that is held in memory by the program and only passed to the operating system when the buffer is full, when it is flushed explicitly or when the stream is closed. C uses three modes:

  • Line buffered: written at every newline. stdout is line buffered when it is a terminal.
  • Fully buffered: written in blocks of a few kilobytes. stdout is fully buffered when it goes to a file or a pipe, and so are files you open.
  • Unbuffered: written immediately. stderr is unbuffered, so that error messages are never delayed or lost.

The difference becomes visible as soon as the output is redirected, which is exactly what happens when a program runs from cron, under systemd or in a container that collects its logs. The same program, run twice:

./report              # stdout is the terminal
./report 2>&1 | cat    # stdout is a pipe
step 1: reading meters.csv
step 2: computing totals
step 3: missing.csv: No such file or directory

step 3: missing.csv: No such file or directory
step 1: reading meters.csv
step 2: computing totals

In the pipe, the two printf lines waited in the buffer while the perror line on stderr went out at once; they only appeared when the program exited and flushed stdout. A log read later then suggests that the error happened first. If the program had crashed instead of exiting normally, the buffered lines would have been lost entirely, which can hide the last thing a program did before a crash. Remedies:

  • fflush(stdout) after a line that must appear now (for example before a long computation or before a risky step).
  • setvbuf(stdout, NULL, _IOLBF, 0) at the start of main, to make stdout line buffered everywhere, or _IONBF for unbuffered.
  • Write diagnostics to stderr, and log progress there too when the order matters.