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
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:
loadat the top ofmain, with anifstream. A missing file is not an error the first time you run it, so return an empty vector and carry on.- Run the menu loop on the vector in memory. No file work at all in here.
saveonce, after the loop ends, with anofstream.
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.