IO Classes in C++

C++ does not handle input and output directly; instead, it relies on a set of types defined in the standard library to deal with IO. These types support IO operations that read data from devices and write data to devices, where a device can be a file, a console window, and so on. Some types also allow in-memory IO, that is, reading data from a string, writing data to a string, and the like.

Applications do not perform IO operations only on the console window; they often need to read from and write to named files, and it is frequently convenient to use IO operations to handle the characters in a string. So, in addition to istream and ostream, the standard library defines several other IO types, declared in three separate header files: iostream defines the basic types used to read from and write to streams, fstream defines the types used to read from and write to named files, and sstream defines the types used to read from and write to in-memory string objects.

The types ifstream and istringstream both inherit from istream. Therefore, we can use ifstream and istringstream objects just as we use istream objects. It is worth noting that we cannot copy or assign IO objects, and reading from or writing to an IO object changes its state, so the references that are passed and returned cannot be const.

Condition States

The IO classes define some functions and flags that help us access and manipulate the condition state of a stream. Once an error occurs in a stream, all subsequent IO operations on it will fail. The simplest way to determine the state of a stream object is to use it as a condition:

while(cin>>word)

The IO library defines a machine-independent iostate type that provides complete functionality for expressing the state of a stream: badbit indicates a system-level error, such as an unrecoverable read/write error; failbit indicates a recoverable error, such as expecting to read a numeric value but reading a character instead; when the end of a file is reached, both eofbit and failbit are set. A goodbit value of 0 indicates that no error has occurred. If any one of badbit, failbit, or eofbit is set, testing the stream’s condition will fail.

Managing the Output Buffer

Each output stream has a buffer used to hold the data that the program reads and writes, as in the following code:

os<<"Hello,world";

The text may be printed immediately, or it may be saved by the operating system in the buffer and printed later. A few ways to explicitly flush the buffer:

cout<<"Hello,world"<<endl;	// output the string, then output a newline and flush the buffer
cout<<"Hello,world"<<flush;	// output the string, then flush the buffer directly
cout<<"Hello,world"<<ends;	// output the string, then output a null character and flush the buffer

When an input stream is tied to an output stream, any attempt to read data from the input stream first flushes the tied output stream. The standard library ties cout and cin together, and you can use the tie() function to manually tie an istream and an ostream together.

File Input and Output

The open and close Functions

After defining an empty file stream object, you can call open to associate it with a file.

ifstream in(ifile);	// construct an ifstream and open the given file
ofstream out;	// the output file stream is not associated with any file
out.open(ifile+".copy")	// open the specified file

Once a file stream is opened, it stays associated with the corresponding file. If you try to associate this file stream with another file, you must first close the corresponding file stream.

in.close();	// close the file
in.open(ifile+"2");	// associate with a new file

File Modes

Every stream has an associated file mode that indicates how the file is to be used. Each file stream type defines a default file mode, and when we do not specify a file mode, the default is used. A file associated with an ifstream is opened in in mode (read mode) by default; a file associated with an ofstream is opened in out mode (write mode) by default. The file modes are shown in the table below:

  • in: open for reading only
  • out: open for writing
  • app: seek to the end of the file before every write operation
  • ate: seek to the end of the file immediately after opening it
  • trunc: truncate the file
  • binary: perform IO in binary

By default, when an ofstream is opened, the contents of the file are discarded. The way to prevent an ofstream from clearing the contents of a given file is to also specify app mode.

ofstream out("file1")	// the file will be cleared
ofstream out("file",ofstream::app|ofstream::out)	// opening in app mode preserves the contents

The only way to preserve the data in a file opened by an ofstream is to open it in app mode or in in mode.

String Streams

The sstream header file defines three types that support in-memory IO. These types can write data to a string and read data from a string, as if the string were an IO stream.

When our task is to process whole lines of text while some other work involves processing individual words within a line, we can typically use an istringstream.

string line,word;
vector<PersonInfo> people;
while(getline(cin,line)){	// keep reading one line at a time from cin
	PersonInfo info;
	istringstream record(line);   // use the line we read to initialize the record object
	record>>info.name;      // process each word from this line individually
	while(record>>word)
		info.phones.push_back(word);
	people.push_back(info);
}

An ostringstream is very useful when we build up the output incrementally and want to print it on the last line.

for(const auto &entry:people){
	ostringstream formatted,badNums;
	for(const auto &nums:entry.phones){
		if(!valid(nums)){   // if an invalid phone number is encountered, put it into badNums to be output together later
			badNums<<" "<<nums;
		}else
		formatted<<" "<<format(nums);   // put all the valid phone numbers into formatted
	}
	if(badNums.str().empty())
		os<<entry.name<<" "<<formatted.str()<<endl;
	else
		cerr<<"Input error: "<<entry.name<<" invalid number(s) "<<badNums.str()<<endl;
}