File Streams

A detailed guide to reading and writing files in C++ using the standard library’s fstream type
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

Working with files typically forms a big part of our C++ projects. We often need to read data from and write data to locations on the user's file system.

To do this, we use file streams. As the name suggests, these are streams, so we'll be leaning heavily on what we learned in our previous chapter. File streams are direct children of istream and ostream, so often we'll be using the exact same methods.

The standard library's implementation of file streams is available within the fstream header:

#include <fstream>

We have three main options for creating file streams:

  • std::ifstream - An input stream, used for reading data from a file. This inherits from std::istream.
  • std::ofstream - An output stream, used for writing data to a file. This inherits from std::ostream.
  • std::fstream - A bidirectional stream, which allows us to both read and write data to the same file. This inherits from std::iostream, which in turn inherits from both std::istream and std::ostream

Opening and Closing a File Stream

In this lesson, we’ll work mostly with the bidirectional fstream object. We can construct it without any arguments:

#include <fstream>

int main() {
  std::fstream File;
}

We do not need to provide a path to a specific file during construction. Instead, we can connect our stream to a file in the underlying file system at any time using the open() method, and we close that connection using close()

#include <fstream>

int main() {
  std::fstream File;
  File.open(R"(c:\test\file.txt)");
  // Use the file, then close it
  File.close();
}

A file stream does not necessarily have a one-to-one mapping to a file. We can reuse the file stream for different files:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File;

  File.open(R"(c:\test\file1.txt)");
  // Do stuff
  File.close();

  File.open(R"(c:\test\file2.txt)");
  // Do stuff
  File.close();
}

Building on what we covered in the previous lesson, we can pass a std::filesystem::path object to open():

#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;

int main() {
  std::fstream File;
  fs::path Path{R"(c:\test\file.txt)"};

  File.open(Path);

  File.close();
}

Or, we can pass a std::filesystem::directory_entry:

#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;

int main() {
  std::fstream File;
  fs::directory_entry DirectoryEntry{
      R"(c:\test\file.txt)"};

  File.open(DirectoryEntry);

  File.close();
}

We can check if a file is open using the is_open() method:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File;

  File.open(R"(c:\test\file.txt)");
  if (File.is_open()) {
    std::cout << "File is open";
  }
}
File is open

Generally, we should not open a file until we need it. However, we do have the option to open the file at the same time we create the file stream. We can do this by passing a path to the constructor:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File{R"(c:\test\file.txt)"};

  if (File.is_open()) {
    std::cout << "File is open";
  }
}
File is open

Open Modes

In addition to passing the path to the open() function or the file stream constructor, we can pass a second argument, specifying the open mode. Open modes define what we can do with our file, and how some of our operations behave.

  • std::ios::in - Allow input to be read from the file. This is enabled by default for input and bidirectional file streams
  • std::ios::out - Allows output to be written to the file. This is enabled by default for output and bidirectional file streams
  • std::ios::ate ("at end") - move the input and output positions to the end of the file upon opening
  • std::ios::app ("append") - write operations are done at the end of the file, regardless of where the output position is
  • std::ios::binary - open the file in binary mode. We cover binary streams in more detail later in the chapter
  • std::ios::trunc ("truncate") - when the file is opened, its existing contents are deleted
  • std::ios::noreplace - if the file we’re opening already exists, throw an exception. In other systems, this sometimes is referred to as opening the file in "exclusive mode".

The noreplace flag was added in C++23 and is not yet supported on all compilers, but can be replicated in other ways. For example, we can check whether a file exists using the fs::exists function, or the exists() method on an fs::directory_entry.

Open modes are provided as the second argument to open() or as the second argument to the file stream constructor:

#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path{R"(c:\test\file.txt)"};

  File.open(Path, ios::out);

  File.close();
}

Open modes are a bitmask, so we can combine multiple modes using the | operator:

#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path{R"(c:\test\file.txt)"};

  File.open(Path, ios::out | ios::noreplace);

  File.close();
}

Open Mode Interactions and Creating Files

We can combine open modes in dozens of different ways. Unfortunately, how these combinations interact is not always obvious. For example, if the file we’re trying to open doesn’t exist, it might get created, or it might not. It depends on the open modes we set.

The file will be created in the following scenarios:

  • Output mode (std::ios::out) is set, and input mode (std::ios::in) is not set. This is the default behavior of an output file stream. Therefore, if we open() a std::ofstream without passing any flags, std::ios::out will be set in isolation, and the file will be created if it doesn’t already exist.
  • Append mode (std::ios::app) or truncate mode (std::ios::trunc) is set, regardless of any other flags.

Aside from this, there are many more unintuitive behaviors and interactions between open modes. Some combinations will throw errors. Some combinations will work but behave in ways we weren't expecting. This is a common source of confusion and bugs.

In general, if your code is compiling but your file is not being written to or updated in the way we expect, the issue is almost always the combination of open modes we’ve selected.

It’s much too complex to memorize all the interactions - we can just look them up when needed. A full table of the interactions is available on a standard library reference, such as cppreference.com.

File Error Handling

File streams have the same error-handling mechanisms as any other stream. We covered those in the previous chapter, and will talk about them in more detail a little later in this lesson,

However, file streams have unique error scenarios we need to consider, caused by their interactions with the underlying filesystem.

Usually, the first error we’ll want to check for when working with files is whether the file is open when we expect it to be.

We can do that with the is_open() method. In the following examples, we’re creating an error by trying to open a file that already exists, using the noreplace open mode.

#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path(R"(c:\test\file.txt)");
  File.open(Path, ios::out | ios::noreplace);

  if (!File.is_open()) {
    std::cout << "The file was not opened";
  }
  File.close();
}
The file was not opened

As we covered in our lesson on output streams, we can use the exceptions() method to cause our stream to throw exceptions when it encounters errors. The exception type will be a std::ios::failure:

#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path(R"(c:\test\file.txt)");
  File.exceptions(ios::failbit | ios::badbit);

  try {
    File.open(Path, ios::out | ios::noreplace);
  } catch (const std::ios::failure& e) {
    std::cout << "The file was not opened";
  }
  File.close();
}
The file was not opened

This exception has a what() and code() method, but they’re not especially useful in this scenario. When we want to find out what went wrong when accessing the file system, we usually need to talk to the operating system, and each system handles and reports errors differently.

System-Specific File Errors

We generally focus on portable code (ie, things that work across as many systems as possible), but we’ll briefly introduce some ways of working with system-specific issues here. For this example, we’ll assume our code is being built for Windows.

By including the windows.h header, we get access to some additional functions.

The GetLastError() function returns the error code of the last issue that occurred on the thread that called it:

#include <windows.h>
#include <filesystem>
#include <fstream>
#include <iostream>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path(R"(c:\test\file.txt)");
  File.open(Path, ios::out | ios::noreplace);

  if (!File.is_open()) {
    std::cout << "Error: " << GetLastError();
  }
}
Error: 80

All the Windows error codes are listed on the official Microsoft site.

In this example, the 80 error code relates to a file already existing. We can use these error codes to have our code react appropriately to specific problems we anticipate might happen.

The standard library also provides access to an object that can help us with system errors. This can be accessed through the std::system_category() function from <system_error>.

This object has a few useful utilities, including the ability to generate an error message from an operator-system-specific error code:

#include <windows.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
using std::ios;

int main() {
  std::fstream File;
  fs::path Path(R"(c:\test\file.txt)");
  File.open(Path, ios::out | ios::noreplace);

  if (!File.is_open()) {
    std::cout << std::system_category().message(
        GetLastError());
  }
}
The file exists.

Using File Streams

Once we’ve opened our file stream, we just use it like we would any other input and output stream. We covered input and output streams in detail here:

The rest of this lesson covers no new topics - it just shows examples of how the concepts covered in those lessons also work as we’d expect with file streams.

Writing to a File

We write to files using the same methods we write to other output streams, such as the << operator, the put() function, and the write() function:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File;
  File.open(R"(c:\test\file.txt)");
  File << "Hello World";
  File.close();
}

After running this code, we should have the file defined within path containing the "Hello World" content.

Reading from a File

We read from files in the same way we read from other input streams, using the >> operator, and methods such as get() and getline().

#include <fstream>
#include <iostream>

int main() {
  std::string Content;

  std::fstream File;
  File.open(R"(c:\test\file.txt)");
  std::getline(File, Content);
  File.close();

  std::cout << Content;
}

This program should print out the first line of the file we specified within our open() call:

Hello World

File Stream Read and Write Position

Like any other stream, file streams can have input and output positions. We use tellp() and seekp() to access and change the output position, whilst tellg() and seekg() work with the input position.

#include <fstream>
#include <iostream>

std::fstream File;

void ReadFile() {
  File.seekg(0);
  std::cout << "File Contents:\n"
            << File.rdbuf() << "\n\n";
}

void ReadCharacter(int Position) {
  File.seekp(Position);
  std::cout << "Character at Input Position "
            << File.tellp() << ": "
            << static_cast<char>(File.get())
            << "\n";
}

void WriteString(std::string String,
                 int Position) {
  File.seekg(Position);
  std::cout << "Writing \"" << String
            << "\" to Output Position "
            << File.tellg() << "\n\n";
  File << String;
}

int main() {
  File.open(R"(c:\test\file.txt)");

  ReadFile();
  ReadCharacter(0);
  ReadCharacter(3);
  ReadCharacter(6);
  WriteString("Everyone", 6);
  ReadFile();

  File.close();
}
File Contents:
Hello World

Character at Input Position 0: H
Character at Input Position 3: l
Character at Input Position 6: W
Writing "Everyone" to Output Position 6

File Contents:
Hello Everyone

When we call open(), the input and output positions are reset:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File{};

  File.open(R"(c:\test\file1.txt)");
  File.seekp(5);
  std::cout << "Output position: "
            << File.tellp();
  File.close();

  File.open(R"(c:\test\file2.txt)");
  std::cout << "\nOutput position: "
            << File.tellp();
  File.close();
}
Output position: 5
Output position: 0

Stream Error Handling

As with the streams we covered in previous lessons, file streams have internal states that we can use to check for errors.

They set failbit, badbit, and eofbit in the usual way, which we can check for directly using rdstate(), or shorthand methods like good() and fail(). Below, we read characters until our stream runs out of content:

#include <fstream>
#include <iostream>

int main() {
  std::fstream File;
  File.open(R"(c:\test\file.txt)");

  while (File.good()) {
    std::cout << static_cast<char>(File.get());
  }

  File.close();
}
Hello World

We can also use the exceptions method, to cause our streams to throw once something goes wrong. In the following example, we’re trying to read from a file that has no content:

#include <fstream>
#include <iostream>
using std::ios;

int main() {
  std::fstream File;
  File.exceptions(ios::badbit | ios::failbit);
  File.open(R"(c:\test\no-content.txt)");

  try {
    std::cout.put(File.get());
  } catch (const std::ios::failure& e) {
    std::cout << e.code() << '\n';
    std::cout << e.what();
  }
  File.close();
}

Typically, the error codes and messages are not very descriptive:

iostream:1
ios_base::failbit set: iostream stream error

If we need to understand the error, we must rely on OS-specific methods, as described in the previous section.

For example, on Windows, this specific scenario would generate a 131 error code from GetLastError().

Passing this to the message() method of the object returned from std::system_category() yields the string: "An attempt was made to move the file pointer before the beginning of the file."

The message is a little cryptic, but it means we tried to read content from our stream (using get()) when the output position was outside the bounds of the file.

Saving and Loading State from Files

The following example shows how we can use file streams to create a rudimentary save/load feature.

The following class gives its objects the ability to save and load their state from a stream. Remember, file streams inherit from the basic stream types, so:

  • an ifstream is an istream
  • an ofstream is an ostream
  • an fstream is an iostream, which is both an istream and an ostream.

Our functions accept references by the base type, as they do not need to be restricted to just working with file streams.

#pragma once
#include <iostream>

class Character {
 public:
  void Save(std::ostream& Stream) {
    Stream << Name << ' ' << Level;
  }

  void Load(std::istream& Stream) {
    Stream >> Name;
    Stream >> Level;
  }

  std::string Name;
  int Level;
};

Over in our main function, we check if a save file exists. If it does, we load our previous state; otherwise, we start from a clean slate.

#include <filesystem>
#include <fstream>
#include <iostream>
#include "Character.h"

namespace fs = std::filesystem;

int main() {
  Character Player;

  std::fstream File;
  fs::directory_entry SaveFile{
      R"(c:\test\savefile.txt)"};

  if (SaveFile.is_regular_file()) {
    std::cout << "Loading a saved game\n";
    File.open(SaveFile, std::ios::in);
    Player.Load(File);
  } else {
    std::cout << "Starting a new game\n";
    File.open(SaveFile, std::ios::out);
    Player.Name = "Conan";
    Player.Level = 1;
    Player.Save(File);
  }

  File.close();

  std::cout << "Name: " << Player.Name;
  std::cout << "\nLevel: " << Player.Level;
}

The first time we run the program, we get this output:

Starting a new game
Name: Conan
Level: 1

On subsequent runs, we get this:

Loading a saved game
Name: Conan
Level: 1

This process of converting our objects into forms that we can store on disk, or send across the internet, is called serialization.

We expand this concept to more complex use cases in the rest of this chapter.

Summary

In this lesson, we explored the fundamental aspects of file streams in C++, covering how to open, read, write, and handle files. We also delved into understanding the various file stream open modes, error handling techniques, and practical applications, enhancing your proficiency in managing file operations.

Key Takeaways:

  • Learned the basics of file streams in C++ using std::fstream, std::ifstream, and std::ofstream, and their inheritance from istream and ostream.
  • Explored how to open and close file streams, and the versatility of file streams in handling multiple files.
  • Understood the different open modes (std::ios::in, std::ios::out, std::ios::ate, etc.) and their implications in file operations.
  • Gained insights into error handling in file streams, including the use of is_open(), std::ios::failure, and system-specific error handling methods.
  • Discussed the concept of serialization and its application in saving and loading object states from files.

Was this lesson useful?

Next Lesson

Installing vcpkg on Windows

An introduction to C++ package managers, and a step-by-step guide to installing vcpkg on Windows and Visual Studio.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Files and Serialization
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Installing vcpkg on Windows

An introduction to C++ package managers, and a step-by-step guide to installing vcpkg on Windows and Visual Studio.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved