stdio.h: Streams, Files, Formatted I/O
This chapter covers <stdio.h>: streams, FILE *, the fopen/fclose/ fread/fwrite file functions, and the printf/scanf families. This is the standard I/O layer of ISO C.
Why This Matters
Every program that reads or writes data uses <stdio.h>. The buffered-stream model, formatted I/O specifiers, and their security implications are core practical knowledge.
Prerequisites
c.stdlib.1— library overview.
Core Concept
Streams and FILE
<stdio.h> models I/O as streams represented by FILE *. A stream is a buffered sequence of bytes. Three streams are opened automatically:
| Stream | Purpose |
|---|---|
stdin | standard input |
stdout | standard output |
stderr | standard error (unbuffered or line-buffered) |
File functions
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
int fflush(FILE *stream);
Modes include "r", "w", "a", "r+", "w+", "a+", with "b" for binary on some platforms.
Formatted output
int printf(const char *fmt, ...);
int fprintf(FILE *stream, const char *fmt, ...);
int snprintf(char *buf, size_t n, const char *fmt, ...);
snprintf is the safe choice: it writes at most n-1 characters plus a null terminator.
Formatted input
int scanf(const char *fmt, ...);
int fscanf(FILE *stream, const char *fmt, ...);
int sscanf(const char *s, const char *fmt, ...);
Syntax
Format specifiers (selected)
| Specifier | Type |
|---|---|
%d | int |
%u | unsigned int |
%x/%X | unsigned int in hex |
%c | char (int after promotion) |
%s | char * (null-terminated) |
%f | double |
%p | void * |
%zu | size_t |
%lld | long long |
%Lf | long double |
The printf/scanf format specifiers differ in some details (e.g., %f takes double for printf but float * for scanf).
Examples
Reading and writing a file
#include <stdio.h>
int main(void)
{
FILE *f = fopen("data.txt", "w");
if (f == NULL) return 1;
fprintf(f, "value: %d\n", 42);
fclose(f);
f = fopen("data.txt", "r");
if (f == NULL) return 1;
int v = 0;
fscanf(f, "value: %d", &v);
printf("read %d\n", v);
fclose(f);
return 0;
}
Expected output: read 42 (and data.txt contains value: 42).
Safe formatted output with snprintf
#include <stdio.h>
int main(void)
{
char buf[16];
int n = snprintf(buf, sizeof buf, "%d", 12345);
printf("wrote %d chars: %s\n", n, buf);
return 0;
}
Binary read/write
#include <stdio.h>
int main(void)
{
int data[4] = {1, 2, 3, 4};
FILE *f = fopen("data.bin", "wb");
fwrite(data, sizeof data[0], 4, f);
fclose(f);
int out[4] = {0};
f = fopen("data.bin", "rb");
fread(out, sizeof out[0], 4, f);
fclose(f);
return 0;
}
How It Works
stdio buffers data in memory to reduce the number of system calls. printf formats into the buffer; the buffer is flushed to the OS when full, when fflush is called, or at program exit. fread/fwrite move bytes between the buffer and your memory.
Variations
Buffering modes
setvbuf controls buffering (full, line, or unbuffered). stderr is typically unbuffered. This matters for interactive programs and logging.
Wide-character I/O
fwprintf, fwscanf, and friends operate on wide strings (c.stdlib.10).
Common Mistakes
- Using
%dforsize_t(use%zu). - Using
%fforfloatinprintf(it is promoted todouble;%fis
correct for printf but scanf needs %f with float *).
- Not checking
fopenforNULL. - Using
gets(removed in C11; never use it). - Forgetting that
scanfreturns the number of successful conversions.
Undefined Behavior
- Mismatched format specifiers and argument types.
VERIFIED - Passing
NULLwhere a validFILE *is required. - Using a
FILE *afterfclose. - Reading/writing beyond a buffer (e.g.,
%swith no width inscanf).
Portability
stdiois standard. Text/binary mode distinction is meaningful on some
platforms (Windows); on POSIX, text and binary are identical.
%zuis C99.
Under the Hood
FILE is an opaque struct holding a buffer, position, and flags. printf formats into the buffer; when flushed, it writes via the OS. The compiler may optimize printf("literal") into fputs or puts.
Practical Usage
- Use
snprintffor all formatted string building. - Check every
fopen/fread/fwritereturn value. - Use
stderrfor error messages (unbuffered). - For untrusted input, use bounded
%swidths inscanfor usefgets.
Exercises
1. Write a program that reads lines with fgets and prints them with line numbers. 2. Use snprintf to build a formatted string safely and verify truncation. 3. Demonstrate the %f/%lf difference between printf and scanf. 4. Write a binary file and read it back, checking fread's return value.
Deep Challenge
Implement a snprintf-style formatter for %d, %s, and %c that always null-terminates and never overflows its buffer. Explain how you handle truncation and the return value semantics (number of chars that *would* have been written).
Related Concepts
c.stdlib.3— stdlib.h (conversion functions).c.sec.3— format string attacks.c.core.33— variadic functions.
References
- ISO/IEC 9899:2018 §7.21 (stdio.h).
Verification
snprintfnull-terminates and returns would-be length.VERIFIED%zufor size_t is C99.VERIFIED- Format/argument mismatch is UB.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Streams and FILE*
- [ ] fopen/fclose
- [ ] fread/fwrite
- [ ] printf/fprintf/snprintf
- [ ] scanf family
- [ ] Format specifiers
- [ ] Buffering
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.lib.stdio | 0 | 6 |