Skip to main content

📝 Lesson 17: File I/O and Exceptions

Learn to read and write files safely in C++, and how exceptions and RAII let your programs handle errors gracefully without leaking resources or crashing.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read from and write to files using ifstream, ofstream, and fstream.
  • Choose and combine file opening modes (ios::in, ios::out, ios::app, ios::binary, ios::trunc, ios::ate) with the bitwise OR operator.
  • Navigate within a file using seekg/seekp and tellg/tellp, and perform binary reads and writes.
  • Handle errors robustly with try/catch, the standard exception hierarchy, and custom exception classes.
  • Apply RAII so that file handles and other resources are always released, even when an exception is thrown.

Estimated Time: 90–120 minutes

Project: Build a CSV/log file processor that reads structured data, validates it, and reports errors using exceptions instead of crashing.

In This Lesson

File Streams: Your Program's Filing Cabinet

File I/O in C++ is like having a filing system - you can read documents (input), write new ones (output), or do both! Streams are the pipelines that connect your program to files.

File Stream Classes

graph TD A[ios_base] --> B[ios] B --> C[istream] B --> D[ostream] C --> E["ifstream<br/>Input File Stream"] D --> F["ofstream<br/>Output File Stream"] C --> G[iostream] D --> G G --> H["fstream<br/>Input/Output File Stream"] style E fill:#E8F5E9,color:#111827 style F fill:#FFE4B5,color:#111827 style H fill:#F3E5F5,color:#111827

Basic File Operations

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

// Writing to a file
void writeToFile() {
    ofstream outFile("output.txt");

    if (!outFile) {
        cerr << "Error opening file for writing!" << endl;
        return;
    }

    outFile << "Hello, File I/O!" << endl;
    outFile << "Line 2" << endl;
    outFile << "The answer is: " << 42 << endl;

    outFile.close(); // Optional - destructor closes automatically
}

// Reading from a file
void readFromFile() {
    ifstream inFile("input.txt");

    if (!inFile.is_open()) {
        cerr << "Error opening file for reading!" << endl;
        return;
    }

    string line;
    while (getline(inFile, line)) {
        cout << "Read: " << line << endl;
    }

    inFile.close();
}

// Reading different data types
void readStructuredData() {
    ifstream inFile("data.txt");

    string name;
    int age;
    double salary;

    while (inFile >> name >> age >> salary) {
        cout << name << " is " << age << " years old, earns $"
             << salary << endl;
    }
}

// Binary file I/O
void binaryFileOperations() {
    // Writing binary data
    ofstream binOut("data.bin", ios::binary);

    int numbers[] = {1, 2, 3, 4, 5};
    binOut.write(reinterpret_cast<char*>(numbers), sizeof(numbers));
    binOut.close();

    // Reading binary data
    ifstream binIn("data.bin", ios::binary);
    int readNumbers[5];
    binIn.read(reinterpret_cast<char*>(readNumbers), sizeof(readNumbers));

    for (int n : readNumbers) {
        cout << n << " ";
    }
}

File Opening Modes

File opening modes are like different ways to open a door - you can open it just to look (read), to put things in (write), or both!

File Opening Modes ios::in Open for reading (default for ifstream) ios::out Open for writing (overwrites existing) ios::app Append to end (preserves existing) ios::binary Binary mode (no text formatting) ios::trunc Truncate if exists (delete contents) ios::ate At end initially (can move pointer) Combining Modes: fstream file("data.txt", ios::in | ios::out | ios::binary);

File Mode Examples

// Different ways to open files
ofstream file1("output.txt");                    // Default: ios::out
ofstream file2("output.txt", ios::app);         // Append mode
ofstream file3("output.txt", ios::out | ios::trunc); // Explicit truncate

fstream file4("data.txt", ios::in | ios::out);  // Read and write
fstream file5("binary.dat", ios::binary | ios::in | ios::out);

// Checking if file opened successfully
ifstream inFile("maybe_exists.txt");
if (inFile.is_open()) {
    // File opened successfully
} else {
    // Handle error
}

// Alternative error checking
if (!inFile) {
    cerr << "Failed to open file!" << endl;
}

// Using exceptions for file errors
ifstream exFile;
exFile.exceptions(ifstream::failbit | ifstream::badbit);
try {
    exFile.open("important.txt");
    // Read file...
} catch (const ifstream::failure& e) {
    cerr << "Exception opening file: " << e.what() << endl;
}

File Position and Seeking

File pointers are like bookmarks - they remember where you are in a file and let you jump to specific locations!

Exception Handling: Safety Net for Your Code

Exceptions are like smoke detectors - they alert you when something goes wrong so you can handle it gracefully instead of letting your program crash!

graph TD A[Normal Execution] --> B{Error Occurs?} B -->|No| C[Continue] B -->|Yes| D[Throw Exception] D --> E[Search for Handler] E --> F{Handler Found?} F -->|Yes| G[Execute catch block] F -->|No| H[Propagate up] G --> I[Continue or Exit] H --> J[Program Terminates] style D fill:#F44336,color:#111827 style G fill:#4CAF50,color:#111827 style J fill:#F44336,color:#111827

Exception Handling Basics

// Basic try-catch structure
try {
    // Code that might throw an exception
    int result = riskyOperation();
    cout << "Success: " << result << endl;
}
catch (const exception& e) {
    // Handle the exception
    cerr << "Error: " << e.what() << endl;
}

// Multiple catch blocks
try {
    processFile("data.txt");
}
catch (const ifstream::failure& e) {
    cerr << "File error: " << e.what() << endl;
}
catch (const runtime_error& e) {
    cerr << "Runtime error: " << e.what() << endl;
}
catch (const exception& e) {
    cerr << "General error: " << e.what() << endl;
}
catch (...) {
    cerr << "Unknown error occurred!" << endl;
}

// Throwing exceptions
void validateAge(int age) {
    if (age < 0) {
        throw invalid_argument("Age cannot be negative");
    }
    if (age > 150) {
        throw out_of_range("Age seems unrealistic");
    }
}

// Custom exception class
class FileFormatException : public runtime_error {
private:
    int lineNumber;

public:
    FileFormatException(const string& msg, int line)
        : runtime_error(msg), lineNumber(line) {}

    int getLineNumber() const { return lineNumber; }
};

Standard Exception Hierarchy

C++ Exception Class Hierarchy exception logic_error runtime_error invalid_argument length_error out_of_range domain_error range_error overflow_error underflow_error system_error bad_alloc

RAII and Exception Safety

RAII (Resource Acquisition Is Initialization) is like having automatic cleanup - resources are tied to object lifetime, so they're always properly released!

Exception Safety and File I/O

// RAII file wrapper
class SafeFile {
private:
    FILE* file;

public:
    SafeFile(const string& filename, const string& mode) {
        file = fopen(filename.c_str(), mode.c_str());
        if (!file) {
            throw runtime_error("Failed to open file: " + filename);
        }
    }

    ~SafeFile() {
        if (file) {
            fclose(file);
        }
    }

    // Delete copy operations
    SafeFile(const SafeFile&) = delete;
    SafeFile& operator=(const SafeFile&) = delete;

    // Move operations
    SafeFile(SafeFile&& other) noexcept : file(other.file) {
        other.file = nullptr;
    }

    FILE* get() { return file; }
};

// Exception-safe file processing
void processFilesSafely(const string& input, const string& output) {
    ifstream inFile(input);
    if (!inFile) {
        throw runtime_error("Cannot open input file");
    }

    ofstream outFile(output);
    if (!outFile) {
        throw runtime_error("Cannot open output file");
    }

    try {
        string line;
        int lineNum = 0;

        while (getline(inFile, line)) {
            lineNum++;

            // Process line - might throw
            string processed = processLine(line);

            outFile << processed << endl;
            if (!outFile) {
                throw runtime_error("Write failed at line " +
                                  to_string(lineNum));
            }
        }
    }
    catch (...) {
        // Files automatically closed by destructors
        throw; // Re-throw the exception
    }
    // Files automatically closed here too
}

Practical File Processing Example

Let's build a complete example that processes CSV files with proper error handling!

class CSVProcessor {
private:
    string filename;
    char delimiter;

public:
    CSVProcessor(const string& file, char delim = ',')
        : filename(file), delimiter(delim) {}

    vector<vector<string>> readCSV() {
        vector<vector<string>> data;
        ifstream file(filename);

        if (!file) {
            throw runtime_error("Cannot open file: " + filename);
        }

        string line;
        int lineNum = 0;

        while (getline(file, line)) {
            lineNum++;
            vector<string> row = parseLine(line, lineNum);
            data.push_back(row);
        }

        return data;
    }

private:
    vector<string> parseLine(const string& line, int lineNum) {
        vector<string> result;
        stringstream ss(line);
        string item;

        while (getline(ss, item, delimiter)) {
            // Trim whitespace
            item.erase(0, item.find_first_not_of(" \t"));
            item.erase(item.find_last_not_of(" \t") + 1);

            result.push_back(item);
        }

        if (result.empty()) {
            throw runtime_error("Empty line at line " +
                              to_string(lineNum));
        }

        return result;
    }
};

// Usage with exception handling
int main() {
    try {
        CSVProcessor processor("data.csv");
        auto data = processor.readCSV();

        // Process data
        for (const auto& row : data) {
            for (const auto& field : row) {
                cout << field << " | ";
            }
            cout << endl;
        }
    }
    catch (const runtime_error& e) {
        cerr << "Error: " << e.what() << endl;
        return 1;
    }
    catch (const exception& e) {
        cerr << "Unexpected error: " << e.what() << endl;
        return 2;
    }

    return 0;
}

Practice Exercise: Log File Analyzer

🏋️ Build a Log File Analyzer

Create a program that analyzes server log files with these features:

  1. Parse log entries with timestamp, level, and message
  2. Filter by date range and severity level
  3. Generate statistics (errors per hour, etc.)
  4. Handle corrupted log entries gracefully
  5. Export results to a report file
struct LogEntry {
    time_t timestamp;
    string level;  // INFO, WARNING, ERROR
    string message;
};

class LogAnalyzer {
private:
    vector<LogEntry> entries;

public:
    void loadLogFile(const string& filename);
    void filterByDateRange(time_t start, time_t end);
    void filterByLevel(const string& level);

    map<string, int> getLevelCounts();
    map<int, int> getErrorsByHour();

    void exportReport(const string& filename);

private:
    LogEntry parseLogLine(const string& line);
    // TODO: Implement parsing with exception handling
};
💡 Hint
  • Use regex for parsing log format
  • Create custom exceptions for parse errors
  • Use RAII for file handling
  • Implement transaction-like behavior for report generation
  • Consider using memory-mapped files for large logs

Advanced File I/O Techniques

graph TD A["Advanced File I/O"] --> B[Memory Mapping] A --> C[Buffering Control] A --> D[File Locking] A --> E["Async I/O"] B --> F["mmap on Unix<br/>MapViewOfFile on Windows"] C --> G["setvbuf()<br/>Custom buffers"] D --> H["Platform specific<br/>flock/LockFile"] E --> I["C++20 coroutines<br/>Platform APIs"]

Performance Optimization

// Custom buffering for better performance
class BufferedFileWriter {
private:
    ofstream file;
    vector<char> buffer;
    size_t bufferSize;
    size_t currentPos;

public:
    BufferedFileWriter(const string& filename, size_t bufSize = 65536)
        : bufferSize(bufSize), currentPos(0) {
        buffer.resize(bufferSize);
        file.open(filename, ios::binary);
        if (!file) {
            throw runtime_error("Cannot open file for writing");
        }
    }

    ~BufferedFileWriter() {
        flush();
    }

    void write(const string& data) {
        for (char c : data) {
            buffer[currentPos++] = c;
            if (currentPos >= bufferSize) {
                flush();
            }
        }
    }

    void flush() {
        if (currentPos > 0) {
            file.write(buffer.data(), currentPos);
            currentPos = 0;
        }
    }
};

// Memory-mapped file reading (platform specific)
#ifdef _WIN32
    // Windows implementation
#else
    // Unix implementation using mmap
#endif

Exception Safety Guarantees

Exception Safety Levels Basic Guarantee No resources leaked, objects in valid state Strong Guarantee Operation succeeds or no changes (commit or rollback) No-throw Guarantee Operation never throws (destructors, swap)

Challenge Exercise: Database File Manager

🏋️ Advanced File I/O Challenge

Build a simple file-based database with:

  1. Binary file format with index
  2. Transaction support (commit/rollback)
  3. Concurrent access handling
  4. Crash recovery
  5. Comprehensive error handling
💡 Hint
  • Use write-ahead logging for transactions
  • Implement file locking for concurrent access
  • Create backup files before modifications
  • Use checksums for data integrity
  • Design custom exception hierarchy

🎯 Quick Quiz

Question 1: In the lesson's multiple catch-block example, why must catch (const exception& e) come after the more specific catch (const runtime_error& e)?

Question 2: Which line from the lesson correctly opens a binary file for both reading and writing?

Question 3: In the SafeFile RAII wrapper from the lesson, what guarantees that the underlying FILE* handle is closed even if an exception is thrown while the object is in scope?

Summary

🎉 Key Takeaways

  • File streams (ifstream, ofstream, fstream) handle file I/O like cin/cout
  • Always check if file operations succeed
  • RAII ensures files are closed even if exceptions occur
  • Binary mode for non-text data to avoid formatting issues
  • Exceptions provide structured error handling
  • Catch specific exceptions before general ones
  • Use standard exceptions when possible
  • Exception safety is crucial for robust programs
  • Buffer wisely for performance with large files
graph LR A["Master File I/O"] --> B[Handle Errors Gracefully] B --> C[Build Robust Applications] C --> D[Process Data Reliably] D --> E[Professional C++ Developer!]

📚 Additional Resources

🚀 What's Next?

You can now read, write, and safely process files while handling errors robustly instead of letting your program crash. Next, in Lesson 18: Move Semantics and Rvalue References, you'll learn how modern C++ transfers resources instead of copying them — the key to passing large objects like file buffers around efficiently.

🎉 File I/O and exceptions mastered!

You can now build programs that survive bad input, missing files, and unexpected errors without crashing or leaking resources. That resilience is what separates a toy program from production-ready code.