A Gentle Introduction to C++ IO StreamsBy Manasij MukherjeeOne of the great strengths of C++ is its I/O system, IO Streams. As Bjarne Stroustrup says in his book "The C++ Programming Language", "Designing and implementing a general input/output facility for a programming language is notoriously difficult". He did an excellent job, and the C++ IOstreams library is part of the reason for C++'s success. IO streams provide an incredibly flexible yet simple way to design the input/output routines of any application. IOstreams can be used for a wide variety of data manipulations thanks to the following features:
The IO stream class hierarchy is quite complicated, so rather than introduce you to the full hierarchy at this point, I'll start with explaining the concepts of the design and show you examples of streams in action. Once you are familiar with elements of the design and how to apply those concepts to design a robust I/O system for your software, an understanding of what belongs where in the hierarchy will come naturally. What do input and output really mean?To get a good idea about what input and output are, think of information as a stream of characters. This makes sense because whatever we enter through a keyboard can only be characters. Suppose the user enters the number 7479....WAIT...! How do you know the user entered a number? The problem is that you don't really know. All you have is a set of 4 characters: '7', '4', '7' and '9'. It is completely up to you, the programmer, whether you want the input to be a number, to be a string, or to be fodder for /dev/random; whether the characters can be valid for the desired type totally depends upon whether that type can interpret the characters in the input stream as a description for an object of that type. You have to get the input characters into a recognizable data type for them to be of any use other than as a character array. IO streams not only define the relation between a stream of characters and the standard data types but also allows you to define a relationship between a stream of characters and your own classes. It also allows you nearly limitless freedom to manipulate those streams both using object oriented interfaces and working directly on character buffers when necessary. (Of course some of the lower level manipulations may be undefined; for example, you can't probe forward into an input stream to see the future!) How do streams work?Streams are serial interfaces to storage, buffers files, or any other storage medium. The difference between storage media is intentionally hidden by the interface; you may not even know what kind of storage you're working with but the interface is exactly the same. The "serial" nature of streams is a very important element of their interface. You cannot directly make random access random reads or writes in a stream (unlike, say, using an array index to access any value you want) although you can seek to a position in a stream and perform a read at that point. Using a serial representation gives a consistent interface for all devices. Many devices have the capability of both producing and consuming data at the same time; if data is being continually produced, the simplest way to think about reading that data is by doing a fetch of the next characters in a stream. If that data hasn't been produced yet (the user hasn't typed anything, or the network is still busy processing a packet), you wait for more data to become available, and the read will return that data. Even if you try to seek past the end (or beginning) of a stream, the stream pointer (i.e. get or put pointer) will remain at the boundary, making the situation safe. (Compare this with accessing data off the end of an array, where the behavior is undefined.) The underlying low-level interface that corresponds to the actual medium very closely is a character buffer (the stream buffer, technically called the streambuf), which can be thought of as the backbone of the stream. Being a buffer, it does not hold the entire content of the stream, if the stream is large enough, so you can't use it for random access. The most important of the basic stream operations are:
Error handling with IO streamsHandling errors gracefully is important for building a robust system. The 'errors' or 'signals' (e.g., reaching the end of the file) in this case generally occur when a stream encounters something it didn't expect. Whether a stream is currently valid can be checked by simply by using the stream as a Boolean: ifstream file( "test.txt" ); if ( ! file ) { cout << "An error occurred opening the file" << endl; } More detailed status of the stream can be obtained using the good(), bad(), fail() and eof() functions. The clear() function will reset the stream and clear the error, which is necessary to perform any further IO on the stream.
You can detect that a particular read or write operation failed by testing the result of the read. For example, to check that a valid integer is read from the user, you can do this: int x; if ( cin >> x ) { cout << "Please enter a valid number" << endl; } This works because the read operation returns a reference to the stream. An example of creating a stream-enabled objectHere is a simple example of an utility designed for writing out logfile entries from command line arguments that takes advantage of some important stream facilities. If you don't understand something, refer to the tutorial on that particular topic. #include <iostream> #include <ctime> #include <sstream> #include <fstream> using namespace std; // timestamp returns the current time as a string std::string timestamp(); class LogStatement; ostream& operator<<(ostream& ost, const LogStatement& ls); class LogStatement { public: LogStatement(std::string s): data(s), time_string( timestamp() ) { }; //This method handles all the outputs. friend ostream& operator<<(ostream&, const LogStatement&); private: std::string data; std::string time_string; }; ostream& operator<<(ostream& ost, const LogStatement& ls) { ost<<"~|"<<ls.time_string<<'|'<<ls.data<<"|~"; return ost; } std::string timestamp() { //Notice the use of a stringstream, yet another useful stream medium! ostringstream stream; time_t rawtime; tm * timeinfo; time(&rawtime); timeinfo = localtime( &rawtime ); stream << (timeinfo->tm_year)+1900<<" "<<timeinfo->tm_mon <<" "<<timeinfo->tm_mday<<" "<<timeinfo->tm_hour <<" "<<timeinfo->tm_min<<" "<<timeinfo->tm_sec; // The str() function of output stringstreams return a std::string. return stream.str(); } int main(int argc, char** argv) { if(argc<2) { // A return of -1 denotes an error condition. return -1; } ostringstream log_data; // This takes all the char arrays in the argv // (except the filename) and produces a stream. for(int i=1;i<argc;i++) { log_data<<argv[i]<<' '; } LogStatement log_entry(log_data.str()); clog<<log_entry<<endl; ofstream logfile("logfile",ios::app); // check for errors opening the file if ( ! logfile ) { return -1; } logfile<<log_entry<<endl; logfile.close(); return 0; } This example should be pretty straightforward. The only two 'new' things here are:
A few other things to noteThe << operator takes in an ostream object, modifies it and returns it, even though it would have been enough to take the ostream object and modify it, with no return value. The value of returning the object is that you can chain operations, as in the following statement: cout<<"Hey, I'm "<< n <<" years old."; As the operator accepts an ostream, you can do anything with the data written to the stream simply by choosing a different subclass of ostream. The example shows writing to a file and to the system log console/file. You could also compress it and archive it, send it over a network, parse it later with another program to determine the number of log entries in a specified time interval, etc. Basically, you can use the class for any sort of output operation for which a subclass of ostream exists. Parts of the IO stream libraryNow that you've seen the basic problems solved by IO streams and how they work, let's look at the different elements of the IO streams library, with a few examples of each in action.Standard Stream Objects for Console I/O: (cout, cin, cerr, clog, etc.)These are declared in the <iostream> header and provide a consistent interface for console I/O. They allow you a lot of control about the exact way you want to read a value from the stream into the given variable (or write the variable to the stream). File I/OFile I/O is done by manually declaring objects of the ifstream, ofstream or fstream classes (from the <fstream> header) and then associating a file with the stream by using the stream's open method with the file's name as an argument. File I/O is particularly important because files are often used to represent a large variety of media, such as the console, devices, disk files, virtual memory, list of running processes and even the black hole '/dev/null'. This is especially true on *nix systems where it is commonly said that "Everything is a file". Here is a very very simple example of writing to a file: #include <iostream> #include <fstream> using namespace std; int main() { ofstream ofs("a.txt",ios::app); if(ofs.good()) { ofs<<"Hello a.txt, I'm appending this on you."; } return 0; } String StreamsStrings are streams and streams are strings. Both are character arrays, but each has a totally different interface (random access strings vs serial stringstreams). By providing both std::string and stringstreams, the C++ standard library ensures that you have the flexibility to choose either interface for your design. By including the <sstream> header, you can make objects of the istringstream, ostringstream and stringstream types. These objects make certain kinds of string manipulations much easier. You can, for example, open a string in a stringstream, extract a floating point number from it to do some operations, and put it back in the stream. #include <iostream> #include <sstream> using namespace std; int main() { stringstream my_stream(ios::in|ios::out); std::string dat("Hey, I have a double : 74.79 ."); my_stream.str(dat); my_stream.seekg(-7,ios::end); double val; my_stream>>val; val= val*val; my_stream.seekp(-7,ios::end); my_stream<<val; std::string new_val = my_stream.str(); cout<<new_val; return 0; } The output is Hey, I have a double : 5593.54 The lower level, where streams meet buffersThis is the most interesting and confusing part of this library, letting you manipulate streams at their lowest levels, bytes and bits. This is accomplished by the abstract class streambuf from which stringbuf and filebuf are derived. Every stream object has one of those as their backbones. Their function rdbuf() lets you access a pointer to the underlying stream buffer. Here is a very simple example of copying a file efficiently with those buffers (thankfully, no ultra-complicated manipulation is involved here!). #include <iostream> #include <fstream> using namespace std; int main() { ifstream ifs("a.txt"); //ios::trunc means that the output file will be overwritten if exists ofstream ofs("a.txt.copy", ios::trunc); if (ifs && ofs ) { ofs<<ifs.rdbuf(); } return 0; } |