Input, Output and Files
printf and its conversion specifications, reading input safely with fgets and sscanf, opening, reading and writing files with stdio, buffering, and the three standard streams.
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 ofmain, to make stdout line buffered everywhere, or_IONBFfor unbuffered. - Write diagnostics to stderr, and log progress there too when the order matters.