Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Reading and Writing Files
Open, read, write, and close files with fstream classes.
Basic file I/O
File I/O in C++ closely resembles console I/O operations with a few additional considerations. C++ provides three fundamental file I/O classes: ifstream (derived from istream) for reading files, ofstream (derived from ostream) for writing files, and fstream (derived from iostream) for both reading and writing. Include the <fstream> header to use these classes.
Unlike std::cout, std::cin, std::cerr, and std::clog which are pre-configured and ready to use, file streams require explicit setup. Fortunately, this process is straightforward: instantiate a file stream object with the filename as a constructor parameter. Then use the insertion (<<) or extraction (>>) operators to write or read data. When finished, either explicitly call close() or allow the file stream object to go out of scope (the destructor automatically closes the file).
Writing data to files
The ofstream class handles file output operations:
#include <fstream>
#include <iostream>
int main()
{
std::ofstream configFile{ "settings.cfg" };
if (!configFile)
{
std::cerr << "ERROR: Could not create settings.cfg\n";
return 1;
}
configFile << "resolution=1920x1080\n";
configFile << "volume=75\n";
configFile << "fullscreen=true\n";
return 0;
// configFile closes automatically when it goes out of scope
}
Check your project directory - you should find settings.cfg containing the three configuration lines we wrote.
You can also use put() to write individual characters to files.
Reading data from files
The ifstream class handles file input operations. Note that ifstream returns false when it reaches the end of the file (EOF), which we'll use to determine how much data to read:
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ifstream configFile{ "settings.cfg" };
if (!configFile)
{
std::cerr << "ERROR: Could not open settings.cfg for reading\n";
return 1;
}
std::string setting{};
while (configFile >> setting)
{
std::cout << setting << '\n';
}
return 0;
// configFile closes automatically when it goes out of scope
}
Output:
resolution=1920x1080
volume=75
fullscreen=true
Wait - that's not quite right. The extraction operator breaks on whitespace, so if our configuration file contained theme=dark mode, we'd get:
theme=dark
mode
To read complete lines including whitespace, use getline():
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ifstream configFile{ "settings.cfg" };
if (!configFile)
{
std::cerr << "ERROR: Could not open settings.cfg for reading\n";
return 1;
}
std::string line{};
while (std::getline(configFile, line))
{
std::cout << line << '\n';
}
return 0;
}
This correctly reads entire lines, preserving any whitespace within them.
Understanding buffered output
C++ may buffer file output for performance optimization. Instead of writing each output operation directly to disk, the system accumulates several operations and writes them together. This process is called flushing the buffer. Closing a file automatically flushes its buffer to disk.
Buffering rarely causes problems, but critical situations exist where it matters. If your program terminates abnormally (via crash or exit() call) with unflushed data in the buffer, that data never reaches the disk. The file stream destructor never runs, the file never closes, the buffer never flushes, and your data is lost. Always explicitly close files before calling exit().
You can manually flush buffers using the flush() member function or by inserting std::flush into the output stream:
#include <fstream>
int main()
{
std::ofstream logFile{ "application.log" };
logFile << "[CRITICAL] System error detected\n";
logFile.flush(); // Ensure this critical message reaches disk immediately
// Even if the program crashes here, the message is safely on disk
return 0;
}
Performance-conscious programmers often prefer '\n' over std::endl when writing to files because std::endl flushes the buffer after inserting a newline. Unnecessary flushes can significantly impact performance during extensive file I/O operations.
File opening modes
By default, ofstream overwrites existing files completely. What if we want to add data to the end of an existing file instead? File stream constructors accept an optional second parameter specifying how to open the file. This parameter, called the mode, accepts flags from the ios class:
| Mode Flag | Meaning |
|---|---|
app |
Append mode - adds data to the end of the file |
ate |
Seeks to the end of the file before any read/write operations |
binary |
Opens the file in binary mode (instead of text mode) |
in |
Opens the file for reading (default for ifstream) |
out |
Opens the file for writing (default for ofstream) |
trunc |
Deletes existing file contents if the file exists |
Combine multiple flags using the bitwise OR operator (|). By default, ifstream uses std::ios::in, ofstream uses std::ios::out, and fstream uses std::ios::in | std::ios::out.
Important note: Due to fstream's design, opening a file with std::ios::in fails if the file doesn't exist. When creating new files with fstream, use only std::ios::out mode.
Let's append additional configuration settings to our settings.cfg file:
#include <fstream>
#include <iostream>
int main()
{
std::ofstream configFile{ "settings.cfg", std::ios::app };
if (!configFile)
{
std::cerr << "ERROR: Could not open settings.cfg for appending\n";
return 1;
}
configFile << "language=english\n";
configFile << "autosave=true\n";
return 0;
}
Now settings.cfg contains:
resolution=1920x1080
volume=75
fullscreen=true
language=english
autosave=true
Explicitly using open() and close()
Besides using the constructor to open files, you can explicitly call the open() member function. This function accepts the same parameters as the constructor (filename and optional mode):
#include <fstream>
int main()
{
std::ofstream dataFile{};
dataFile.open("session1.dat");
dataFile << "User logged in at 09:00\n";
dataFile << "User completed task A\n";
dataFile.close();
dataFile.open("session2.dat");
dataFile << "User logged in at 14:30\n";
dataFile << "User completed task B\n";
dataFile.close();
return 0;
}
This creates two separate data files using a single ofstream object.
Practical example: Saving and loading application state
Here's a complete example that saves application state and loads it back:
#include <fstream>
#include <iostream>
#include <string>
void saveUserPreferences(const std::string& username, int score, bool notifications)
{
std::ofstream saveFile{ "userdata.txt" };
if (!saveFile)
{
std::cerr << "ERROR: Could not save user preferences\n";
return;
}
saveFile << username << '\n';
saveFile << score << '\n';
saveFile << notifications << '\n';
std::cout << "Preferences saved successfully\n";
}
void loadUserPreferences()
{
std::ifstream saveFile{ "userdata.txt" };
if (!saveFile)
{
std::cerr << "ERROR: Could not load user preferences\n";
return;
}
std::string username{};
int score{};
bool notifications{};
std::getline(saveFile, username);
saveFile >> score;
saveFile >> notifications;
std::cout << "Loaded preferences:\n";
std::cout << " Username: " << username << '\n';
std::cout << " High Score: " << score << '\n';
std::cout << " Notifications: " << (notifications ? "enabled" : "disabled") << '\n';
}
int main()
{
saveUserPreferences("PlayerOne", 9500, true);
std::cout << "\n--- Restarting application ---\n\n";
loadUserPreferences();
return 0;
}
Output:
Preferences saved successfully
--- Restarting application ---
Loaded preferences:
Username: PlayerOne
High Score: 9500
Notifications: enabled
Checking if files are open
The is_open() member function returns true if a stream is currently connected to a file:
#include <fstream>
#include <iostream>
int main()
{
std::ofstream logFile{ "system.log" };
if (logFile.is_open())
{
std::cout << "Log file is ready for writing\n";
logFile << "Application started\n";
}
logFile.close();
if (!logFile.is_open())
{
std::cout << "Log file has been closed\n";
}
return 0;
}
Deleting files
To delete a file, use the remove() function from <cstdio>:
#include <cstdio>
#include <iostream>
int main()
{
if (std::remove("temporary.tmp") == 0)
{
std::cout << "Temporary file deleted successfully\n";
}
else
{
std::cout << "Failed to delete temporary file\n";
}
return 0;
}
Warning about writing pointers to files
Never write memory addresses to files. While technically possible to write a pointer's address value to disk, it's extremely dangerous. Variables occupy different memory addresses each time a program runs. An address that was valid when you wrote it to disk will almost certainly be invalid when you read it back.
Consider this scenario: You store an integer sessionCount at address 0x00405C10 with value 42. You write the value 42 and address 0x00405C10 to disk. Later, you load these values. But now sessionCount lives at 0x00405C08. Your loaded pointer still points to 0x00405C10 - which now contains random data or belongs to a different variable. Attempting to access this pointer causes undefined behavior.
Instead of saving pointers, save the actual data values and reconstruct your program's state from those values when loading.
File I/O in C++ provides powerful capabilities for persistent data storage. The next lesson explores random file access, which allows reading and writing at arbitrary file positions rather than sequentially from beginning to end.
Summary
File stream classes: C++ provides ifstream for reading files, ofstream for writing files, and fstream for both operations. Include the
Writing to files: Use ofstream and the insertion operator (<<) to write data to files. Files are created automatically if they don't exist. The destructor automatically closes files when they go out of scope.
Reading from files: Use ifstream and the extraction operator (>>) or getline() to read data. Use getline() to preserve whitespace within lines.
Buffered output: C++ may buffer file output for performance. Files are automatically flushed when closed, but abnormal termination can lose unflushed data. Use flush() or std::flush to manually force data to disk.
Performance considerations: Prefer '\n' over std::endl when writing to files because std::endl flushes the buffer after every newline, significantly impacting performance during extensive I/O.
File opening modes: Use ios flags like app (append), ate (seek to end), binary (binary mode), in (reading), out (writing), and trunc (truncate). Combine flags with bitwise OR (|).
Using open() and close(): Besides constructor initialization, explicitly call open() to open files and close() to close them, enabling file reuse with a single stream object.
Checking file status: Use is_open() to verify whether a stream is currently connected to a file. Check for file opening failures to handle errors gracefully.
Deleting files: Use std::remove() from
Never write pointers to files: Pointer addresses are invalid across program runs. Save actual data values instead and reconstruct program state when loading.
Understanding file I/O operations enables persistent data storage, configuration file handling, log file creation, and data exchange between program runs and different applications.
Reading and Writing Files - Quiz
Test your understanding of the lesson.
Practice Exercises
Basic File I/O
Learn to read from and write to files using fstream. Practice opening, writing, reading, and closing files.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!