C++ · lesson 19 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7File I/O in C++
C++ file I/O run for real: fstream open modes and silent failures, line/word/whole-file reads, CSV parsing, binary records, and std::filesystem.
By SkillByExample · Updated
<fstream> streams are RAII wrappers: the constructor opens the file and the destructor closes it, so handles don’t leak even when exceptions fly. C++17’s <filesystem> adds portable path and directory operations. Every example below runs in an empty directory.
Opening Files: ifstream and ofstream
std::ofstream writes, std::ifstream reads. The important part is what happens when an open fails.
#include <fstream>
#include <iostream>
#include <string>
int main() {
{
std::ofstream out("output.txt"); // creates or truncates
if (!out) { std::cerr << "cannot open output.txt\n"; return 1; }
out << "Hello, file!\n" << 42 << "\n";
} // closed (and flushed) here
std::ifstream in("output.txt");
std::string line;
int n = 0;
while (std::getline(in, line)) std::cout << ++n << ": " << line << "\n";
std::ifstream missing("no-such-file.txt");
std::cout << std::boolalpha << "missing opened? " << missing.is_open() << "\n";
int value = -1;
missing >> value; // silently does nothing
std::cout << "read from missing: " << value << ", fail() = " << missing.fail() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o basic basic.cpp
$ ./basic
1: Hello, file!
2: 42
missing opened? false
read from missing: -1, fail() = true
The inner braces close out, which flushes it, before the file is reopened for reading. The missing file is the lesson: opening it threw nothing, and reading from it threw nothing either — value kept its -1 and only fail() reveals the problem. Check a stream immediately after opening it (if (!in)), or call in.exceptions(std::ios::failbit) to make failures throw.
Open Modes
Modes combine with | and decide whether existing content is kept, truncated or appended to.
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
std::string slurp(const char* path) {
std::ifstream in(path);
std::ostringstream buf;
buf << in.rdbuf();
return buf.str();
}
int main() {
{ std::ofstream log("app.log"); log << "start\n"; }
{ std::ofstream log("app.log", std::ios::app); log << "appended\n"; }
std::cout << "after app:\n" << slurp("app.log");
{ std::ofstream log("app.log"); log << "oops\n"; } // default mode truncates
std::cout << "after plain ofstream:\n" << slurp("app.log");
{
std::fstream rw("app.log", std::ios::in | std::ios::out); // no truncation
rw.seekp(0);
rw << "OO"; // overwrite 2 bytes
}
std::cout << "after in|out overwrite:\n" << slurp("app.log");
}
$ g++ -std=c++20 -Wall -Wextra -o modes modes.cpp
$ ./modes
after app:
start
appended
after plain ofstream:
oops
after in|out overwrite:
OOps
std::ios::app sends every write to the end. A plain std::ofstream defaults to out | trunc, so the third open silently wiped both lines — the most common way to lose a log file. in | out opens an existing file without truncating it, and seekp(0) positioned the write at the start, so “OO” overwrote the first two bytes of “oops”. There is no insert mode: writing in the middle of a file overwrites.
Reading Strategies
Line by line, token by token, or everything at once:
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::ifstream in("poem.txt");
std::string line;
int lines = 0;
while (std::getline(in, line)) ++lines; // line by line
in.clear(); // reset EOF state...
in.seekg(0); // ...and rewind
std::string word;
int words = 0;
while (in >> word) ++words; // whitespace-separated tokens
std::ifstream again("poem.txt");
std::ostringstream buf;
buf << again.rdbuf(); // whole file at once
std::string all = buf.str();
std::cout << lines << " lines, " << words << " words, " << all.size() << " bytes\n";
std::cout << "last word: " << word << "\n";
}
$ printf 'Roses are red,\nviolets are blue,\nC++ streams are RAII\nand so are you.\n' > poem.txt
$ g++ -std=c++20 -Wall -Wextra -o reading reading.cpp
$ ./reading
4 lines, 14 words, 70 bytes
last word: you.
After the getline loop hits end-of-file, the stream is in a failed state; clear() resets it and seekg(0) rewinds — without clear(), the word loop would read nothing. >> skips any whitespace, so line breaks don’t matter to it. buf << again.rdbuf() copies the whole file in one go; its 70 bytes include the four newlines that getline strips.
Parsing CSV with std::istringstream
An std::istringstream turns one line into a stream, and std::getline(ss, field, ',') reads up to each comma.
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
struct Record { std::string name; int age; double score; };
int main() {
std::ifstream in("people.csv");
std::string line;
std::getline(in, line); // skip the header
std::vector<Record> records;
int line_no = 1;
while (std::getline(in, line)) {
++line_no;
std::istringstream ss(line);
Record r;
std::string age, score;
std::getline(ss, r.name, ','); // read up to each comma
std::getline(ss, age, ',');
std::getline(ss, score, ',');
try {
r.age = std::stoi(age);
r.score = std::stod(score);
records.push_back(r);
} catch (const std::exception& e) {
std::cout << "line " << line_no << " skipped (" << e.what() << "): " << line << "\n";
}
}
for (const auto& r : records) std::cout << r.name << " | " << r.age << " | " << r.score << "\n";
}
$ printf 'name,age,score\nAlice,30,98.5\nBob,n/a,87\nCarol,41,77.25\n' > people.csv
$ g++ -std=c++20 -Wall -Wextra -o csv csv.cpp
$ ./csv
line 3 skipped (stoi): Bob,n/a,87
Alice | 30 | 98.5
Carol | 41 | 77.25
Bob’s age isn’t a number, so std::stoi threw std::invalid_argument (whose what() in libstdc++ is just the function name) and the line was skipped with a message instead of crashing the whole import. Real-world CSV also has quoted fields containing commas, which this simple splitter doesn’t handle — use a CSV library for anything you don’t control.
Binary File I/O
write() and read() move raw bytes. Open with std::ios::binary and cast the object’s address to char*.
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>
struct Packet { std::uint32_t id; float value; };
int main() {
std::vector<Packet> out_packets{{1, 1.5f}, {2, -2.0f}};
{
std::ofstream out("data.bin", std::ios::binary);
std::uint32_t count = out_packets.size();
out.write(reinterpret_cast<const char*>(&count), sizeof count);
out.write(reinterpret_cast<const char*>(out_packets.data()), count * sizeof(Packet));
}
std::cout << "sizeof(Packet) = " << sizeof(Packet) << ", file size = "
<< std::filesystem::file_size("data.bin") << " bytes\n";
std::ifstream in("data.bin", std::ios::binary);
std::uint32_t count = 0;
in.read(reinterpret_cast<char*>(&count), sizeof count);
std::vector<Packet> packets(count);
in.read(reinterpret_cast<char*>(packets.data()), count * sizeof(Packet));
for (const auto& p : packets) std::cout << "id " << p.id << " value " << p.value << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o binary binary.cpp
$ ./binary
sizeof(Packet) = 8, file size = 20 bytes
id 1 value 1.5
id 2 value -2
$ od -A d -t x1 data.bin
0000000 02 00 00 00 01 00 00 00 00 00 c0 3f 02 00 00 00
0000016 00 00 00 c0
0000020
The file is 4 bytes of count plus two 8-byte packets: 20 bytes. od shows the layout exactly: 02 00 00 00 is the count in little-endian byte order (least significant byte first, as x86 stores it), then 01 00 00 00 for id 1 and 00 00 c0 3f, the IEEE-754 float 1.5, also little-endian. A big-endian machine would read the count as 33554432. That, plus padding and type sizes, is why raw struct dumps are not a portable file format.
std::filesystem (C++17)
Paths, directories, copying and iteration, portably:
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <vector>
namespace fs = std::filesystem;
int main() {
fs::path root = "demo";
fs::create_directories(root / "data" / "output"); // makes every missing level
fs::path file = root / "data" / "input.csv";
std::ofstream(file) << "a,b\n1,2\n"; // temporary stream, closed at ;
std::cout << "path " << file << "\n parent " << file.parent_path() << ", filename "
<< file.filename() << ", stem " << file.stem() << ", ext " << file.extension() << "\n";
std::cout << "size " << fs::file_size(file) << " bytes\n";
fs::copy(file, root / "data" / "output" / "input.csv", fs::copy_options::overwrite_existing);
fs::rename(root / "data" / "output" / "input.csv", root / "data" / "output" / "done.csv");
std::vector<fs::path> all;
for (const auto& entry : fs::recursive_directory_iterator(root)) all.push_back(entry.path());
std::sort(all.begin(), all.end()); // iteration order is unspecified
for (const auto& p : all) std::cout << (fs::is_directory(p) ? "[dir] " : "[file] ") << p.string() << "\n";
std::error_code ec;
auto n = fs::file_size("missing.txt", ec); // no exception
std::cout << "missing.txt: " << ec.message() << " (returned " << static_cast<long long>(n) << ")\n";
std::cout << "removed " << fs::remove_all(root) << " entries\n";
try { std::cout << fs::file_size("missing.txt"); }
catch (const fs::filesystem_error& e) { std::cout << "throwing overload: " << e.what() << "\n"; }
}
$ g++ -std=c++20 -Wall -Wextra -o fs fs.cpp
$ ./fs
path "demo/data/input.csv"
parent "demo/data", filename "input.csv", stem "input", ext ".csv"
size 8 bytes
[dir] demo/data
[file] demo/data/input.csv
[dir] demo/data/output
[file] demo/data/output/done.csv
missing.txt: No such file or directory (returned -1)
removed 5 entries
throwing overload: filesystem error: cannot get file size: No such file or directory [missing.txt]
operator/ joins path components with the platform’s separator. create_directories made all three levels at once. recursive_directory_iterator visits entries in an unspecified order, so the program sorts them before printing. The last lines show the two error styles: the std::error_code overload of file_size reports “No such file or directory” and returns static_cast<uintmax_t>(-1), printed here as -1, while the throwing overload raises filesystem_error with the operation and path in its message. remove_all returns how many entries it deleted: 5 files and directories.
Putting It Together: a File Processor
Iterate a directory, filter by extension, transform each file line by line into a parallel output directory:
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
int main() {
fs::path input = "input", output = "output";
fs::create_directories(output);
for (const auto& entry : fs::directory_iterator(input)) {
if (entry.path().extension() != ".txt") continue; // skip other files
std::ifstream in(entry.path());
std::ofstream out(output / entry.path().filename());
std::string line;
int count = 0;
while (std::getline(in, line)) out << ++count << ": " << line << "\n";
std::cout << "processed " << entry.path().filename() << " (" << count << " lines)\n";
}
}
$ mkdir input && printf 'first\nsecond\n' > input/notes.txt && printf 'x\n' > input/todo.txt && printf 'skip me' > input/image.png
$ g++ -std=c++20 -Wall -Wextra -o processor processor.cpp
$ ./processor
processed "todo.txt" (1 lines)
processed "notes.txt" (2 lines)
$ cat output/notes.txt
1: first
2: second
image.png was skipped by the extension check, and each .txt file got numbered lines in output/. directory_iterator order is unspecified, so the two “processed” lines can appear in either order on another filesystem.
Practice
-
Make
basic.cppthrow instead of failing silently whenno-such-file.txtcan’t be opened.Solution
std::ifstream missing; missing.exceptions(std::ios::failbit | std::ios::badbit); try { missing.open("no-such-file.txt"); } catch (const std::ios_base::failure& e) { std::cout << "open failed: " << e.what() << "\n"; }Set
exceptions()beforeopen(). Beware that withfailbitenabled, agetlineloop also throws when it reaches end of file. -
Write a function
count_lines(const fs::path&)that returnsstd::optional<int>— empty if the file can’t be opened.Solution
std::optional<int> count_lines(const std::filesystem::path& p) { std::ifstream in(p); if (!in) return std::nullopt; int n = 0; for (std::string line; std::getline(in, line);) ++n; return n; }count_lines("poem.txt")returns 4, and a missing file givesstd::nullopt. -
In
csv.cpp, also reject lines where the age parses but has trailing junk, likeDan,4x,50.Solution
std::size_t used = 0; r.age = std::stoi(age, &used); if (used != age.size()) throw std::invalid_argument("age");std::stoi("4x")returns 4 and setsusedto 1; comparing it to the field length catches the leftoverx. -
Change
binary.cppto store thecountas big-endian bytes explicitly, so the file is the same on every platform.Solution
unsigned char be[4] = { static_cast<unsigned char>(count >> 24), static_cast<unsigned char>(count >> 16), static_cast<unsigned char>(count >> 8), static_cast<unsigned char>(count)}; out.write(reinterpret_cast<const char*>(be), 4); // reading: unsigned char b[4]; in.read(reinterpret_cast<char*>(b), 4); std::uint32_t count = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3];Shifts operate on values, not memory layout, so this code produces
00 00 00 02on any machine. -
Extend
processor.cppto print the total size in bytes of all.txtfiles it processed, usingstd::filesystem.Solution
std::uintmax_t total = 0; // inside the loop, after the extension check: total += entry.file_size(); // after the loop: std::cout << "total " << total << " bytes\n";directory_entry::file_size()may use cached metadata from the directory scan. For the sample input it prints 15 (13 + 2).