TRT
Sign in

Foundations · Module 4 · Lesson 5 of 9

Writing Text Files With ofstream

Write data to a file with std::ofstream, see how the stream buffers text before the operating system writes it, and learn to open files safely, choose truncate or append, and write records that can be read back.

FreeAbout 8 minutes

By the end you can

  • Open a file for writing, check for failure, and choose between truncating and appending
  • Explain buffering and when written data actually reaches the file
  • Write records as text with safe delimiters and enough numeric precision
  • Predict what a crash before the flush leaves in the file

Builds on Anatomy of Your First Program, Strings: C-Strings and std::string, Structs: Grouping Related Data.

01 · A question first

A write to an ofstream looks as if it goes straight into the file, but a crash a moment later can leave the file empty. Where does the text wait, when does it really reach the disk, and what does opening the file do to a file that already exists?

Make a guess, then step through the trace below to check it.

02 · Watch it run

From the stream's buffer to the filestep 1 of 5
std::ofstream out("notes.txt");out << "Ada," << 36 << "\n";out << "Lin," << 28 << "\n";out.flush();out.close();
  1. part 0stream: open(looked at in this step)
  2. part 1buffer: empty
  3. part 2file: empty

Constructing the ofstream opens the file. If notes.txt does not exist it is created, and if it does exist it is emptied. The program now holds a file descriptor and a buffer of a few kilobytes. The three cells are the stream, its buffer, and the file on disk.

Machine work: open the file; allocate the stream's buffer

03 · The idea

Start with the problem

Everything the program has built so far lives in variables, on the stack or on the heap, and all of it disappears when the program ends. To keep information, a program must write it somewhere that survives: a file.

C++ treats files the way it treats the terminal. In lesson 2 of module 1 you wrote to std::cout with <<, and a file stream, std::ofstream, accepts exactly the same operations:

cpp
#include <fstream>
 
std::ofstream out("contacts.txt");
out << "Ada," << 36 << "\n";

Three things can go wrong that have no counterpart for std::cout: the file may not be creatable, the text may not be on the disk when you think it is, and the numbers may not read back the way they were written.

How the idea works

Opening. Declared in the header <fstream>, an std::ofstream opens its file when it is constructed. A missing file is created. An existing file is truncated, which means emptied at once, before the first write. The second argument selects other behavior, and the flags can be combined with |:

cpp
std::ofstream log("log.txt", std::ios::app);     // keep the old content, add to the end

This flag type is a set of bit flags, the use of enumerations that lesson 4 mentioned.

Checking. Opening can fail, because the folder does not exist, the permissions forbid it, or the disk is full. As with the input streams of lesson 5 in module 1, the stream records the failure and tests as false:

cpp
if (!out) {
    std::cerr << "cannot open the file\n";
    return 1;
}

Returning a nonzero exit status, as lesson 2 described, tells whoever started the program that it failed.

Writing. The insertion operators work as for std::cout: numbers become their decimal text, and strings are copied as characters. A record is written by putting a delimiter between its fields and a newline at the end. A stream cannot be copied, so a function that receives one takes it by reference, as lesson 7 of module 2 advised:

cpp
void save(std::ofstream& out, const Contact& c) {
    out << c.name << ',' << c.age << '\n';
}

Buffering. The trace shows what happens underneath. An ofstream collects the characters in its own buffer, typically a few kilobytes of memory (steps 2 and 3). Only when the buffer fills, or when you call flush, or when the stream is closed, does it hand the bytes to the operating system with a single write system call (step 4). The reason is cost: a system call takes microseconds, and copying a character into a buffer takes nanoseconds, so batching makes writing fast.

Closing. The destructor of the stream closes the file when it goes out of scope, so an explicit close() is usually unnecessary. Call it, and then check the stream, when you need to know that the final write succeeded, because an error in the last flush shows up only then.

Where the file is. A relative name refers to the current working directory of the process, which is where it was started from and not where the executable lives. The header <filesystem> offers std::filesystem::current_path() to see it.

Why it is correct

One stream interface for terminals, files and strings, through the overloaded << of lesson 8 in module 2, means that code that formats data does not need to know where the data goes. A function can write to std::cout in a test and to a file in production.

Buffering is correct because the destructor and close flush it, so a well-behaved program loses nothing, and it is fast because it replaces thousands of system calls with a few. Truncating on open matches the most common intent, writing a fresh file, and the separate append mode serves logs.

What it costs

System calls. One write costs microseconds. Writing a million small pieces with a call each would take seconds, while the buffered stream takes milliseconds. Using std::endl on every line forces a flush, as lesson 2 explained, and writing a large file that way is often ten times slower or more.

Durability. After a flush, the data is in the operating system's cache, not necessarily on the disk. If the machine loses power, it can vanish. Making it durable requires a platform call such as fsync, which costs milliseconds and is outside standard C++.

Text size and speed. A number written as text takes up to 10 characters for a 32-bit integer and must be parsed again on reading. Converting a double to text takes on the order of 100 nanoseconds.

Pitfalls and variations

Unchecked opens. A stream that failed to open silently ignores every write. Always check.

Truncating by accident. Opening a file for writing destroys its content immediately, even if the program later crashes before writing anything. For important data, write a temporary file and rename it over the old one when done.

Lost data. A crash, a kill signal or a call to std::abort skips the destructor, and unflushed text is lost.

Delimiters in the data. A name such as Smith, John breaks a comma-separated line. Quote and escape the fields, choose a delimiter that cannot occur, or write the length of each field first.

Precision. The default output of a double has 6 significant digits, so 1.0 / 3.0 is written as 0.333333. To read the value back exactly, write it with std::setprecision(17), from the header <iomanip>, or use the C++20 function std::format, whose default is the shortest text that reads back exactly.

Raw struct bytes. Do not write a struct's bytes directly, for the padding and layout reasons of lesson 2.

Newlines. On Windows, a text-mode stream turns the newline into two characters when writing.

Variations. std::ostringstream builds text in memory with the same operators. std::fstream opens a file for reading and writing. The standard library also offers std::filesystem::exists and std::filesystem::rename for managing the files themselves.

Where it is used

Programs write logs, configuration, exports in CSV and similar formats, save files and reports. The habits of this lesson, checking each open, flushing at meaningful moments, and choosing a safe format, apply equally to formats far larger than a text file.

The next lesson, "Reading Text Files and Handling Errors With ifstream", reads such a file back, line by line, and deals with what can be wrong in it.

04 · Prove it

Question 1 of 4 · answer from memory

A program runs `std::ofstream out(name);` and the file already holds 1 MB of text. What is in the file right after that line?

05 · Practice

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save