Filing Information with the Streams Library - Reading and Writing Files - C++ All-in-One For Dummies (2009)

C++ All-in-One For Dummies (2009)

Book V

Reading and Writing Files

image

Contents at a Glance

Chapter 1: Filing Information with the Streams Library

Seeing a Need for Streams

Programming with the Streams Library

Handling Errors When Opening a File

Flagging the ios Flags

Chapter 2: Writing with Output Streams

Inserting with the << Operator

Formatting Your Output

Chapter 3: Reading with Input Streams

Extracting with Operators

Encountering the End of File

Reading Various Types

Reading Formatted Input

Chapter 4: Building Directories and Contents

Manipulating Directories

Getting the Contents of a Directory

Copying Files

Moving and Renaming Files and Directories

Chapter 5: Streaming Your Own Classes

Streaming a Class for Text Formatting

Manipulating a Stream

Chapter 1: Filing Information with the Streams Library

In This Chapter

Seeing the need for a streams library

Using the right header files

Opening a file

Dealing with errors

Working with flags to customize your file opening

First things first. We’ve all heard of rivers and lakes and streams, and it’s interesting just how many common words are used in computer programming. That’s handy, because it lets us use words we already know with similar meaning, but it’s also a bummer in that it’s harder to impress strangers. While we don’t have gluggerbumbles and plickershops in computer programming — words most people have never even heard of, mainly because we made them up — we do have streams!

Most programmers think of a stream as the same thing as a file. You know — a file that’s stored on your hard drive or maybe on a floppy disk or Zip drive. But streams go beyond just files. A stream is any type of data structure that you stream your data into and out of in a sequence of bytes.

For example, if we open an Internet connection to a top-secret computer that stores all our top-secret data (ooooh), and we start putting our data on the remote computer, we might use a stream-based data structure. By that we mean we 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 we wrote first gets there first and so on. It’s kind of like a stream of water.

You can use the same approach for storing data into a file. Rather than just filling a huge 5MB data structure and then dropping it onto the hard drive, you write your data piece after piece; the information goes into the file.

In this chapter, we talk about different kinds of streams available to you, the C++ programmer.

Seeing a Need for Streams

When you write a program that deals with files, you must use a specific order:

1. Open the file.

Before you can use a file, you must open it. In doing so, you specify a filename.

2. 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).

3. Close the file.

After you have finished reading from and writing to a file, you must close the file.

For example, a program that tracks your stocks and writes your portfolio to a file at the end of the day might do these steps:

1. Ask the user for a name of a file.

2. Open the file.

3. For each stock object, write the stock data to the file.

4. Close the file.

The next morning, when the program starts, it might want to read the information back in. Here’s what it might do:

1. Ask the user for the name of the file.

2. Open the file.

3. While there’s more data in the file, create a new Stock object, read the data from the file, and put the data into the Stock object.

4. Close the file.

image Here are a couple of reasons to close a file after you have finished using it:

Other programs might be waiting to use the file. Some operating systems, such as Windows 2000, allow a program to lock a file, meaning that no other programs can open the file while the program that locked the file is using it. Thus, in these situations, after you close a file, another program can use it.

When you write to a file, the operating system decides whether to immediately write the information onto the hard drive or floppy disk or to hold on to it and gather more information, finally writing it all as a single batch. When you close a file, the operating system puts all your remaining data into the file. This is called flushing the file.

You have two ways to write to a file:

Sequential access: In sequential access, you write to a file or read from a file from beginning to end. With this approach, when you open the file, you normally specify whether you plan to read from or write to the file, but not both at the same time. After you open the file, if you are writing to the file, the data you write continually gets added to the end of the file. Or if you are reading from the file, you read the data at the beginning, then you read the data that follows, then you read the data that follows that data, and so on, up to the end.

Random access: With random access, you can read and write to any byte in a file, regardless of which byte you previously read or wrote. In other words, you can skip around. You can read some bytes, then move to another portion of the file and write some bytes, and then move elsewhere and write some more.

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. And 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, we show you how to open files, write to them, read from them, and close them.

Programming with the Streams Library

Before we begin talking about the streams library (the actual libraries include fstream, iostream, and sstream, but don’t worry about that now — we’ll get into the specifics later), we need to tell you about some compatibility issues. The ANSI C++ standard document gives a complete library of classes that handle streams and general input/output. (There is a joint ANSI/ISO standard today, but we’ll focus on the ANSI standard in this book since that is the standard that appears in the GNU gcc documentation.) However, not all compiler systems (commonly called implementations) have the header files for these new classes. At the time of this writing, many implementations of the gcc for Windows do have them as an update or as part of the current release. However, some older compilers, perhaps the one on your hard drive, might not be compatible. That’s because gcc 2.95 did not yet have full support for the Standard Library, particularly in the stream classes. Newer versions of the gcc compiler are available from 3.1 on up. And a version of MinGW includes gcc3.1.

Fortunately, most of the classes in the Standard Library are available with almost all the compilers currently available. Therefore, in most of this book, we limit ourselves to the classes you can find in most compilers. That way, everyone can be happy (even us!). However, because you may have compilers that use the newer classes, we sometimes discuss them in sidebars.

Getting the right header file

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 because these are auxiliary classes that you’ll probably rarely use. Here are two of the more common classes that you will use. (And remember: These classes are available in pretty much all C++ implementations, whether the complete Standard Library is present or not.)

♦ ifstream: This is a stream you instantiate if you want to read from a file.

♦ ofstream: This is a stream you instantiate if you want to write to a file.

Before you can use the ifstream and ofstream classes, you include the proper header file. This is where things get ugly. In the early days of C++, people used the header file <fstream.h>. But somewhere in the mid-1990s, people started using the Standard Template Library (and in the late 1990s, the Standard C++ Library), both of which required you to include <fstream> (without the .h extension).

Because we want to stay up to date, in this book we use the ones without the .h. However, the Standard Template Library and the Standard Library put all their classes and objects inside the std namespace. (The Standard Template Library and Standard Library are both C++ libraries but implement some functionality differently — the article at http://en.wikipedia.org/wiki/Standard_Template_Library describes these differences, but you won’t need to worry about them when working through the examples in this book.) Thus, when you want to use an item from the streams library, you must either

♦ Prepend its name with std, as in this example:

std::ofstream outfile(“MyFile.dat”);

♦ Include a using directive before the lines where you use the stream classes, as in this example:

using namespace std;

ofstream outfile(“MyFile.dat”);

By default, the gcc compiler automatically recognizes the std namespace (it’s as if you had a line using namespace std; even when you don’t). We focus on the gcc compiler, we don’t use either of the two preceding methods — putting std:: before our stream class names or including a using namespace std; line.

image In the spirit of the rest of this book, if you are using a compiler other than gcc, we recommend that you follow your #include lines with the line using namespace std;. Then you can type all the sample code as-is throughout this book, including the stream examples, without needing to put std:: before every class or object from the Standard Library.

Opening a file

Let’s see, what did we call the name of this file? We think it was MyGreatChapter.doc. So we go to the word processor, choose File⇒Open, and type MyGreatChapter.doc.

Oops. We get an error message. That file doesn’t exist.

Oh, that’s right; we haven’t written it yet. Instead, we create a new document inside the word processor, type 800 cool pages over the course of a relaxing evening, and then (when we’re all finished) we save the file. We give it the name MyGreatChapter.doc. Then we shut down the word processor, hang out by the pool, brag to our friends about the new novels we’re writing, and go to bed.

The next morning, we open the document. This time it exists, so the word processor opens it and reads in the information.

As you can see, two issues present themselves in opening a file:

♦ Create a new file

♦ Open an existing file

Here’s where life gets a little strange: Some operating systems treat these two items as a single entity. The reason is that when you create a new file, normally you want to immediately start using it, which means you want to create a new file and then open it. And so the process of creating a file is often embedded right into the process of opening a file.

And when you open an existing file that you want to write to, you have two choices:

♦ Erase the current contents; then write to the file.

♦ Keep the existing contents and write your information to the end of the file. This is called appending to a file.

Separating a path name

Everybody wants to be different and unique. The people who wrote Microsoft’s MS-DOS operating system, instead of following in the tradition of using Unix’s / for a path name separator, decided to use \, thus adding the word backslash to the vocabularies of millions of people. So today, on Windows, you see such path names as C:\MyDataFolder\MyMessyPath\DifficultToType\LetterToEditor.doc. But on Unix, you see forward slashes, as in /usr/something/LetterToEditor.doc. And if this difference isn’t bad enough, think about what the backslash means in a string in C++. It means that a letter follows, and the compiler interprets the two characters together as something else. For example, \t means a tab, and \n means a newline character. And how do you put a backslash into a string? You put two backslashes. Ugh! That means the earlier MS-DOS-style string (the one with Windows in it, Mr. Bill Gates!) must look like this if you use it in a C++ program:

“C:\\MyDataFolder\\MyMessyPath\\DifficultToType\\LetterToEditor.doc”

Yes, you must type every backslash twice if you want the compiler to get the correct string. But instead of doing this, we’d like to propose a much better solution! Don’t use backslashes at all, even if you’re programming for Windows (or, we suppose, MS-DOS). Yes, it’s true! Stop the press! Call the talk radio show! Announce it to the world! When you write a C++ program on Windows, the libraries are smart enough to know that a forward slash works instead of a backslash! Therefore, you can use this string:

“C:/MyDataFolder/MyMessyPath/DifficultToType/LetterToEditor.doc”

In this book, you see us using forward paths. That way, our samples work on both Unix and Windows.

The 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!)

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.dat”);

outfile << “Hi” << endl;

outfile.close();

return 0;

}}

The short program in Listing 1-1 opens a file called MyFile.dat. It does this 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, ofstreamis derived from the very class that cout is an instance of, and so that means all the things you can do with cout you can also do with your file! Wow! We know we’re excited!

When we’re finished writing to the file, we close it by calling the close member function. This is important!

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. This one is for those of you with a slightly older (such as gcc2.95) compiler:

ofstream outfile(“MyFile.dat”, ios::app);

And this is for those of you with a newer compiler, such as gcc3.1 or later:

ofstream outfile(“MyFile.dat”, 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.

For newer compilers, the ios_base class is a base for ofstream and ifstream. (A couple of classes are in between. ofstream is a template class derived from a template class called basic_ofstream, which is derived from a template class called basic_ios, which is derived from the class ios_base. You can view a simplified diagram of these classes at http://www.cplusplus.com/reference/iostream/.)

You can also read from an existing file. This works just like the cin object. Listing 1-2 opens the file created by Listing 1-1 and reads the string back in. The easiest way to read the file for now is to copy it from the Listing 1-1 folder to the Listing 1-2 folder you create. The book’s source code includes the file in the proper folder for you.

imageIf you try to run Listing 1-2, it’s possible that your program will not find the file created by Listing 1-1. If so, you may want to add a path name to both Listing 1-1 and Listing 1-2, such as:

ofstream outfile(“/MyFile.dat”);

ifstream infile(“/MyFile.dat”);

Finding your files

Whenever you open a new file, you must know where the file is, not just what the file is called. In other words, you need to supply both a path and a filename, not just a filename. You can obtain a path for your file in different ways, depending on your program. For example, you may be saving all your files in a particular directory; you would then precede your filenames with that directory (that is, path) name. The string class makes this easy, as in this code:

const string MyPath = “c:\\GreatSoftwareInc”;

string Filename = MyPath + “\\” + “MyFile.dat”;

ofstream outfile(Filename.c_str());

The reason we had to call c_str on the string is that the ofstream class doesn’t have a constructor for a string instance, only a C-style string. The c_str function returns a pointer to a C-style string equivalent of the string. Also, remember to #include <string>when you use the string class!

Also, when you are using a constant path as we did in this example, you may, instead, store the path name in some initialization file that lives somewhere on your user’s computer, rather than hard code it in your program as we did in this example. You may also include an Options window where your users can change the value of this path.

Listing 1-2: Using Code to Open a File and Read from It

#include <iostream>

#include <fstream>

#include <string>

using namespace std;

int main()

{

string word;

ifstream infile(“MyFile.dat”);

infile >> word;

cout << word << endl;

infile.close();

return 0;

}}

When you run this program, the string you wrote to the file from Listing 1-1, “Hi,” appears on the screen. It worked! It read the string in from the file!

Handling Errors When Opening a 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 on a floppy disk — and you can run into problems when working with physical devices. For example, part of the disk might be damaged, causing an existing file to become corrupted. Or, less disastrous, you might run out of disk space. Or, even less disastrous, you might try to open a file in a directory that doesn’t exist.

imageIf you try to open a file for writing by specifying a full path and filename but the directory does not exist, the computer responds differently, depending on the operating system you are using. If you are unsure how your particular operating system will respond, try writing a simple test program that tries to create and open something like /abc/def/ghi/jkl/abc.txt. (Of course, you’ll want to be sure to use a directory that doesn’t exist; we’re assuming /abc/def/ghi/jkl doesn’t exist on your hard drive.) Then one of two things will happen: Either the directory and the file will get created or nothing will happen.

On our Windows 2000 system, if we attempt to create a file in a directory that doesn’t exist, the system does not create the directory. That’s because deep down inside, the program ultimately calls an operating system function that does the dirty work of creating the file. And this particular operating system function (it’s called CreateFile, if you even care) has a rule that it will not create a directory for you.

If you want to determine whether the ostream class was unable to create a file, you can call its fail member function. This function returns true if the object couldn’t create the file. And that’s what happens when a directory doesn’t exist. Listing 1-3 shows an example of this.

Listing 1-3: Returning True When ostream Cannot Create a File

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

ofstream outfile(“/abc/def/ghi/MyFile.dat”);

if (outfile.fail()) {

cout << “Couldn’t open the file!” << endl;

return 0;

}

outfile << “Hi” << endl;

outfile.close();

return 0;

}}

When you run this code, assuming that you don’t have a directory called /abc/def/ghi on your system, you should see the message Couldn’t open the file! We’re assuming also that your particular operating system doesn’t create a directory in this case; if it does, your computer will open the file, write Hi to it, and move on with its happy life after closing things out.

As an alternative to calling the fail member function, you can use an operator available in various stream classes. This is 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;

}

imageMost people prefer to use !outfile instead of outfile.fail(), although our opinion is that !outfile stinks. In addition to its aromatic properties, we think it makes confusing code. The reason is that outfile is an object, and in our brains, the notion of !outfile, which we would pronounce “not outfile,” simply doesn’t make sense. In fact, !outfile trips up many beginning programmers. They know that outfile is not a pointer in this sample code, and they wonder how you could test it against 0 as you normally can only do with a pointer. (Remember, by saying !x, where x is some pointer, you’re testing x against 0.) And that simply doesn’t make sense! And so to avoid confusion, we prefer to just call fail. It makes more sense.

Here are some reasons your file creation may choke:

♦ The directory doesn’t exist.

♦ You’re out of disk space and out of luck.

♦ Your program doesn’t have the right permissions to create a file.

♦ The filename was invalid — that is, it contained characters the operating system doesn’t allow in a filename, such as * or ?.

image Like any good program, your program should do two things:

1. Check whether a file creation succeeded.

2. If the file creation failed, handle it appropriately. Don’t just print a horrible message like Oops! Aborting!, leaving your poor users with no choice but to toss the monitor onto the floor. Instead, do something friendlier, like present a message telling them there’s a problem and suggest that they might free more disk space.

Flagging the ios Flags

When you open a file by constructing either an ofstream or ifstream instance, you can modify the way the file will open by supplying what are called flags. In computer terms, a flag is simply a small item whose presence or lack of presence tells a function how to do something. With the ofstream and ifstream classes, the function in question is the constructor.

A flag looks like ios::app if you’re using a compiler that is not fully ANSI-compliant, or like ios_base::app if you’re using one that is fully ANSI-compliant. 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 in either of the following examples:

ofstream outfile(“AppendableFile.txt”, ios::app);

ofstream outfile(“AppendableFile.txt”, ios_base::app);

You can see that we added the flag as a second parameter to the constructor. Other flags exist besides app, and you can combine them by using the or operator, |. For example, one flag is ios::nocreate (which isn’t included in newer compilers, but we show you how to overcome this limitation later in this section). This one means “only open the file if it already exists.” That is, don’t create the file if it doesn’t exist. (Remember, ofstream creates a file if it doesn’t already exist.) If the file doesn’t exist, the open will fail, and when you call fail, you will get back a true.

The ios::nocreate flag is handy with ios::app. Together, these mean open an existing file and append to it. That is, the two together will work only if the file already exists, and the call will open the file for an append operation. If the file doesn’t already exist, the file won’t be created. Here’s a sample call:

ofstream outfile(“/MyFile.dat”, ios::app | ios::nocreate);

if (outfile.fail()) {

cout << “Couldn’t open the file!” << endl;

return 0;

}

outfile << “Hi” << endl;

outfile.close();

If MyFile.dat doesn’t exist when you run this code, you get the message Couldn’t open the file! But if MyFile.dat does exist, the program opens it, appends the string Hi to it, and finally closes it.

imageIt turns out that the nocreate flag is not available in the new Standard Library. Bummer. Therefore, the code we just gave you works only if you’re using an earlier version of the library. When using the CodeBlocks compiler, you see the following error message:

error: `nocreate’ is not a member of `std::ios’

However, you will want to test whether or not your particular compiler includes a library that supports ios::nocreate. Your compiler may support it anyway, even if it includes the new Standard Library. As an alternative to ios::nocreate, you can use the following code:

ifstream infile(“/MyFile.dat”);

if (infile.fail())

{

cout << “Couldn’t open the file!” << endl;

return 0;

}

infile.close();

ofstream outfile(“/MyFile.dat”, ios::app);

outfile << “Hi” << endl;

outfile.close();

In this case, you begin by attempting to open the file for reading. If the file doesn’t exist, you can’t read from it and the code exits with a failure message. If the code can read from the file, it reopens the file for writing. This is a cumbersome workaround, but it works.

Following is a list of the available flags. First, here are the ones for ios, in case you’re using a compiler that is not completely ANSI compliant:

♦ ios::app: This flag means that you want to open a file and append to it.

♦ ios::in: Include this flag if you want to read from a file.

♦ ios::out: Include this flag if you want to write to a file.

♦ ios::trunc: Include this flag if you want to wipe out the contents of the file before writing to it. It’s the opposite of append, and it’s also the default if you don’t specifically include ios::app.

♦ ios::nocreate: Use this flag if you want to ensure that the file will not be created if it doesn’t exist, resulting in the file not being opened.

♦ ios::noreplace: This flag is the opposite of nocreate. Use this flag if you only want to create a new file. If you use this flag and the file already exists, the file will not open, and fail will return true.

ANSI-compliant compilers don’t support the ios::noreplace flag either. In this case, you can use the opposite of the fix for the ios:nocreate flag, as shown here:

ifstream infile(“/MyFile.dat”);

if (!infile.fail())

{

cout << “The file already exists!” << endl;

return 0;

}

infile.close();

ofstream outfile(“/MyFile.dat”);

outfile << “Hi” << endl;

outfile.close();

In this case, the code attempts to open the file for reading. If the file exists, the code shows an error message and exits. Otherwise, the code creates a new file and writes to it.

The following flags are available in a complier that’s absolutely ANSI-compliant!

♦ ios::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 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 file. It’s the opposite of trunc — that is, the information that’s already in the file when you open it will stay there.

image Why do you need an in flag and an out flag? It seems that the computer should know whether you’re writing to a file or reading from it based on whether you use ofstream or ifstream. The answer to why you have an in flag and an out flag is that other classes are available besides ofstream and ifstream. The compilers that don’t yet fully support the ANSI standard have a generic class in their libraries called fstream. The ANSI-compliant compilers have in their libraries a template class called basic_filebuf and a class calledfilebuf. If you use these classes, you can use the in and out flags.