Chapter 1
IN THIS CHAPTER
Seeing the need for a streams library
Opening a file
Dealing with errors
Working with flags to customize your file opening
You’ve heard of rivers, lakes, and streams, and it’s interesting just how many common words are used in computer programming. That’s handy, because it lets programmers use words they already know with similar meaning. Using common terms makes it easier to visualize abstract concepts in a concrete way.
Most programmers think of a stream as a file — the type stored on a hard drive, Universal Serial Bus (USB) flash drive, or Secure Digital (SD) card. But streams go beyond just files. A stream is any type of data structure that you can access as a flow of data, essentially a sequence of bytes. Streams are used to access all sorts of devices, such as smart speakers. Rather than just fill a 500MB data structure and then drop it onto the hard drive, you write your data piece after piece; the information goes into the file.
Streams go further than a wide variety of devices, however. Opening an Internet connection and putting data on a remote computer usually requires a stream-based data structure. You write the data in sequence, one byte after another, as the data goes over the Internet like a stream of water, reaching the remote computer. The data you write first gets there first, followed by the next set of data you write, and so on.
This chapter discusses different kinds of streams available to you, the C++ programmer. In addition, you discover how to handle errors and use flags to modify how you open files.
When you write an application that deals with files, you must use a specific order:
Open the file.
Before you can use a file, you must open it. In doing so, you specify a filename.
Access the file.
After you open a file, you either store data into it (this is called writing data to the file) or get data out of it (this is called reading data from the file).
Close the file.
After you have finished reading from and writing to a file, you must close the file.
For example, an application that tracks your stocks and writes your portfolio to a file at the end of the day might do these steps:
The next morning, when the application starts, it might want to read the information back in. Here’s what it might do:
You have two ways to write to a file:
Back in the days of the C programming language, several library functions let you work with files. However, they stunk. They were cumbersome and made life difficult. So, when C++ came along, people quickly created a set of classes that made life with files much easier. These people used the stream metaphor we’ve been raving about. In the sections that follow, you discover how to open files, write to them, read from them, and close them.
The libraries you use to work with streams are divided into various groups, each of which requires its own header. The libraries divide input and output into separate classes, as shown in Figure 1-1. In addition, the kind of input and output determines which header you use. The libraries also support specific commands that include cin
and cout
— the commands you have used for so many purposes so far.
FIGURE 1-1: Working with streams requires use of the appropriate headers and commands.
Now that you have a basic overview of how these various headers and commands work with the streams library to provide stream output, it’s time to get the details. The following sections help you understand how to use code to create streams of data that could go to a file, Internet connection, or some other location, such as a smart speaker.
The streams library includes several classes that make your life much easier. It also has several classes that can make your life more complicated, mainly auxiliary classes that you rarely use. Here are three of the more common classes that you use:
ifstream
: This is a stream you instantiate if you want to read from a file. The if part of the name stands for input file.ofstream
: This is a stream you instantiate if you want to write to a file. The of part of the name stands for output file.fstream
: This is a stream you instantiate if you want to both read and write to a file. The f part of the name stands for file (in a general sense, rather than specifically for input or output).Before you can use the ifstream
, ofstream
, or fstream
classes, you #include <fstream>
. As with many C++ classes and objects, you find these classes inside the std
namespace. Thus, when you want to use an item from the streams library, you must either
std
, as in this example:
std::ofstream outfile("MyFile.txt");
using
directive before the lines where you use the stream classes, as in this example:
using namespace std;
ofstream outfile("MyFile.txt");
Opening a file means to obtain access to a file on disk. The process of opening a file returns a variable that allows you to do things with that file, such as read or write it. You have two options for opening a file:
Some operating systems treat these two methods as a single entity. The reason is that when you create a new file, normally you want to immediately start using it, which means that you want to create a new file and then open it. So the process of creating a file is often embedded right into the process of opening a file.
When you open an existing file that you want to write to, you have several choices:
The FileOutput01
example code, in Listing 1-1, shows you how to open a brand-new file, write some information to it, and then close it. (But wait, there’s more: This version works whether you have the newer ANSI-compliant compilers or the older ones!)
LISTING 1-1: Using Code That Opens a File and Writes to It
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream outfile("../MyFile.txt");
outfile << "Hi" << endl;
outfile.close();
cout << "File Written!" << endl;
return 0;
}
The short application in Listing 1-1 opens a file called MyFile.txt
. (The ../
part of the file path places the file in the parent directory for the example, which is the Chapter01
folder; see the “Finding your files” sidebar, in this chapter, for details.) The application opens the MyFile.txt
file by creating a new instance of ofstream
, which is a class for writing to a file. The next line of code writes the string "Hi"
to the file. It uses the insertion operator, <<
, just as cout
does. In fact, ofstream
is derived from the same class as cout
,
as shown in Figure 1-1, so anything you can do with cout
you can also do with your file. When you finish writing to the file, you close it by calling the close()
method.
If you want to open an existing file and append to it, you can modify Listing 1-1 slightly. All you do is change the arguments passed to the constructor, as follows:
ofstream outfile("MyFile.txt", ios_base::app);
The ios::app
item is an enumeration inside a class called ios
, and the ios_base::app
item is an enumeration in the class called ios_base
. The ios
class is the base class from which the ofstream
class is derived. The ios
class also serves as a base class for ifstream
, which is for reading files.
You can read from an existing file. You perform this task in a manner similar to using the cin
object to read from the keyboard. The FileRead01
example, shown in Listing 1-2, opens the file created by Listing 1-1 and reads the string back in. This example uses the parent directory again as a common place to create, update, and read files.
LISTING 1-2: Using Code to Open a File and Read from It
#include <iostream>
#include <fstream>
using namespace std;
int main() {
string word;
ifstream infile("../MyFile.txt");
infile >> word;
cout << word << endl;
infile.close();
return 0;
}
When you run this application, the string written earlier to the file in Listing 1-1 — Hi
— appears onscreen.
You may notice in Figure 1-1 that there is an fstream
class that derives from iostream
, which itself derives from both istream
and ostream
. Using the fstream
class can save a lot of effort when you need to both read and write a file. The FileReadWrite01
example, shown in Listing 1-3, demonstrates how to both read and write the same file without closing the file handle first.
LISTING 1-3: Reading and Writing a File Using a Single Handle
#include <iostream>
#include <fstream>
using namespace std;
int main() {
fstream outfile("../MyFile.txt",
ios::in | ios::out | ios::trunc);
outfile << "Hi" << endl;
outfile.flush();
string Data;
outfile.seekg(0, ios::beg);
outfile >> Data;
outfile.close();
cout << "File Written!" << endl;
cout << Data << endl;
return 0;
}
The first part of this example works just like the example in Listing 1-1. You add opening modes to ensure that the handle works as anticipated: ios::in
means that the file is open for input, ios::out
means that the file is open for output, and ios::trunc
means that the file is truncated (the old data is removed) before you add new data. Instead of closing the file, you call flush()
, which ensures that the data actually appears on disk.
The example then creates an input string
, Data
, to receive information from the file. Before you can look at the file data, however, you must reposition the file pointer to point to the beginning of the file by using seekg()
. A file pointer tells you the place where you will either read or write in a file. When you initially write to the file, the file pointer is at the end of the file, so to read the file you must reposition it to the beginning of the file. Notice that you now read the data just as you did in Listing 1-2.
You’re not very likely to write single bits of data to a file in most cases. You usually want to work with something more complicated, like a container (Book 5, Chapter 6 tells you about various kinds of containers). The basic idea is to combine the file techniques in this chapter with the container techniques shown in Book 5, Chapter 6 to create an application that works with containers. Listing 1-4 shows the OutputVector
example that demonstrates how to perform this task.
LISTING 1-4: Saving a Vector to Disk
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
int main() {
vector<string> MyData;
MyData.push_back("One");
MyData.push_back("Two");
ofstream outfile("../MyData.txt");
for (Element : MyData)
outfile << Element << endl;
outfile.close();
cout << "File Written!" << endl;
return 0;
}
The example begins by creating a vector
, MyData
, that stores two strings. It then opens a file for output and uses a for
loop to process the MyData
elements one at a time. Each element appears on a separate line, which allows you to read the input file one line at a time to recreate the original vector
from the disk file.
When you open a file, all kinds of things can go wrong. A file lives on a physical device — a fixed disk, for example, or perhaps a flash drive or SD card — and you can run into problems when working with physical devices. For example:
*
or ?
.If you want to determine whether the ostream
class was unable to create a file, you can call its fail()
method. This method returns true
if the object couldn’t create the file. That’s what happens when a directory doesn’t exist. The DirectoryCheck01
example, shown in Listing 1-5, demonstrates an example of using the fail()
method.
LISTING 1-5: Returning True When ostream Cannot Create a File
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream outfile("/abc/def/ghi/MyFile.txt");
if (outfile.fail()) {
cout << "Couldn't open the file!" << endl;
return 0;
}
outfile << "Hi" << endl;
outfile.close();
return 0;
}
When you run this code, you should see the message Couldn’t open the file!
when your particular operating system doesn’t create a directory. If it does, your computer will open the file and write Hi
to it.
As an alternative to calling the fail()
method, you can use an operator available in various stream classes. This is !
, fondly referred to as the bang operator, and you would use it in place of calling fail()
, as in this code:
if (!outfile)
{
cout << "Couldn't open the file!" << endl;
return 0;
}
Oops! Aborting!
. Instead, do something friendlier — such as presenting a message telling users that there’s a problem and suggesting that they might free more disk space. (There are other reasons not covered in this book, such as lack of rights to the area of disk where the file is written — you need to perform application testing to locate all the possible reasons a file creation might fail and then provide error handling for each potential issue.)When you open a file by constructing a stream instance, you can modify the way the file will open by supplying flags. In computer terms, a flag is simply an indicator whose presence or lack of presence tells a function how to do something. The flag appears in the constructor when working with a stream.
A flag looks like ios_base::app
. This particular flag means that you want to write to a file, but you want to append to any existing data that may already be in a file. You supply this flag as an argument of the constructor for ofstream
, as shown here:
ofstream outfile("AppendableFile.txt", ios_base::app);
You can see the flag as a second parameter to the constructor. Other flags exist besides app
, and you can combine them by using the or operator, |
. Following is a list of the available flags:
ios_base::ate
: Use this flag to go to the end of the file after you open it. Normally, you use this flag when you want to append data to the end of the file.ios_base::binary
: Use this flag to specify that the file you’re opening will hold binary data — that is, data that does not represent character strings.ios_base::in
: Specify this flag when you want to read from a file.ios_base::out
: Include this flag when you want to write to a file.ios_base::trunc
: Include this flag if you want to wipe out the contents of a file before writing to it.ios_base::app
: Include this flag if you want to append to the current file pointer position of the file (which is at the beginning when you first open the file). It’s the opposite of trunc
— that is, the information that’s already in the file when you open it will stay there.The FileOutput02
example, shown in Listing 1-6, shows how to use a flag to append information to the output of Listing 1-1.
LISTING 1-6: Appending to an Existing File
#include <iostream>
#include <fstream>
using namespace std;
int main() {
string filename = "../MyFile.txt";
ifstream check(filename);
if (!check) {
cout << "File doesn't exist.";
return -1;
} else {
check.close();
}
fstream datafile(filename, ios_base::app);
datafile << " There" << endl;
datafile.close();
cout << "File Written!" << endl;
return 0;
}
If the file exists, you want to close the file handle to it before you write to it by calling check.close()
. You can then reopen the file for appending by adding the ios_base::app
flag. The example outputs some additional text and closes the file.