DEV Community

Cover image for A Comprehensive Guide on File Handling in C++
Gurkirat Singh
Gurkirat Singh

Posted on

A Comprehensive Guide on File Handling in C++

Hello World! You have been working with data in the program and struggling with saving its states across multiple restarts. Yeah, I know that is temporary and will be gone as the program exists. In this post, I will teach you how you can use C++ file handling and save not only text data into it but also binary data like Class or Struct.

You can clone the repository for all the snippets and source code used in this tutorial https://github.com/tbhaxor/cpp-file-handling-tutorial.

Table of Contents

  1. Why do you need File Handling Anyway?
  2. Getting Started with File Handling
  3. Writing into the File
  4. Reading from the File
  5. Check Whether the File is Open or Not
  6. Closing file handle
  7. Appending into the Existing File
  8. Storing Class Object in the File
  9. Using I/O Streams of the File

Why do you need File Handling Anyway?

The program state gets vanished when it is exited or the functions go out of scope during execution. Sometimes it's required to have the states for future execution.

Take the example of a game, you don't play it continuously. Saving the same and loading it later is very basic. I had first seen this in the Project IGI game and then Age of Empires. Nowadays multiplayer games are getting popular and they use databases to store this information.

That's a great idea to use databases. So why not use the database, it can be shared easily. Let me tell you that there is no different magic going on there, DBMS uses files to store the data. So it boils down to file handling.

Getting Started with File Handling

There are and will be 2 types of operation in the file: Read file will allow you to retrieve the content and write file will allow you to put the content into the file using the current stream. A stream is basically a channel that carries your data from/to the file.

std::ifstream class is used to open the file in reading mode and std::ofstream is used to open the file in the write mode.

These classes provide different methods and properties as an abstraction to deal with files, or I would say even the large files.

Text File vs Binary File

There are two types of files: Human readable (or text) and human non-readable (binary).

It is easy to determine whether a file is a text or binary: If you open a file in a text editor (let's assume notepad or vscode), it will show all the characters and you can pronounce them. They might or might not make sense, but it's not required here. This file is a text file. If you are unable to pronounce any character, then that file is a binary file.

Note A binary file can contain a few readable characters as well, and that is ok.

Text File Binary File
Text File Binary File

What is EOF

While seeing the file format and the number of total bytes, we can determine that there are nn bytes in the file that can be read, but how does an opened stream will know in general? EOF is also known as End-of-File in computing. It is a condition or a notification to the file reader that tells no more retrieve more information from the file and seems like it is ended.

When you try to read a file and it is failed, both failed and eof bits are set in the error flags. You can use .eof() method to get this information.

So when does this bit set? An EOF bit is set after the ifstream reader tried to read the contents from the file and fails while the stream is still good (there is no error while performing I/O operations). So at least in the case of an ifstream, it waits for the OS to tell it that there's no more data read more

This is not only used for files but also streams and sockets

Programmatically, it will look like the following

std::cout << ifile->bad() << " " << ifile->eof() << " " 
          << ifile->fail();

// 0 1 1
Enter fullscreen mode Exit fullscreen mode

Writing into the File

Enough talking, let's start by writing a text content into the file using std::ofstream. Before all of that, you need to instantiate the ofstream and open the file:

std::ofstream *file = new std::ofstream();
file->open("file.txt", std::ios::out); 
Enter fullscreen mode Exit fullscreen mode

You can use std::ofstream file; // w/o pointer as well, but I prefer pointers.

The .open() function, as it says, open the file with name (first argument) and the mode (second argument).

I will discuss different modes with you as we proceed, std::ios::out mode is used to tell the function, that opens the file in write mode, overwrite the contents of it if it exists or create a new one.

Now it's time to write into the file stream using the left shift operator <<. This will format all your data into char* before writing.

(*file) << "Hello World" << std::endl;
Enter fullscreen mode Exit fullscreen mode

Since we are using a pointer variable, therefore it is required to dereference the file handle to access the << operator from the std::ofstream object stored.

Reading from the File

Reading is pretty simple and uses std::ifstream and >> operator with std::ios::in mode.

std::ifstream *file = new std::ifstream();
file->open(argv[1], std::ios::in);
Enter fullscreen mode Exit fullscreen mode

For reading files in text mode, there is only one option std::ios::in

std::string data;
(*file) >> data;
std::cout << data << std::endl; // Hello\n
Enter fullscreen mode Exit fullscreen mode

Reading until line end (eol)

When you write a file with << it formats whitespaces and streams that to the file, but while reading the content, it will stop at either EOF or the first whitespace character hit.

EOL or End-of-line is defined by the character \n or \r\n and is considered as the whitespace. So if there are multiple lines, you can use the std::getline() function.

std::getline(*file, data);
std::cout << data << std::endl; // Hello World\n
Enter fullscreen mode Exit fullscreen mode

Check Whether the File is Open or Not

Till now we are believing that the file exists and is ready for any I/O operation, but what if the condition is otherwise? What if it failed to open? Will it throw an error?

The answer to the question is, NO. It will not throw error but the RW will not work. You can use std::ifstream::is_open() or std::ofstream::is_open(). The functions returns boolean value, so if the file is actually opened successfully, it will return true.

if (file->is_open()) {
     // do the rest actions here
} else {
    std::cout << "Unable to open file\n";
}
Enter fullscreen mode Exit fullscreen mode

Closing file handle

When you are done with the file operations, it is recommended to close the file handle which will dispose of the file stream after writing any pending sequence of characters and clearing the buffer for both input and output streams.

For this you can use std::ofstream::close() or std::ifstream::close.

file->close();
Enter fullscreen mode Exit fullscreen mode

If the file is not successfully opened, the function will fail and set the failbit flag.

Appending into the Existing File

What if you want to append the data into the file without losing old content. In the ios::out, it overwrites the file and reading the content of files, appending in memory and then writing to a file doesn't seem to be a space and time-efficient solution.

There is another mode of opening the file that you can use in place of ios::out, which is append mode ios::app. This will allow you to write from the end of the file to the size of the data.

When you choose to open the file with append mode, the put cursor of the handle is set at the end of the file.

file->open(argv[1], std::ios::app);
Enter fullscreen mode Exit fullscreen mode

Storing Class Object in the File

So far you have seen me dealing with the text file, but in the real world, there are more than binary files with the custom format, like Zip archives, PDF files and other document files.

To open the file in binary mode, you need to specify std::ios::binary mode in the open() method of std::ifstream or std::ofstream.

in_file->open("file.bin", std::ios::binary | std::ios::in);

out_file->open("file.bin", std::ios::binary | std::ios::out);
Enter fullscreen mode Exit fullscreen mode

For reading and writing, you must use std::ifstream::read() and std::ofstream::write() because the binary file saves the raw bytes and does not perform any kind of formatting.

With this, you can now store the object of structs or classes directly into the file without serializing it into the textual format. It will be then automatically deserialized when the file is read.

Let's create a simple class called Student that will have name and age fields and a whoami() method to return a std::string object.

class Student {
  private:
    std::string name;
    unsigned short age;
  public:
    Student() {
      this->name = ""
      this->age = 0;
    }

    Student(const char * name, unsigned short age) {
      this->name = std::string(name);
      this->age = age;
    }

  std::string whoami() const {
    std::stringstream s;
    s << "I am " << this->name << " and " << this->age << " years old";
    return s.str();
  }

};

Student student("Gurkirat Singh", 20);
Enter fullscreen mode Exit fullscreen mode

You can save this to a file using the following snippet by casing it to char* because of its definition in the stdc++. read more

out_file->write(reinterpret_cast<char*>&student, sizeof(student));
Enter fullscreen mode Exit fullscreen mode

Once this is successfully saved into the file, you will verify that there are some weird characters present in the file and that is normal, because the string data also stores the information of deserializing the object.

Student mystudent;
in_file->read(reinterpret_cast<char*>&mystudent, sizeof(mystudent));

std::cout << mystudent.whoami() << std::endl; // I am Gurkirat Singh and 20 years old
Enter fullscreen mode Exit fullscreen mode

Now as a beginner, you must be thinking that the Student type is different from the char type. So why does the casting pointer not affect serialization? I would like to tell you that, you are getting confused between pointer type and other data types. For any data type, the pointer type would be 8 bytes or 4 bytes based on the CPU architecture, because it stores the starting address of the memory where that actual data of a type is stored. If the type of data is char, then it will consume 1 byte in the memory otherwise sizeof(T).

Using I/O Streams of the File

It is not like you can open a file only in one mode (that is either read or write), using std::fstream you can perform both input/output operations on the file with the same file handle.

Read/Write Cursors

There are two types of cursors in the file get and put. The get cursor is responsible for handling reading from the file which is known as input operation and the put cursor is responsible for handling writing into the file which is known as output operation.

To get the current position of the cursors, use the std::fstream::tellp() or std::fstream::tellg(). You can also set the position of these cursors making (either forward or backward) while the file is open using std::fstream::seekp() or std::fstream::seekg() functions.

You can also seek the cursor relatively based on the three types of the seek directions

  • std::ios::beg — from the beginning of file
  • std::ios::cur — from the current position of the cursor
  • std::ios::end — from the ending of the file or where the reader hits EOF

Note You must clear the flags using std::fstream::clear() to clear all the error bits before using seekg() or seekp(). read more

Oldest comments (1)

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited
std::ofstream *file = new std::ofstream();

There is no reason to dynamically allocate the file in your example. It's actually worse if you do because then you don't benefit from RAII, in this case the fact that the destructor will automatically flush and close the file upon destruction.

Even if you do dynamically allocate objects, you should never use native pointers, but something like std::unique_ptr.

this->name = ""

There's no reason to initialize a std::string like this since it has a default constructor that does the same thing, except more efficiently. Also, you forgot the ;.

out_file->write(reinterpret_cast<char*>&student, sizeof(student));

That's the wrong way to write that out. You're writing out implementation-defined bytes, both that the compiler may introduce to your class for padding, and also for std::string itself. It's also not portable.

The correct way is to overload << for Student like:

std::ostream& operator<<( std::ostream &o, Student const &s ) {
    return o << s.name << ',' << s.age;
}
Enter fullscreen mode Exit fullscreen mode

where you choose a serialization format yourself. You would then also overload >> to deserialize the object.