To write a file in C++, include <fstream>, construct a std::ofstream with the file name, check that it opened, then send data with <<. The destructor closes and flushes the file for you when the object goes out of scope, so you rarely need to call close() at all.
#include <fstream>. 2. std::ofstream out("notes.txt"); 3. if (!out) { /* handle the failure */ } 4. out << "text"; 5. Let the destructor close it. Skipping step 3 is the single most common reason a program reports success and writes nothing.Every example below was compiled with g++ -std=c++20 -Wall -Wextra -pedantic on GCC 13.3 and run, and the output you see is the real output. The one C++23 example is marked as such. If you are new to C++ file I/O, read the sections in order. If you already write files and something is broken, jump to the troubleshooting section.
The shortest version that works
Here is the whole idea in twelve lines. Note the error check on line five, which most tutorials leave out.
#include <fstream>
#include <iostream>
int main() {
std::ofstream out("notes.txt");
if (!out) {
std::cerr << "Could not open notes.txt for writing\n";
return 1;
}
out << "First line\n";
out << "Second line, number = " << 42 << '\n';
// out closes itself here, flushing whatever is still buffered
std::cout << "Wrote notes.txt\n";
}std::ofstream is an output file stream. It behaves like std::cout, which means every operator<< you already know works on it, including the ones you write yourself. Making your own types printable and writable is what overloading the stream insertion operator in C++ is for.
Three ways to open the file
The constructor form is the one to use by default. The .open() form exists for cases where you do not know the file name yet. The third form is not a different API at all, just a scope you create on purpose so the file closes at a point you control.
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. Constructor opens the file
std::ofstream a("a.txt");
// 2. Default-construct, then .open() later
std::ofstream b;
b.open("b.txt", std::ios::app);
// 3. RAII scope: the file is closed at the closing brace
{
std::ofstream c("c.txt");
c << "written and closed before the next line runs\n";
}
std::ifstream check("c.txt");
std::string line;
std::getline(check, line);
std::cout << "c.txt contains: " << line << '\n';
if (a && b) std::cout << "a.txt and b.txt opened fine\n";
}That prints c.txt contains: written and closed before the next line runs, which proves the point. Because the braces ended, the data was flushed and the file was closed before the read happened.
Open modes and what each one really does
A second argument to the constructor or to .open() controls how the file is opened. These are bit flags, so you combine them with |.
| Flag | What it does | Missing file? |
|---|---|---|
std::ios::out | Default for ofstream. Opens for writing and truncates existing content to zero bytes. | Created |
std::ios::app | Every write goes to the end of the file, even if you seek elsewhere first. The mode for log files. | Created |
std::ios::trunc | Explicitly empties the file. Redundant with a plain out, useful combined with in|out. | Created |
std::ios::binary | No character translation. On Windows this is what stops '\n' becoming CRLF. | Created |
std::ios::ate | Seeks to the end once, at open time. Later seeks move freely, so this is not the same as app. | Depends on the other flags |
std::ios::in | std::ios::out | Read and write in place, overwriting from the current position and keeping the rest of the file. | Open fails |
The last two rows surprise people, so here they are running. Each dump() call prints the file contents with each line in brackets.
#include <fstream>
#include <iostream>
#include <string>
void dump(const char* label, const char* path) {
std::ifstream in(path);
std::cout << label << ": ";
std::string line;
while (std::getline(in, line)) std::cout << '[' << line << ']';
std::cout << '\n';
}
int main() {
{ std::ofstream f("log.txt"); f << "run 1\n"; } // default: out|trunc
dump("after first write ", "log.txt");
{ std::ofstream f("log.txt"); f << "run 2\n"; } // truncated again
dump("after plain reopen ", "log.txt");
{ std::ofstream f("log.txt", std::ios::app); f << "run 3\n"; }
dump("after ios::app ", "log.txt");
// ios::ate seeks to the end but does NOT force later writes to the end
{ std::ofstream f("log.txt", std::ios::in | std::ios::out | std::ios::ate);
f << "run 4\n"; }
dump("after in|out|ate ", "log.txt");
// in|out without ate overwrites from byte 0 and keeps the rest
{ std::ofstream f("log.txt", std::ios::in | std::ios::out);
f << "XX\n"; }
dump("after in|out at 0 ", "log.txt");
}after first write : [run 1]
after plain reopen : [run 2]
after ios::app : [run 2][run 3]
after in|out|ate : [run 2][run 3][run 4]
after in|out at 0 : [XX][ 2][run 3][run 4]Look at that last line. Writing three bytes at position zero replaced run with XX plus a newline, leaving 2 stranded on its own line. Overwrite mode does not shift bytes for you. And opening a file that does not exist with in | out fails outright, which catches people who expect it to behave like plain out.
Writing with <<, write() and put()
Three different jobs. operator<< formats a value as text and honors manipulators and the stream’s locale. put() writes exactly one character with no formatting. write() pushes a block of bytes and adds nothing, not even a terminator.
#include <fstream>
#include <iostream>
#include <string>
int main() {
std::ofstream out("modes.txt");
// operator<< : formatted text output, applies locale and manipulators
out << "count=" << 7 << " ratio=" << 0.5 << '\n';
// put() : one character, unformatted
out.put('X');
out.put('\n');
// write() : raw block of bytes, no formatting, no null terminator
std::string blob = "raw bytes, no newline added";
out.write(blob.data(), static_cast<std::streamsize>(blob.size()));
out.put('\n');
out.close();
std::ifstream in("modes.txt");
std::string line;
while (std::getline(in, line)) std::cout << "[" << line << "]\n";
}'\n' over std::endl. Both write a newline, but std::endl also flushes the stream. Flushing on every line turns one buffered write into thousands of syscalls, which is a measurable slowdown on large files and buys you nothing the destructor does not already do.Flushing, closing, and what RAII buys you
An ofstream buffers. Data you write sits in memory until the buffer fills, you call flush(), you call close(), or the object is destroyed. This program checks the on-disk size before and after a flush.
#include <fstream>
#include <iostream>
int main() {
std::ofstream out("buffered.txt");
out << "line written to the buffer\n";
// Nothing is on disk yet on most implementations.
std::ifstream peek1("buffered.txt", std::ios::ate);
std::cout << "before flush: " << peek1.tellg() << " bytes on disk\n";
out.flush();
std::ifstream peek2("buffered.txt", std::ios::ate);
std::cout << "after flush: " << peek2.tellg() << " bytes on disk\n";
}On GCC 13.3 that prints before flush: 0 and after flush: 27. This is the mechanism behind the classic empty-file bug: the program crashed, called std::exit, or was killed before the destructor ran. RAII covers the normal path and the exception path, because destructors run during stack unwinding. It cannot help if the process dies outright, so for long-running writers, flush at meaningful checkpoints.
Writing formatted numbers and a vector of records
Default stream formatting drops trailing zeros, so 92500.0 lands in the file as 92500. If another program parses the file, pin the format down with std::fixed and std::setprecision from <iomanip>.
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
struct Employee {
std::string name;
int id{};
double salary{};
};
int main() {
std::vector<Employee> staff = {
{"Dana Whitfield", 1041, 92500.0},
{"Miguel Ortiz", 1042, 78250.5},
{"Priya Raman", 1043, 105000.0}
};
std::ofstream out("staff.csv");
if (!out) {
std::cerr << "cannot open staff.csv\n";
return 1;
}
// Without this, 92500.0 is written as "92500" and 105000.0 as "105000".
out << std::fixed << std::setprecision(2);
out << "name,id,salary\n";
for (const Employee& e : staff)
out << e.name << ',' << e.id << ',' << e.salary << '\n';
out.close();
if (!out) {
std::cerr << "write or close failed\n";
return 1;
}
std::ifstream in("staff.csv");
std::string line;
while (std::getline(in, line)) std::cout << line << '\n';
}Notice the second error check, after close(). A stream can open fine and still fail mid-write, most often because the disk filled or a quota was hit. The reverse trip, turning that CSV back into objects, is covered in storing text file data into a vector in C++.
Binary writes, and the portability caveat
For a compact file you can reload quickly, write the raw object bytes. The trick is reinterpret_cast to const char*, because write() deals in characters.
#include <cstdint>
#include <fstream>
#include <iostream>
#include <vector>
struct Sample {
std::int32_t id;
double value;
};
int main() {
std::vector<Sample> samples = {{1, 3.5}, {2, -0.25}, {3, 1e6}};
std::ofstream out("samples.bin", std::ios::binary);
if (!out) return 1;
out.write(reinterpret_cast<const char*>(samples.data()),
static_cast<std::streamsize>(samples.size() * sizeof(Sample)));
out.close();
std::ifstream in("samples.bin", std::ios::binary);
std::vector<Sample> back(samples.size());
in.read(reinterpret_cast<char*>(back.data()),
static_cast<std::streamsize>(back.size() * sizeof(Sample)));
std::cout << "sizeof(Sample) = " << sizeof(Sample) << " bytes\n";
for (const Sample& s : back)
std::cout << s.id << " -> " << s.value << '\n';
}sizeof(Sample) printed 16 here, not 12, because of the four padding bytes before the double. Change a compiler flag, a member order, or the target architecture and the file becomes garbage. The technique is also only defined for trivially copyable types, so a struct containing a std::string is out. Use it for a local cache you can regenerate, never for an interchange format.Paths and directories with std::filesystem
Since C++17, <filesystem> handles path joining, directory creation and size checks without platform #ifdef blocks. ofstream accepts a fs::path directly. Note that ofstream will never create a missing directory for you.
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path dir = fs::current_path() / "reports" / "2026-07";
std::error_code ec;
fs::create_directories(dir, ec);
if (ec) {
std::cerr << "could not create " << dir << ": " << ec.message() << '\n';
return 1;
}
fs::path file = dir / "summary.txt";
std::ofstream out(file);
if (!out) {
std::cerr << "could not open " << file << '\n';
return 1;
}
out << "report body\n";
out.close();
std::cout << "absolute path: " << fs::absolute(file) << '\n';
std::cout << "exists: " << std::boolalpha << fs::exists(file) << '\n';
std::cout << "size: " << fs::file_size(file) << " bytes\n";
}Use the / operator to join path components rather than concatenating strings. It picks the right separator per platform, so you never hand-escape a Windows path like C:\\data\\out.txt. If you do need a literal Windows path in source, either double every backslash or use a raw string literal: R"(C:\data\out.txt)". The full reference is the cppreference filesystem library documentation.
Turning stream errors into exceptions
If you would rather not check a return value after every operation, tell the stream to throw. Pass a mask of the states you care about to exceptions(). failbit covers logical failures like a failed open; badbit covers unrecoverable I/O errors.
#include <fstream>
#include <iostream>
int main() {
std::ofstream out;
out.exceptions(std::ios::failbit | std::ios::badbit);
try {
out.open("/definitely/not/a/real/dir/data.txt");
out << "this line never runs\n";
} catch (const std::ios_base::failure& e) {
std::cerr << "stream error: " << e.what() << '\n';
std::cerr << "code: " << e.code().message() << '\n';
return 1;
}
}Be warned that the message quality is poor. On GCC 13.3 this prints basic_ios::clear: iostream error, which tells you nothing about which file or why. That is a long-standing libstdc++ limitation. When you need a real reason, check errno or use std::filesystem calls, which report a proper std::error_code.
ofstream vs fprintf vs std::format vs std::print
Some readers searching for this actually mean C, not C++. The C way still compiles in every C++ program and is worth knowing, because plenty of existing code uses it.
#include <cstdio>
int main() {
std::FILE* f = std::fopen("legacy.txt", "w");
if (!f) {
std::perror("fopen");
return 1;
}
std::fprintf(f, "id=%d name=%s ratio=%.3f\n", 17, "sensor-a", 0.4267);
std::fputs("second line\n", f);
if (std::fclose(f) != 0) {
std::perror("fclose");
return 1;
}
f = std::fopen("legacy.txt", "r");
char buf[128];
while (std::fgets(buf, sizeof buf, f)) std::fputs(buf, stdout);
std::fclose(f);
}And here is the C++23 option, which pairs printf-style brevity with compile-time checked format strings. It needs GCC 14 or newer, or MSVC 19.37 or newer, because that is when <print> landed in the standard libraries. This one was compiled with g++-14 -std=c++23.
#include <cstdio>
#include <print>
int main() {
std::FILE* f = std::fopen("out.txt", "w");
if (!f) return 1;
std::print(f, "id={} value={:.2f}\n", 7, 3.14159);
std::println(f, "second line");
if (std::fclose(f) != 0) return 1;
std::println("wrote out.txt");
}| Approach | Since | Format string checked? | Use it when |
|---|---|---|---|
std::ofstream with << | C++98 | Type-safe by construction | Default choice. Works everywhere, extensible to your own types, RAII closes it. |
std::fprintf | C89 | Only via compiler warnings | You are writing C, or maintaining code that already uses FILE*. |
std::format into a stream | C++20 | Yes, at compile time | You want tidy layout control today and cannot require a C++23 library. |
std::print / std::println | C++23 | Yes, at compile time | GCC 14+ or MSVC 19.37+. Shortest correct code, and Unicode-aware on Windows consoles. |
Troubleshooting the five failures that cost the most time
The file is created but empty. You never flushed and the destructor never ran, usually because of a crash, a std::exit call, or an abort while the stream was still alive. Add an explicit out.flush() at a checkpoint and see if the data appears.
Permission denied. The open silently sets failbit and you did not check it. Confirm with if (!out), then check the directory permissions rather than the file’s. You need write and execute on the containing directory to create a file there. Writing under /var or C:\Program Files without elevation fails for this reason.
Extra carriage returns, or missing ones. In text mode on Windows, every '\n' you write becomes CRLF on disk. On Linux and macOS it stays LF. If a checksum differs between platforms, or a parser sees stray \r characters, open with std::ios::binary and write the exact bytes you intend.
The file went somewhere you cannot find. A relative path resolves against the process working directory, not the source file and not the executable. Run std::cout << std::filesystem::current_path() once and the mystery ends. IDEs are the usual culprit: Visual Studio defaults the working directory to the project folder, not the Debug output folder.
You appended when you meant to truncate, or the reverse. Plain std::ofstream out(path) destroys the old contents with no warning. If a run wiped a file you cared about, that is why. Conversely, if a file keeps growing across runs, something is passing std::ios::app.
Frequently asked questions
Do I need to call close() on an ofstream?
No, in normal code. The destructor closes and flushes when the object goes out of scope, including when an exception unwinds the stack. Call close() explicitly only when you want to check the result of the close, or when you need the file released before the scope ends so another process can read it.
How do I append to a file instead of overwriting it in C++?
Pass std::ios::app as the second argument: std::ofstream log("app.log", std::ios::app);. Every write then goes to the end of the file regardless of any seek you perform, which makes it the correct mode for logs. If the file does not exist, it is created.
Why is my C++ output file empty even though the program ran?
Almost always buffering. The data was still in memory when the process ended abnormally, so nothing reached disk. Let the ofstream go out of scope normally, or call flush() at checkpoints. The other cause is a failed open you never checked, which leaves an empty or missing file.
Can std::ofstream create a directory that does not exist?
No. Opening reports/out.txt when reports/ is missing simply fails. Call std::filesystem::create_directories(path.parent_path()) first, then open the file. That function creates every missing level and returns quietly if the directory already exists.
Should I use ofstream or fprintf for writing files in C++?
Use ofstream. It is type-safe, it closes itself, and it works with operators you define for your own classes. Reach for fprintf only in C code or an existing FILE* codebase. On a C++23 toolchain, std::print gives you the concise syntax without the type-safety hole.
What is the fastest way to write a large file in C++?
Open in binary mode, avoid std::endl, and batch your data into fewer, larger write() calls instead of many small << operations. Enlarging the stream buffer with pubsetbuf before opening can help too, though the gains are implementation-dependent.
Wrapping up
For nearly every case, the answer is four lines: construct an ofstream, check it with if (!out), write with <<, and let scope close it. Add std::ios::app for logs, std::ios::binary when the exact bytes matter, and std::filesystem whenever a path has more than one component.
The part worth internalizing is the error check. It costs three lines and it is the difference between a program that tells you the disk is full and one that reports success while producing nothing. If you expect to be asked about this code in an interview, being able to say plainly why the check matters is exactly the kind of reasoning covered in explaining your coding solution in an interview. And if your next task involves matching file names or header keys, read comparing two strings while ignoring case in C++, where the naive version has a genuine undefined-behavior bug.

