Ch11.11: C-Style I/O
Overview
C provides a set of I/O facilities built around FILE*, along with functions such
as fopen, fclose, printf, scanf,
fprintf, and fscanf. These are the oldest and most widely used
I/O primitives in the C and C++ ecosystems.
fast_io wraps the C FILE* interface so that you can use it with
print/println/scan while keeping RAII semantics, type
safety, and performance.
Standard C I/O: <cstdio>
Before discussing how fast_io wraps C I/O, let’s review the standard C
I/O API itself. All C I/O functions are declared in <cstdio> (or
<stdio.h> in C). The core abstraction is FILE*, an opaque
pointer to a stream that represents an open file, pipe, or device.
Opening and Closing Files
#include <cstdio>
int main() {
// Open a file for writing
FILE* fp = std::fopen("output.txt", "w");
if (!fp) {
return 1; // Failed to open
}
// ... use the file ...
// Always close when done
std::fclose(fp);
}
The mode string controls how the file is opened:
"r"— read (file must exist)"w"— write (creates or truncates)"a"— append (creates or appends)"r+"— read and write (file must exist)"w+"— read and write (creates or truncates)"a+"— read and append (creates or appends)- Add
"b"for binary mode:"rb","wb", etc.
Formatted Output: printf, fprintf
#include <cstdio>
int main() {
int x = 42;
double pi = 3.14159;
char const* name = "Alice";
// printf: write to stdout
std::printf("Name: %s, Age: %d, Pi: %.2f\n", name, x, pi);
// fprintf: write to a FILE*
FILE* fp = std::fopen("data.txt", "w");
std::fprintf(fp, "x = %d, pi = %f\n", x, pi);
std::fclose(fp);
// sprintf: write to a string buffer
char buffer[100];
std::sprintf(buffer, "Value: %d", x);
}
Common format specifiers:
%dor%i— signed integer%u— unsigned integer%x,%X— hexadecimal (lowercase/uppercase)%o— octal%f— floating-point (decimal notation)%e,%E— floating-point (scientific notation)%g,%G— floating-point (shortest representation)%s— string%c— character%p— pointer%%— literal%
Formatted Input: scanf, fscanf
#include <cstdio>
int main() {
int x;
double y;
// scanf: read from stdin
std::printf("Enter two numbers: ");
std::scanf("%d %lf", &x, &y);
// fscanf: read from a FILE*
FILE* fp = std::fopen("data.txt", "r");
std::fscanf(fp, "x = %d, y = %lf", &x, &y);
std::fclose(fp);
// sscanf: read from a string
char const* input = "42 3.14";
std::sscanf(input, "%d %lf", &x, &y);
}
Warning: The scanf family is prone to buffer overflows and
format string vulnerabilities. Always validate input and never pass user-controlled strings
as format specifiers.
Character and Line I/O
#include <cstdio>
int main() {
FILE* fp = std::fopen("example.txt", "r");
// Read a single character
int ch = std::fgetc(fp);
// Read a line (up to n-1 characters or newline)
char buffer[256];
std::fgets(buffer, sizeof(buffer), fp);
// Write a single character
std::fputc('A', stdout);
// Write a string
std::fputs("Hello, World!\n", stdout);
std::fclose(fp);
}
Binary I/O
#include <cstdio>
int main() {
// Write binary data
FILE* fp = std::fopen("data.bin", "wb");
int numbers[] = {1, 2, 3, 4, 5};
std::fwrite(numbers, sizeof(int), 5, fp);
std::fclose(fp);
// Read binary data
fp = std::fopen("data.bin", "rb");
int buffer[5];
std::size_t count = std::fread(buffer, sizeof(int), 5, fp);
std::fclose(fp);
}
Standard Streams
C provides three predefined streams:
stdin— standard input (usually keyboard)stdout— standard output (usually terminal)stderr— standard error (usually terminal, unbuffered)
#include <cstdio>
int main() {
std::fprintf(stdout, "This goes to stdout\n");
std::fprintf(stderr, "This goes to stderr\n");
// printf() is equivalent to fprintf(stdout, ...)
std::printf("Same as stdout\n");
}
Appendix: Complete C I/O Function Reference
| Function | Description |
|---|---|
fopen |
Open a file |
fclose |
Close a file |
fflush |
Flush a stream’s buffer |
printf |
Formatted output to stdout |
fprintf |
Formatted output to a stream |
sprintf |
Formatted output to a string buffer |
snprintf |
Formatted output to a string buffer with size limit |
vprintf |
Formatted output to stdout (variadic) |
vfprintf |
Formatted output to a stream (variadic) |
vsprintf |
Formatted output to a string buffer (variadic) |
vsnprintf |
Formatted output to a string buffer with size limit (variadic) |
scanf |
Formatted input from stdin |
fscanf |
Formatted input from a stream |
sscanf |
Formatted input from a string |
fgetc / getc |
Read a character from a stream |
fgets |
Read a line from a stream |
fputc / putc |
Write a character to a stream |
fputs |
Write a string to a stream |
gets |
Read a line from stdin (deprecated, unsafe) |
puts |
Write a string to stdout (adds newline) |
ungetc |
Push a character back onto a stream |
fread |
Read binary data from a stream |
fwrite |
Write binary data to a stream |
fseek |
Set file position |
ftell |
Get current file position |
rewind |
Set file position to beginning |
fgetpos |
Get file position (portble) |
fsetpos |
Set file position (portable) |
feof |
Test for end-of-file |
ferror |
Test for error |
clearerr |
Clear error and end-of-file indicators |
perror |
Print error message to stderr |
remove |
Delete a file |
rename |
Rename a file |
tmpfile |
Create a temporary file |
tmpnam |
Generate a temporary filename (deprecated) |
setvbuf |
Set stream buffering mode |
setbuf |
Set stream buffer |
fast_io Wrappers for FILE*
There are four types you need to know:
-
::fast_io::c_file— an owning wrapper aroundFILE*. It closes the file automatically on destruction (RAII). This is the locked variant: every operation acquires the C stream’s internal lock, so it is safe to share across threads. -
::fast_io::c_file_unlocked— same asc_file, but it does not acquire the internal lock. Use this when you manage locking yourself (for example, with::fast_io::native_mutex) or when you know the stream is only accessed from one thread. -
::fast_io::c_io_observer— a non-owning view over aFILE*. It does not close the file on destruction. Likec_file, it is the locked variant. -
::fast_io::c_io_observer_unlocked— a non-owning view that does not lock. Use it when you control synchronization externally.
Using c_file with print/println
c_file opens a file from a path and exposes it to fast_io’s
I/O system. You can use print, println, and scan on
it just like any other fast_io file type.
#include <fast_io.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
// Open a file for writing using C's fopen under the hood.
::fast_io::c_file cf("output.txt", ::fast_io::open_mode::out);
// Use fast_io's print with the c_file wrapper.
print(cf, "Hello from c_file!\n");
::std::size_t const answer{42zu};
println(cf, "The answer is: ", answer);
// cf is closed automatically when it goes out of scope.
}
Wrapping an Existing FILE*
If you already have a FILE* from C code (for example, from a library or from
stdout), use c_io_observer to wrap it without taking ownership.
#include <cstdio>
#include <fast_io.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
// Open a file with standard C API.
FILE* fp{::std::fopen("data.txt", "w")};
if (!fp)
{
return 1;
}
{
// Wrap the raw FILE* in a non-owning observer.
::fast_io::c_io_observer ciob{fp};
print(ciob, "Written through c_io_observer\n");
::std::size_t const n{100zu};
println(ciob, "Count: ", n);
}
// You are still responsible for closing the FILE* yourself.
::std::fclose(fp);
}
To take ownership of an existing FILE*, construct a c_file with
::fast_io::from FILE or the appropriate constructor so that it is closed
automatically:
#include <cstdio>
#include <fast_io.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
FILE* fp{::std::fopen("owned.txt", "w")};
if (!fp)
{
return 1;
}
// Take ownership: c_file will call fclose on destruction.
::fast_io::c_file cf{fp};
print(cf, "Now owned by fast_io::c_file\n");
}
Thread Safety with ::fast_io::native_mutex
The locked variants (c_file and c_io_observer) rely on the C
stream’s own internal lock. If you are using the unlocked variants,
you must synchronize access yourself. ::fast_io::native_mutex provides a portable,
fast mutex for this purpose.
The key performance insight: instead of locking and unlocking the mutex for every single write, you should lock once, then use the unlocked handle to write as much data as you need, and finally release the lock. This avoids the overhead of acquiring and releasing the mutex repeatedly.
A common use case is writing to stdout. The standard streams are shared across
the whole process, so if you want to use the fast unlocked variants, you need your own
mutex.
#include <fast_io.h>
#include <fast_io_legacy.h>
::fast_io::native_mutex stdout_mtx;
void print_report()
{
// Get the raw FILE* for stdout and wrap it unlocked.
::fast_io::c_io_observer_unlocked ciob{stdout};
// Lock once, print everything, then unlock.
::fast_io::io_lock_guard guard{stdout_mtx};
// All prints inside this scope use the unlocked handle
// without any further locking overhead.
print(ciob, "=== Report ===\n");
for (int i{}; i != 100; ++i)
{
print(ciob, "Row ", i, ": ", i * i, "\n");
}
print(ciob, "=== End ===\n");
}
int main()
{
using namespace ::fast_io::iomnp;
print_report();
}
Important: Format String Safety
When you call C’s printf family, you must never pass a
runtime string directly as the format string. If the string contains a %
character, printf will try to interpret it as a format specifier, which leads
to undefined behaviour, crashes, or security vulnerabilities.
// DANGEROUS: If `user_input` contains "%s" or "%n", this is undefined behaviour.
char const* user_input{"helloworld\n"};
printf(user_input); // WRONG
// SAFE: The format string is a compile-time literal; the data is passed as an argument.
char const* user_input{"helloworld\n"};
printf("%s", user_input); // RIGHT
With fast_io, this class of bugs does not exist. print and
println do not use format strings at all — every argument is typed and
formatted independently.
#include <fast_io.h>
int main()
{
using namespace ::fast_io::iomnp;
// No format string, no vulnerability.
char const* user_input{"helloworld\n"};
// Wrap the C-string with os_c_str manipulator
print(::fast_io::mnp::os_c_str(user_input));
}
Reading with scan
Just as fscanf reads formatted input from a FILE*, you can use
scan on a c_file or c_io_observer.
#include <fast_io.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
::fast_io::c_file cf("input.txt", ::fast_io::open_mode::in);
::std::size_t a{};
::std::size_t b{};
scan(cf, a, b);
::fast_io::c_io_observer ciob_stdout{stdout};
println(ciob_stdout, "a + b = ", a + b);
}
Standard Streams: c_stdin, c_stdout, c_stderr
fast_io provides convenient functions to access the standard streams:
::fast_io::c_stdin()— returns ac_io_observerwrappingstdin::fast_io::c_stdout()— returns ac_io_observerwrappingstdout::fast_io::c_stderr()— returns ac_io_observerwrappingstderr
When you call print() or scan() without specifying a device as the first parameter,
they automatically use ::fast_io::c_stdout() and ::fast_io::c_stdin() respectively:
#include <fast_io.h>
#include <fast_io_legacy.h>
int main()
{
using namespace ::fast_io::iomnp;
// print() without device defaults to c_stdout()
print("Enter a number: ");
// scan() without device defaults to c_stdin()
::std::size_t n{};
scan(n);
// println() without device also defaults to c_stdout()
println("You entered: ", n);
// You can also be explicit:
print(::fast_io::c_stdout(), "Same as above\n");
scan(::fast_io::c_stdin(), n);
// Use c_stderr() for error messages
if (n == 0) {
print(::fast_io::c_stderr(), "Error: number is zero\n");
}
}
These functions return non-owning observers, so they don't close the underlying streams. The implicit default to stdout/stdin makes simple I/O very concise.
Key Takeaways
-
::fast_io::c_filewrapsFILE*with RAII — the file is closed automatically. -
::fast_io::c_io_observeris a non-owning view over an existingFILE*. -
The
_unlockedvariants skip internal locking. Use::fast_io::native_mutexto synchronize them manually when needed. -
Never pass a runtime string as the format argument to
printf. Always use a literal format string:printf("%s", str), notprintf(str). -
Prefer
::fast_io::print/println/scanover the C functions — they are type-safe, faster, and immune to format string attacks.