Advanced Topics

File Handling

Master C++ file I/O — ifstream, ofstream, fstream, text vs binary files, seekg/seekp, error handling, C++17 filesystem, and practical file processing patterns.

File Handling

C++ file I/O uses stream classes from <fstream>. They work the same way as cout/cin — you write with << and read with >> or getline. RAII means files close automatically when streams go out of scope. C++17 adds <filesystem> for directory traversal, file metadata, and path manipulation.


Opening and Closing Files

cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
    // ofstream — write only
    ofstream out("output.txt");
    if (!out.is_open()) {
        cerr << "Failed to open output.txt\n";
        return 1;
    }
    out << "Hello, File!\n";
    out << "Line 2\n";
    out.close();   // Optional — closes when out goes out of scope

    // ifstream — read only
    ifstream in("output.txt");
    if (!in) {   // Shorthand for !in.is_open()
        cerr << "Cannot read output.txt\n";
        return 1;
    }

    string line;
    while (getline(in, line)) {
        cout << line << "\n";
    }
    // File closes automatically when 'in' goes out of scope

    return 0;
}

Open Modes

cpp
#include <fstream>
using namespace std;

// Open modes (can be combined with |)
ofstream f1("file.txt");                        // Default: out | trunc (overwrite)
ofstream f2("file.txt", ios::app);             // Append to existing
ofstream f3("file.txt", ios::out | ios::trunc);// Overwrite (explicit)
ofstream f4("file.bin", ios::out | ios::binary);// Binary mode

// fstream — read AND write
fstream rw("file.txt", ios::in | ios::out);

// Mode flags
// ios::in     — read
// ios::out    — write
// ios::app    — append (seek to end before each write)
// ios::ate    — seek to end immediately after opening
// ios::trunc  — truncate file to zero length on open
// ios::binary — binary mode (no newline translation)

Reading Text Files

cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;

// Read entire file into a string
string readFile(const string& path) {
    ifstream f(path);
    if (!f) throw runtime_error("Cannot open: " + path);
    ostringstream ss;
    ss << f.rdbuf();   // Stream entire file through stringstream
    return ss.str();
}

// Read file line by line into vector
vector<string> readLines(const string& path) {
    ifstream f(path);
    if (!f) throw runtime_error("Cannot open: " + path);
    vector<string> lines;
    string line;
    while (getline(f, line)) {
        lines.push_back(line);
    }
    return lines;
}

// Read integers from file
vector<int> readIntegers(const string& path) {
    ifstream f(path);
    vector<int> nums;
    int n;
    while (f >> n) {   // Reads whitespace-separated integers
        nums.push_back(n);
    }
    return nums;
}

int main() {
    // Create a test file
    {
        ofstream out("test.txt");
        out << "Alice 90\nBob 85\nCharlie 78\n";
    }

    // Read line by line
    for (const auto& line : readLines("test.txt")) {
        cout << line << "\n";
    }

    // Parse name-score pairs
    ifstream f("test.txt");
    string name;
    int score;
    while (f >> name >> score) {
        cout << name << " got " << score << "\n";
    }

    return 0;
}

Writing Text Files

cpp
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;

struct Record {
    string name;
    int    age;
    double salary;
};

int main() {
    vector<Record> records = {
        {"Alice",   30, 75000.0},
        {"Bob",     25, 62000.0},
        {"Charlie", 35, 89000.0},
    };

    // Write CSV
    ofstream csv("employees.csv");
    csv << "Name,Age,Salary\n";
    for (const auto& r : records) {
        csv << r.name << "," << r.age << "," << fixed << setprecision(2) << r.salary << "\n";
    }

    // Append to log file
    ofstream log("app.log", ios::app);
    log << "[2025-03-01 10:30:00] Application started\n";
    log << "[2025-03-01 10:30:05] Processing 3 records\n";

    // Check stream state
    if (csv.fail()) {
        cerr << "Write error!\n";
    }

    return 0;
}

Binary Files

cpp
#include <iostream>
#include <fstream>
using namespace std;

struct Pixel {
    uint8_t r, g, b, a;
};

int main() {
    // Write binary data
    vector<Pixel> image(256);
    for (int i = 0; i < 256; i++) {
        image[i] = {(uint8_t)i, (uint8_t)(255-i), 128, 255};
    }

    ofstream out("image.bin", ios::binary);
    out.write(reinterpret_cast<const char*>(image.data()),
              image.size() * sizeof(Pixel));

    // Read binary data
    vector<Pixel> loaded(256);
    ifstream in("image.bin", ios::binary);
    in.read(reinterpret_cast<char*>(loaded.data()),
            loaded.size() * sizeof(Pixel));

    cout << "Loaded " << in.gcount() << " bytes\n";
    cout << "First pixel: "
         << (int)loaded[0].r << " "
         << (int)loaded[0].g << " "
         << (int)loaded[0].b << "\n";  // 0 255 128

    // Write a struct directly
    struct Header {
        char magic[4] = {'B','I','N','0'};
        uint32_t version = 1;
        uint64_t dataSize = 0;
    };

    Header h;
    h.dataSize = 256 * sizeof(Pixel);
    ofstream hout("data.bin", ios::binary);
    hout.write(reinterpret_cast<const char*>(&h), sizeof(h));

    return 0;
}

Warning: Binary serialisation is platform-dependent — endianness and struct padding may differ between machines. For portable serialisation, use text formats (JSON, CSV) or a cross-platform library like Protocol Buffers.


File Seeking

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // Create a file with known content
    {
        ofstream out("seek_test.txt");
        out << "Hello, World! This is a test.";
    }

    fstream f("seek_test.txt", ios::in | ios::out);

    // Get file size
    f.seekg(0, ios::end);        // Seek to end
    streamsize size = f.tellg(); // Position = file size
    cout << "File size: " << size << " bytes\n";  // 30

    // Read from a specific position
    f.seekg(7, ios::beg);        // Seek to offset 7 from beginning
    string word;
    f >> word;
    cout << "Word at offset 7: " << word << "\n";  // World!

    // seekg (read position) vs seekp (write position)
    f.seekp(0, ios::beg);        // Seek write position to start
    f.write("Goodbye", 7);       // Overwrite first 7 characters

    f.seekg(0, ios::beg);        // Back to start for reading
    string content;
    getline(f, content);
    cout << content << "\n";     // Goodbye, World! This is a test.

    return 0;
}

Error Handling with Streams

cpp
#include <iostream>
#include <fstream>
using namespace std;

int main() {
    // Enable exceptions on stream failure
    ifstream f;
    f.exceptions(ios::failbit | ios::badbit);

    try {
        f.open("nonexistent.txt");
    } catch (const ios_base::failure& e) {
        cerr << "File error: " << e.what() << "\n";
    }

    // Manual error checking
    ifstream f2("existing.txt");
    if (!f2) {
        // Check individual error bits
        if (f2.eof())  cerr << "End of file\n";
        if (f2.fail()) cerr << "Logical error\n";
        if (f2.bad())  cerr << "I/O error\n";

        f2.clear();   // Reset error flags
    }

    return 0;
}

C++17 <filesystem>

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

int main() {
    // Path operations
    fs::path p = "/home/user/projects/myapp";
    cout << p.filename()  << "\n";  // myapp
    cout << p.parent_path() << "\n";// /home/user/projects
    cout << p.extension() << "\n";  // (empty for directories)

    fs::path f = "/home/user/data.csv";
    cout << f.stem()      << "\n";  // data
    cout << f.extension() << "\n";  // .csv
    cout << (f.parent_path() / "output.txt") << "\n";  // /home/user/output.txt

    // Check existence and properties
    if (fs::exists("/tmp")) {
        cout << "is_dir: " << fs::is_directory("/tmp") << "\n";    // 1
        cout << "is_file: " << fs::is_regular_file("/tmp") << "\n"; // 0
    }

    // Create directories
    fs::create_directories("output/data/processed");

    // Directory iteration
    for (const auto& entry : fs::directory_iterator("output")) {
        cout << entry.path() << " ";
        cout << (entry.is_directory() ? "(dir)" : "(file)") << "\n";
    }

    // Recursive directory iteration
    for (const auto& entry : fs::recursive_directory_iterator(".")) {
        if (entry.path().extension() == ".cpp") {
            cout << entry.path() << " ("
                 << fs::file_size(entry.path()) << " bytes)\n";
        }
    }

    // Copy, move, rename, delete
    fs::copy("source.txt", "dest.txt", fs::copy_options::overwrite_existing);
    fs::rename("old.txt", "new.txt");
    fs::remove("temp.txt");
    fs::remove_all("output/data/processed");  // Remove recursively

    return 0;
}

Practical Example: CSV Database

cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

struct Employee {
    int    id;
    string name;
    string department;
    double salary;

    string toCsv() const {
        return to_string(id) + "," + name + "," + department + ","
               + to_string(salary);
    }

    static Employee fromCsv(const string& line) {
        istringstream ss(line);
        string field;
        Employee e;
        getline(ss, field, ','); e.id = stoi(field);
        getline(ss, e.name,       ',');
        getline(ss, e.department, ',');
        getline(ss, field, ',');  e.salary = stod(field);
        return e;
    }
};

class EmployeeDB {
    string         filename;
    vector<Employee> employees;
    int            nextId;

public:
    EmployeeDB(const string& file) : filename(file), nextId(1) {
        load();
    }

    void load() {
        ifstream f(filename);
        if (!f) return;   // New file — no data yet

        string line;
        getline(f, line);  // Skip header
        while (getline(f, line)) {
            if (!line.empty()) {
                auto e = Employee::fromCsv(line);
                employees.push_back(e);
                nextId = max(nextId, e.id + 1);
            }
        }
        cout << "Loaded " << employees.size() << " employees\n";
    }

    void save() const {
        ofstream f(filename, ios::trunc);
        f << "ID,Name,Department,Salary\n";
        for (const auto& e : employees) f << e.toCsv() << "\n";
        cout << "Saved " << employees.size() << " employees\n";
    }

    void add(const string& name, const string& dept, double salary) {
        employees.push_back({nextId++, name, dept, salary});
    }

    void list() const {
        cout << left << setw(4) << "ID"
             << setw(15) << "Name"
             << setw(12) << "Dept"
             << "Salary\n" << string(40, '-') << "\n";
        for (const auto& e : employees) {
            cout << setw(4)  << e.id
                 << setw(15) << e.name
                 << setw(12) << e.department
                 << fixed << setprecision(0) << e.salary << "\n";
        }
    }
};

int main() {
    EmployeeDB db("employees.csv");

    db.add("Alice",   "Engineering", 85000);
    db.add("Bob",     "Design",       70000);
    db.add("Charlie", "Marketing",    65000);

    db.list();
    db.save();

    return 0;
}

Summary

  • ifstream, ofstream, fstream inherit from standard streams — <<, >>, getline all work identically
  • Files are automatically closed when the stream goes out of scope — RAII in action
  • Open modes: ios::in (read), ios::out (write), ios::app (append), ios::binary, ios::trunc
  • seekg() moves the read position; seekp() moves the write position; tellg()/tellp() return the current position
  • Binary files use read()/write() with reinterpret_cast<char*> — beware of endianness and padding
  • Check stream state with f.fail(), f.bad(), f.eof() — or enable exceptions with f.exceptions(...)
  • C++17 <filesystem> provides path, directory_iterator, create_directories, copy, rename, remove_all

Next, we'll explore multithreading — running code in parallel with std::thread, mutexes, and futures.