std::ofstream is the output counterpart to the std::ifstream from the previous chapter. It's a stream that sends characters to a file on disk instead of to the terminal. Once it's open, writing to it with << works the same way as writing to std::cout, and the C++ runtime takes care of buffering the data and flushing it to the file. This chapter covers how to open a file for writing, the formatted and single-character write paths, the difference between "\n" and std::endl, when to call flush(), how to detect failed writes, and the default truncating behaviour. The full set of open modes (append, binary, and others) is the subject of the next chapter.
To open a file for writing, construct an std::ofstream and pass it a filename. The class lives in the <fstream> header, the same header used in the File Reading chapter.
Those five lines do several things. The constructor std::ofstream out("products.txt") does three things at once: it builds an ofstream object on the stack, asks the operating system to open or create products.txt, and binds the stream to that file. If anything fails (permission denied, read-only filesystem, invalid path), the stream is constructed in a failed state and is_open() returns false. Always check.
The other way to attach a stream to a file is to default-construct first and call open() later. This helps when the filename isn't known at construction time, or when the same stream object is reused for several files in sequence.
close() flushes any buffered data and releases the file handle. Calling it explicitly is usually not needed. The ofstream destructor runs when the variable goes out of scope (at the end of main() here), and that destructor does the same thing: flush, then close. That's the RAII pattern. The stream owns the file handle, and when the stream dies, the handle is released. This behaviour comes automatically, the same way std::vector frees its buffer.
One default behaviour matters up front: opening a file with std::ofstream truncates it. If products.txt already had content, it's wiped to zero bytes the moment the file opens. The append mode that adds to an existing file (instead of replacing it) is a separate option. For now, every example in this chapter overwrites the file on each run.
Once the stream is open, write to it with the same << operator used for std::cout. The operator is overloaded for every fundamental type, for std::string, and for any user-defined type with a matching operator<< overload.
The loop writes one product name per line. The body looks identical to std::cout << name << "\n";. Same operator, same syntax, different destination. That symmetry is the point of the stream design: write the data once, choose where it goes by picking which stream object to write to.
To confirm the file contains what was written, open it back up with an ifstream and read it line by line.
Writing and reading back is the easiest way to confirm a file's contents while learning. It works regardless of how the file was created, and it doesn't depend on any operating-system tools.
Chained << calls can mix types in a single line, like with std::cout. The stream converts numbers to their text representation automatically.
Each piece flows through the operator chain left to right. The integer 3 becomes the characters "3", the double 124.95 becomes the characters "124.95". Controlling exactly how many decimal places a double prints requires the manipulators (std::setprecision, std::fixed). For now, the default formatting is fine.
put(c) writes one character to the stream. It's the explicit character version of <<. Everyday formatted output rarely needs it, but it helps for a char value (not a string, not an integer) that needs to be written as a literal byte.
Why not use out << status? Because << is overloaded for char, and the overload writes the character correctly. The two are equivalent for char. The difference matters if the variable's type is int rather than char. With out << someInt;, the integer gets formatted as its decimal text representation. With out.put(someInt), the integer is interpreted as a single byte value and that byte is written.
put is the right tool for "write this exact byte", not "format this number as text". Binary writes (raw byte arrays, structs, image data) use a different function called write(). put is the one-character variant.
Two ways to end a line of output. They look interchangeable. They're not.
"\n" is a string containing a single newline character. Writing it to the stream sends that one byte to the stream's internal buffer.
std::endl is a manipulator. It writes a newline and then flushes the stream. Flushing means asking the stream to push any buffered data out to the operating system right now, instead of waiting until the buffer fills up or the stream closes.
Both lines appear in the file. The difference is invisible in the final output. Where it matters is timing and performance.
std::endl forces a flush on every call. For one-off writes that doesn't matter. For a loop writing thousands of lines, every flush is a system call, and that turns what could be one batched write into thousands of tiny ones. Use "\n" for bulk writes and let the stream's buffer do its job. Reserve std::endl for places where the data needs to reach disk right now (an interactive log being tailed, a checkpoint before risky work).
A code example that demonstrates the cost: writing a hundred thousand log lines.
The version with "\n" writes to the stream's buffer (typically 4-8 KB), and the operating system only sees a write call when the buffer fills. The version with std::endl writes one line, flushes, writes one line, flushes, and so on. Same final file, much different runtime.
When is std::endl the right call? Interactive logs being watched with tail -f, debug traces where a crash might prevent the destructor from flushing, and checkpoint writes before a long-running computation. Everywhere else, prefer "\n".
To flush explicitly without writing a newline, call flush() directly.
Without that explicit flush(), a crash before the second << would leave the buffer holding "Starting batch job" and the file empty. With it, the first line is safely on disk before the risky work starts.
Reads can fail because the data ran out. Writes can fail too, for different reasons: the disk is full, the file was opened on a network share that went down, the process has lost write permission, or a quota was hit. The stream tracks these failures with its state-flag system.
The flags that matter for output:
| Flag | Meaning |
|---|---|
good() | No error flags set. The stream is healthy. |
fail() | A formatting or logical error happened (write didn't produce the expected result). |
bad() | A serious error happened (the stream is unusable, often due to an I/O error). |
eof() | End-of-file reached (mostly relevant for input streams). |
For output, fail() and bad() are the two to check. fail() is also set whenever the stream is in any kind of error state, so testing the stream's truthiness (if (!out)) is the catch-all check.
The interesting question is when to check. After every << is overkill: stream error flags are sticky, so once a write fails the stream stays in the failed state and every subsequent << is a no-op. Writing a whole block of output and checking the stream state once at the end covers the common cases (out of space, lost connection) without cluttering the code.
There's a second moment to check: after close() (or after the stream goes out of scope, if the check happens before that point). The destructor's flush is the last chance for buffered data to fail, and the failure can happen there even if every << looked successful.
Why does the close-time check matter? Because the bytes written inside the loop might still be sitting in the stream's buffer, never touched the operating system yet. The failure that surfaces during the close is the disk reporting that it ran out of space when the runtime finally tried to write that buffer down. A check after each << would miss it.
This is the one default behaviour to keep firmly in mind. With:
The file orders.txt is opened for writing and truncated to zero bytes. Any content it had before the open is gone. The next byte written goes at position 0.
This is fine when the program's job is to produce a fresh file on every run (regenerate a report, dump a snapshot, write a build artifact). It's not fine when the program's job is to add to a log without erasing the history. For that, use append mode, which opens the file for writing but positions the write cursor at the end and leaves the existing bytes intact.
An illustration of the truncating default, so the behaviour isn't surprising later:
Two runs, only the last one's content survives. To get both lines in the file, use std::ios::app. For this chapter, every example uses the default truncating mode by design.
A larger example exercises every piece of the chapter together. The program builds a small list of orders, writes them to a file using a mix of << and put(), checks for write failures, and then reads the file back to confirm the contents made it.
A few details matter. The header line uses "\n" rather than std::endl because the program is doing bulk writes and there's no reason to flush after each line. The status character is written with put() because the Order::status field is a char, and using put makes the intent ("this single byte is the status code") clearer than << o.status. The explicit close() plus the out.fail() check after it catches any failure that surfaced during the final flush. The read-back loop at the end uses the standard ifstream and std::getline pattern.
One detail to watch: Dana's total is 219.5 in the file, not 219.50. The default formatting for double drops trailing zeros. Controlling that requires std::fixed and std::setprecision. For a CSV that a downstream program will parse as a number, the trailing zero doesn't matter; for a human-readable receipt where money should always have two decimal places, it does. The mechanism for fixing it is the same set of manipulators that work with std::cout.
The data flow for this program, end to end:
The orders vector goes into the output stream, which holds the bytes in its buffer until a flush. The final close (explicit or via the destructor) commits the buffer to disk. The input stream then opens the same file and feeds the bytes back through getline to std::cout. Each arrow is a few lines of code, and the picture is the entire shape of file output in C++: build the data, hand it to a stream, let the stream and the operating system carry it the rest of the way.
Everything in this chapter is formatted text output. << converts numbers to their decimal text representation, put() writes one character, and the file is human-readable. That's almost always the desired outcome.
The other path is binary output: writing the raw bytes of a value or buffer to the file without any text conversion. The function for that is write(buffer, size), and it takes a const char* plus a byte count. Binary writes are the right tool for image data, serialised structs, audio samples, and anything else where text formatting would be both wasteful and lossy. This chapter sticks to formatted text.
The pieces above cover creating a file, writing formatted lines to it, and detecting failures. The remaining piece is how to open the file: truncate, append, fail if it already exists, treat as binary? The next chapter lays out the full set of flags (std::ios::out, std::ios::app, std::ios::trunc, std::ios::binary, std::ios::in, std::ios::ate) and what happens when they combine.
9 quizzes