In C++, you can interact (read/write) with disk files using filestream classes.
The C++ filestream is very similar to standard cin and cout streams.
Just like cin and cout, there are two classes for filestream: ifstream for input and ofstream for output.
For the input stream, you can imagine there is a stream connected to disk file and characters are copied from the file to the stream and your program is reading characters from the stream.
For the output stream, you can imagine the opposite way of the input stream: your program writes to the stream and stream to the disk file.
Basic Usage of Filestream
There are two classes to interact with disk files: ifstream and ofstream.
Although usage of the filestream is very similar to cin and cout there are some differences worthwhile to note.
You don’t need to create an object for cin and cout because they are global objects and will be created/destroyed depending on the start/end of the program.
However, you have to create your own objects for ifstream and ofstream manually so you can control which files to read and write.
You need to include a header called ‘fstream’ in order to use filestreams.
And you have two choices to open the file.
1. you can provide the file name (and path if the file is not located in the same directory as the program) in the constructor of ifstream or ofstream.
2. you can explicitly call ‘open’ member function with the filename.
Let’s take a look at some example here
#include <iostream> #include <fstream> using namespace std; int main() { // first option ifstream inptuFileStream1("/some/filename/with/path"); ofstream outputFileStream1("/some/filename/with/path"); // second option ifstream inputFileStream2; ofstream outputFileStream2; inputFileStream2.open("/some/filename/with/path"); outputFileStream2.open("/some/filename/with/path"); return 0; }
Disk files are managed by the operating systems (create/update/delete) and C++ facilities actually calls OS subroutines internally in order to work with those disk files.
Creating filestream objects or calling ‘open’ with the filename is actually establishing the relationship with those files: ifstream for read and ofstream for write.
Once the relationship is established, you can use those filestreams just like cin/cout.
After opening the file it is very important to test if the file is successfully opened but I will talk about that later in this post.
Let’s quickly take a look at how the read/write operations are done with file streams.
// read from filestream and place the data to string string data; int intData; inputFileStream1 >> data >> intData; // write to filestream string data2 = "this is data to write"; int intData2 = 10; outputFileStream1 << intData2 << data2;
It’s very simple and in fact, it works exactly like cin and cout once the relationship with the disk file is set correctly.
Once you are done with the filestream, it is generally considered a good practice to close the stream telling the OS that the connection to the disk file should be closed now so the state of the file can be updated properly.
For the input stream, you might not care that much since you probably didn’t change the file at all during the read.
However, it might be crucial to update the state for the output stream since the filesystem usually buffers data internally for efficiency which means that those data in the buffers need to be properly flushed to the file.
Fortunately, it is very easy to close the file streams.
// close the input filestream inputFileStream1.close(); // close the output filestream outputFileStream1.close();
What if you want to read files multiple times from the beginning?
You just need to make sure to clear internal state bits of the filestream, which I will discuss later in the post, and close and reopen the stream.
Filestream Errors
We have taken a quick look at how to open a file and read/write to and from the file.
However, we need to understand filestream errors in order to understand and use filestream better.
A stream object has internal bits as data members that remembers the state.
It is important to understand these because the stream will not work properly if there is any error bit is turned on.
In this section, I will mostly focus on the input stream case since it cares about errors the most.
Here are a few examples of state bits.
good
The stream is considered ‘good‘ after the file is successfully opened and has not had any problems during read or write.
If there is any error it will turn on corresponding error bit and it’s not in ‘good’ state anymore.
Consequently, all the read/write stream operation will be no-op.
It will stay the same until ‘clear()‘ function is called on the stream which clears all the error bits.
Please note that closing the filestream doesn’t clear the bits. You have to explicitly call ‘clear()‘ unless you use fresh new stream.
In short, the stream needs to be in ‘good‘ state to do read or write operation.
Member function for checking ‘good‘ is good().
fail
The stream is considered ‘fail‘ when it fails to read from the input stream.
The common example for this is type errors such that trying to read string and place to int variable.
In this case, it will turn on the ‘fail‘ bit and thus it’s not in ‘good‘ state anymore until it’s clear() is called.
eof
This means the stream reached the end of the file and is not able to read anymore.
This is not really an error but it is still necessary to indicate reading the file is done.
Just like ‘fail‘, when ‘eof‘ is set, the stream is not in ‘good‘ state anymore.
It is worthwhile to note a couple of things regarding ‘eof‘.
- It is set only after attempting to read after reading the last character of the file. On the other hand, ‘eof‘ is not set when it reads the last character.
- ‘fail‘ bit will be also set when ‘eof‘ is encountered.
- User shouldn’t test ‘eof‘ to control the reading since it is only raised when end-of-file is reached. But what if stream read failed before the end? Testing ‘eof‘ will not be able to catch anything.
I am not going to discuss ‘bad‘ bit here since I think it’s more related to hardware error and there isn’t anything we can do about it.
Detecting and Handling Errors
After going over some important error bits we need to be able to detect and handle those errors.
// this is a member function and tells you if there isn't any errors bool good(); // conversion operator returns same value as good() above. // please note that >> operator returns the stream object // and thus conversion operator will work the same. if (inputFileStream) if (!inputFileStream) if (inputFileStream >> data) while (inputFileStream >> data) // returns true if the file is opened successfully. bool is_open(); // returns true if fail bit is set due to input failure bool fail(); // returns true if eof bit is set bool eof(); // resets all the error bits. clear the error state. // this is necessary to resume the stream. // otherwise, the disk IO operations will be just noop until it's cleared void clear();
Handy Member Functions for Reading
There are other ways to read inputs instead of using >> operator.
I will briefly explain a couple of functions.
istream& get(char&);
cin.get(charVar);
Reads one character to provided variable.
It does not skip anything. (For ex. >> operator just skips characters like spaces, tabs, and newlines)
This returns the reference to the stream object itself.
istream& getline(char *array, int n);
inputFile.getline(bufferCharArr, bufferLen);
This function reads characters to the provided buffer until it reaches ‘\n’ or ‘n’ – 1 characters are read.
It terminates the buffer with ‘\0’ regardless so you will get valid cstring after the operation.
‘n’ needs to be less than the buffer size in order to prevent any buffer overflows.
For your convenience, ‘\n’ is removed from the stream and not placed onto buffers because you typically don’t want to read newlines.
If the buffer array is filled without finding the ‘\n’, the input operation will fail the same way as invalid input error to notify you newline was not found.
istream& getline(istream&, std::string&);
Special function provided by <string> library.
This function reads the entire line into the provided string variable.
You don’t have to worry about buffer overflow since string grows as much as the memory permits.
I personally think this is the most recommended way to process a file line by line.
Code Example
This code example does the following operations.
- read a file that has a few integers.
- write to another file with a value multiplied by 2
#include <iostream> #include <fstream> using namespace std; int main() { // input file in the same directory as the program. ifstream inputFile("data.txt"); // check if the file is opened successfully if (!inputFile.is_open()) { cout << "File was not successfully opened." << endl; return -1; } // prepare output stream to write result ofstream outputFile("result.txt"); // check if the file is opened successfully if (!outputFile.is_open()) { cout << "File was not successfully opened." << endl; return -1; } int data; // read until input failure happens while (inputFile >> data) { outputFile << data * 2 << endl; } inputFile.close(); outputFile.close(); return 0; }
I love your lecture notes. easy and clear. Please keep upload posts.