Skip to content
C++
Library
since C++98
Basic

Cstdio

<cstdio> exposes C stdio (printf, fopen, snprintf, fread) in namespace std: formatted and binary file I/O, its pitfalls, and modern C++ alternatives.

<cstdio>since C++98

The C++ header that declares the C standard I/O library (FILE, printf, fopen, fread, and related functions) in namespace std, with the same semantics as C's <stdio.h>.

Overview

<cstdio> is the C++ wrapper around C's <stdio.h>. It guarantees the declarations exist in std::; whether they are also in the global namespace is implementation-defined, so portable code should write std::printf and std::fopen. <stdio.h> remains valid in C++ and, in practice, puts names in the global namespace, with std:: availability not guaranteed by older standards.

The library is built around the opaque std::FILE stream type. It provides:

  • Formatted output: printf, fprintf, sprintf, snprintf, and the v* variants taking va_list.
  • Formatted input: scanf, fscanf, sscanf.
  • Character and line I/O: fgetc, fputc, fgets, fputs, puts, ungetc.
  • Binary I/O: fread, fwrite, fseek, ftell, fgetpos, fsetpos, rewind.
  • Stream management: fopen, freopen, fclose, fflush, setvbuf, tmpfile.
  • File operations: remove, rename, tmpnam.
  • Error state: feof, ferror, clearerr, perror.
  • Standard streams: stdin, stdout, stderr, plus EOF, BUFSIZ, FILENAME_MAX, SEEK_SET/CUR/END.

Version notes:

  • std::snprintf, std::vsnprintf, std::vsscanf, and the %zu/%lld length modifiers entered C++ in C++11 (from C99).
  • std::gets was deprecated in C++11 and removed in C++14. Never use it.
  • The exclusive mode flag "x" for fopen (fail if the file exists) is available since C++17 (from C11).
  • std::print / std::println (C++23, <print>) write to a FILE* and are the modern replacement for most fprintf use.

<cstdio> is not constexpr-friendly and not thread-neutral: each FILE has an internal lock, so individual calls are thread-safe, but sequences of calls interleave unless you hold the lock with POSIX flockfile.

Syntax

cpp
#include <cstdio>

std::FILE* std::fopen(const char* filename, const char* mode);
int        std::fclose(std::FILE* stream);
std::size_t std::fread(void* buf, std::size_t size, std::size_t count, std::FILE* stream);
std::size_t std::fwrite(const void* buf, std::size_t size, std::size_t count, std::FILE* stream);
int        std::fprintf(std::FILE* stream, const char* format, ...);
int        std::snprintf(char* buf, std::size_t n, const char* format, ...); // since C++11
char*      std::fgets(char* buf, int n, std::FILE* stream);
int        std::fseek(std::FILE* stream, long offset, int origin);

Mode strings: "r", "w", "a", with optional + (update) and b (binary). "wx" (C++17) fails if the file exists. Failure returns nullptr with errno set.

Examples

RAII wrapper with unique_ptr

FILE* is a raw resource; wrap it so every path closes it.

cpp
#include <cstdio>
#include <memory>      // C++11 for unique_ptr
#include <vector>
#include <string>

struct FileCloser {
    void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); }
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>; // C++11

FilePtr open_file(const char* path, const char* mode) {
    return FilePtr{std::fopen(path, mode)};
}

std::vector<unsigned char> read_all(const char* path) {
    auto f = open_file(path, "rb");
    if (!f) return {};

    std::fseek(f.get(), 0, SEEK_END);
    long len = std::ftell(f.get());
    std::rewind(f.get());
    if (len < 0) return {};

    std::vector<unsigned char> data(static_cast<std::size_t>(len));
    std::size_t got = std::fread(data.data(), 1, data.size(), f.get());
    data.resize(got);              // short read is possible
    return data;
}

A stateless deleter type keeps sizeof(FilePtr) == sizeof(FILE*); a std::function deleter or shared_ptr<FILE> works but costs extra.

Safe formatting with snprintf

cpp
#include <cstdio>
#include <string>

std::string format_id(const char* prefix, unsigned long id) {
    // C++11: snprintf returns the length that WOULD have been written
    int n = std::snprintf(nullptr, 0, "%s-%08lx", prefix, id);
    if (n < 0) return {};

    std::string out(static_cast<std::size_t>(n), '\0');
    // buffer needs n+1 for the terminator; std::string guarantees it since C++11
    std::snprintf(out.data(), out.size() + 1, "%s-%08lx", prefix, id); // data() non-const since C++17
    return out;
}

The two-pass idiom (measure, then write) avoids truncation. out.data() returning char* requires C++17; on C++11/14 use &out[0].

Line-oriented reading with proper error checks

cpp
#include <cstdio>
#include <cstring>
#include <string>

bool for_each_line(const char* path, void (*fn)(const std::string&)) {
    std::FILE* f = std::fopen(path, "r");
    if (!f) { std::perror(path); return false; }

    char buf[512];
    std::string line;
    while (std::fgets(buf, sizeof buf, f)) {
        line += buf;
        if (line.back() == '\n') {          // full line assembled
            line.pop_back();
            fn(line);
            line.clear();
        }
    }
    if (!line.empty()) fn(line);            // last line without newline

    bool ok = !std::ferror(f);
    std::fclose(f);
    return ok;
}

Best Practices

  • Prefer streams or std::format/std::print for new code. Streams are type-safe and extensible. Since C++20, std::format gives type-checked format strings; since C++23, std::print(FILE*, ...) writes them directly to stdio streams.
  • Use <cstdio> where it fits: interop with C libraries, FILE* handed to you by an API, binary I/O with predictable performance, fread/fwrite into preallocated buffers, setvbuf tuning, and stderr logging in crash paths.
  • Always RAII-wrap FILE*. An early return or exception between fopen and fclose leaks the handle. If you wrap it in a class, delete copy operations (or make it move-only); a copyable wrapper double-closes.
  • Check every return value: fopen for nullptr, fread/fwrite for short counts, and fclose on written files, because buffered data is flushed at close and failures often surface only there.
  • Use snprintf, never sprintf. Pair it with compiler format checking (__attribute__((format(printf, ...))) on GCC/Clang) for your own variadic wrappers.
  • Disambiguate with ferror/feof after a short read: feof means end of input, ferror means an I/O failure.
  • Mixing iostream and stdio: with sync_with_stdio(true) (the default) output interleaves correctly, at a speed cost. Call std::ios::sync_with_stdio(false) only if you never touch stdout/stdin through stdio.

Common Pitfalls

  • Format/argument mismatch is undefined behavior. printf("%d", 3.0) or %s with a std::string compiles (with at most a warning) and misbehaves. Use %zu for size_t (C++11), %lld for long long, and .c_str() for strings. Passing non-trivial class types through ... is conditionally supported at best.
  • Narrow-type promotion: float promotes to double and small integers to int in variadic calls; scanf receives pointers, so %f into a double* is wrong (needs %lf).
  • scanf("%s", buf) has no bounds checking. Always specify a width (%31s) or prefer fgets plus parsing (std::from_chars since C++17).
  • gets is gone. It was removed in C++14; code that still compiles it is relying on a non-conforming mode.
  • Macro side effects: getc and putc may be macros that evaluate the stream argument more than once, so getc(*files++) is a bug. Use fgetc/fputc when the argument has side effects.
  • Mixed reads and writes on "r+"/"w+" streams require an intervening fflush or a positioning call (fseek, rewind); otherwise behavior is undefined.
  • Text vs. binary mode: on Windows, "r" translates \r\n, which corrupts binary data and makes ftell offsets unreliable. Use "rb"/"wb".
  • ftell returns long, which is 32-bit on Windows and limits files to 2 GB; use _ftelli64 or POSIX fseeko/ftello for large files, or std::filesystem::file_size (C++17) for size queries.
  • tmpnam is racy (check-then-create). Use tmpfile() or POSIX mkstemp.
  • Use after fclose, or fclose twice, is undefined behavior. A FILE* copied into two owners invites exactly this.
  • Not exception-aware: stdio reports errors through return codes and errno, and never throws. Callbacks and destructors that call into it must propagate failures deliberately.

See Also

  • std::unique_ptr with custom deleters (C++11) for owning FILE*
  • std::format (C++20) and std::print (C++23) as type-safe printf replacements
  • std::filesystem (C++17) for remove, rename, and size/metadata operations beyond what <cstdio> offers
  • <fstream> and <iostream> for stream-based I/O
  • <cstdarg> for writing your own variadic functions that forward to vfprintf