Contents

Chapter 15

Files: Making Your Program Remember

ifstream, ofstream, and reading a file line by line.

The contact book from chapter 14 is a good program with one fatal flaw: quit it and everyone’s gone. Every program in this book so far has been an elaborate way of forgetting things.

Files fix that, and the tools look familiar on purpose.

Writing

#include <fstream>

std::ofstream out("todo.txt");
out << "buy milk\n";
out << "learn pointers\n";
out.close();

std::ofstream is an output file stream. You give it a filename, and then it behaves like std::cout: same <<, same everything. That similarity is deliberate, and it means you already know how to write files.

close() tells it you’re done. You can leave it out and the file is closed for you when out goes out of scope, which is a mechanism chapter 27 explains properly. Writing it explicitly is a good habit while you’re learning, because it puts the “and now the file is finished” moment somewhere you can see.

Reading

std::ifstream in("todo.txt");

std::string line;
while (std::getline(in, line)) {
    std::cout << "  [" << line << "]\n";
}
  [buy milk]
  [learn pointers]

std::ifstream is the input one, and it behaves like std::cin.

That while (std::getline(in, line)) is the same shape chapter 8 showed you and promised to explain. Here’s the short version: getline returns the stream, and a stream used as a condition is true while it’s still working. Reaching the end of the file makes the read fail, which ends the loop. Chapter 17 is where this stops being a shape you copy, but for reading a file it is the idiom, and there is no better one to learn instead.

Note what the lines don’t have: the \n you wrote is gone. getline reads up to the newline and throws it away, which is almost always what you want and worth knowing before you wonder why your output has no blank lines in it.

Did it actually open?

Files fail. The name is wrong, the file isn’t there, the folder is read-only. Ask:

std::ifstream in("todo.txt");

if (!in) {
    std::cout << "could not open todo.txt\n";
    return 1;
}

If you skip this check, nothing dramatic happens, which is the problem. A getline loop on a file that never opened runs zero times and your program cheerfully reports an empty list. return 1 from main is the other half of chapter 1’s return 0: a non-zero exit code means something went wrong.

Where does the file go?

"todo.txt" has no folder in it, so it lands in whatever directory you were in when you ran the program, not where the source file lives, and not where the compiler put the binary.

This is the second-biggest source of confusion in this chapter, and the fix is one command. In the terminal, before running:

pwd

That prints the directory you’re in, and that’s where todo.txt will be. Run ls after your program and you should see it.

If you’re running from an editor’s run button, the directory may be somewhere you did not choose, which is one of the reasons this book runs things from the terminal.

Words and numbers, not just lines

getline gives you whole lines. An ifstream also does everything std::cin does, so >> works and reads one whitespace-separated item at a time:

std::ifstream in("nums.txt");

int n = 0;
int total = 0;
while (in >> n) {
    total += n;
}

Given a file containing 10 20 30 and 40 on two lines, that totals 100. It does not care where the line breaks were, because >> treats every kind of whitespace the same.

Which to use is the same choice as chapter 5’s, for the same reasons. getline when a line is the unit and might contain spaces: a to-do item, a full name. >> when the file is a run of values and the layout doesn’t matter.

Saving something with fields

A to-do item is one string, so one line per item works. A Contact from chapter 14 has three fields, and you need them back apart again.

Pick a character that won’t appear in the data and put it between them:

out << c.name << '|' << c.phone << '\n';
Ada Lovelace|555-0100
Alan Turing|555-0142

Reading it back needs getline twice on the same line, which it can do if you hand it a stream made from that line:

#include <sstream>

std::istringstream parts(line);
std::getline(parts, c.name, '|');
std::getline(parts, c.phone);

std::istringstream is a stream that reads from a string instead of a file, and it’s useful far beyond this. The third argument to getline is what to stop at: '|' instead of the usual newline.

This is the honest beginner’s version of a real format, and it has a real flaw: the day a name contains a |, the file is broken. Actual formats spend most of their complexity on exactly that problem. Knowing that the flaw is there is enough for now.

Adding instead of replacing

Sometimes you want to keep what’s there:

std::ofstream append("todo.txt", std::ios::app);
append << "third thing\n";

std::ios::app is append mode: writes go on the end and nothing is destroyed. It is the exception to the callout above, and it’s the right tool for a log, a file you only ever add to.

For a save file, plain overwriting is usually what you want: write the whole current list, replacing whatever was there. Just do it at the right moment.

Exercise 1 · Write it, read it, find it

Write a program that saves five lines to numbers.txt, closes it, then reopens it and prints them with a getline loop.

Run it, then run pwd and ls in the same terminal and actually look at the file. Open it in your editor. It should be exactly what you’d expect, which is reassuring in a way that reading about it is not.

Now change the filename to something in a folder that doesn’t exist, like "nowhere/numbers.txt". Add the if (!out) check and confirm it fires. Without the check, notice the program reports success.

Check yourself

1. What happens the moment you open an existing file with std::ofstream?

Not quite. The truncation happens at open. A program that opens its save file early and writes late has already lost the data.

Yes. Opening for writing truncates. Use std::ios::app if you meant to keep what was there.

Not quite. It opens it happily. Overwriting is the default behaviour, not an error.

2. Your getline loop over a file runs zero times. What is the likeliest cause?

Not quite. Possible, and worth checking with ls. But a file that never opened behaves identically, which is why the check matters.

Yes. A failed open produces a stream that fails every read, so the loop ends before it starts. if (!in) is the only thing that distinguishes it.

Not quite. Append mode is for writing. Reading needs an ifstream and nothing else.

3. After std::getline(in, line), what does line contain?

Not quite. The newline is consumed and discarded. Printing these back out without adding \n yourself runs them all together.

Yes. getline reads up to the newline and throws it away, so writing lines back out means supplying it again.

Not quite. One line per call. Looping is what gets you the whole file.

Project

A to-do list that survives quitting

Roughly 60 minutes

The first program in this book you could genuinely use. It loads your list when it starts, lets you add and remove items, and saves when you quit.

Your list:
  1. buy milk
  2. learn pointers

1) Add  2) Remove  3) Quit
Choice:

Structure it as three jobs with names:

std::vector<std::string> load(const std::string& path);
void save(const std::string& path,
          const std::vector<std::string>& items);
void show(const std::vector<std::string>& items);

Look at those signatures before writing the bodies. load returns a fresh vector; save takes one by const& because it only reads it. Chapter 12’s rule, and by now it should be reflex.

The order of operations is the whole project:

  1. load at the top of main, with an ifstream. A missing file is not an error the first time you run it, so return an empty vector and carry on.
  2. Run the menu loop on the vector in memory. No file work at all in here.
  3. save once, after the loop ends, with an ofstream.

Keeping the file out of the middle is what makes this program simple. Every version that reads and writes the file inside the menu loop is harder and, if you hit the trap above, destructive.

For removing an item, ask for a number and rebuild the vector without it, the same “build a new one and assign it over” move as chapter 12’s drop_empty. Remember the reader counts from 1 and the vector counts from 0.

Stretch one, don’t lose work on a crash. Call save after every change as well as at the end. Now think about what happens if the program dies halfway through writing, and why real programs write to a temporary file and rename it.

Stretch two, the reader types nonsense. Type banana at the menu and watch chapter 8’s runaway loop come back to haunt you. That’s chapter 17, and it is the next thing this program needs.