⚙️ Unit 13 • C++ Systems Engineering

File Handling & Persistent Stream Architecture

Low-level persistent storage and stream abstraction in C++: the <fstream> inheritance hierarchy, stream buffer mechanics, text vs binary serialization with reinterpret_cast, file mode bitmasks, append records, random-access positioning with seekg/seekp, and file pointer telemetry via tellg/tellp.

🌊 Stream Hierarchy: ios_base → fstream
💾 Binary I/O: Raw read() / write() Blocks
🎯 Random Access: seekg(), seekp() Offsets
📍 Telemetry: tellg() & tellp() Byte Counters
01

Introduction to Streams & The Stream Class Hierarchy

In C++, input and output operations are abstracted through the concept of a Stream—an ordered sequence of bytes flowing between a source (producer) and a destination (consumer). Stream classes decouple program logic from physical peripheral hardware (such as hard disks, SSDs, terminals, or network sockets).

std::ifstream

Input File Stream: Derived from std::istream. Specializes in sequential or random-access reading of data from external files into memory.

std::ofstream

Output File Stream: Derived from std::ostream. Specializes in creating, overwriting, and appending data from memory to persistent files.

std::fstream

Bidirectional File Stream: Derived from std::iostream. Combines both reading and writing capabilities over a single file descriptor.

1
Root Foundation: std::ios_base & std::ios
Manages formatting state flags, stream status flags (eofbit, failbit, badbit), and links to internal std::streambuf.
2
Intermediate Streams: std::istream & std::ostream
Provides formatted extraction (>>) and insertion (<<) operators. Combined into std::iostream via multiple inheritance.
3
File Specializations: <fstream>
std::ifstream, std::ofstream, and std::fstream tie the stream buffers directly to OS disk file descriptors.
02

Opening, Closing & File Mode Bitmasks

Opening a file associates an active C++ stream object with a physical disk path. In modern C++, file stream destructors follow the RAII idiom—automatically flushing buffers and closing file descriptors upon leaving scope.

File Mode Flag Meaning & Operational Behavior
std::ios::in Opens file for reading (Default for ifstream; file must exist).
std::ios::out Opens file for writing (Default for ofstream; truncates existing content unless specified).
std::ios::app Append Mode: All write operations seek to the end of file before each write.
std::ios::trunc Truncate Mode: Discards existing file contents, reducing size to 0 bytes.
std::ios::binary Binary Mode: Disables newline translations (bypasses Windows \r\n ↔ \n conversion).
std::ios::ate At-the-End: Opens file and seeks directly to the end of file immediately after open.
file_modes_and_text_io.cpp
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::string filename = "telemetry_log.txt";

    // 1. Writing Text Records (Create / Overwrite)
    {
        std::ofstream outFile(filename, std::ios::out | std::ios::trunc);
        if (!outFile.is_open()) {
            std::cerr << "Error opening file for write!\n";
            return 1;
        }
        outFile << "RECORD_001: 24.5C 1013hPa\n";
        outFile << "RECORD_002: 25.1C 1012hPa\n";
    } // RAII: outFile automatically closes here!

    // 2. Appending Records to Existing File
    {
        std::ofstream appendFile(filename, std::ios::out | std::ios::app);
        if (appendFile.is_open()) {
            appendFile << "RECORD_003: 26.0C 1011hPa [APPENDED]\n";
        }
    } // Closed automatically

    // 3. Reading File Line-by-Line
    {
        std::ifstream inFile(filename, std::ios::in);
        if (!inFile.is_open()) {
            std::cerr << "Error opening file for read!\n";
            return 1;
        }

        std::string line;
        std::cout << "--- Telemetry Log Contents ---\n";
        while (std::getline(inFile, line)) {
            std::cout << line << "\n";
        }
    }

    return 0;
}
03

Text Files vs Binary Files: Bitwise Struct Serialization

The operating system distinguishes between human-readable text representations and raw binary dumps:

Text Files (Formatted ASCII/UTF-8)
  • Numbers are converted to character sequences (e.g., float 12.34 takes 5 bytes of ASCII).
  • Newline transformation active: on Windows, \n is converted to Carriage Return + Line Feed (\r\n).
  • Slow formatting overhead with human-readable files.
Binary Files (Raw Memory Dumps)
  • Data is written as an exact bitwise mirror of RAM using read() and write().
  • An int strictly takes 4 bytes; a double takes 8 bytes.
  • Zero serialization/formatting CPU overhead; maximum throughput.
binary_struct_serialization.cpp
#include <iostream>
#include <fstream>

struct DeviceConfig {
    int deviceId;
    double samplingRate;
    char firmwareTag[8];
};

int main() {
    DeviceConfig outgoing{101, 1000.0, "v2.4.1"};

    // 1. Binary Serialization via write() and reinterpret_cast
    std::ofstream binOut("config.bin", std::ios::out | std::ios::binary);
    if (binOut.is_open()) {
        binOut.write(reinterpret_cast<const char*>(&outgoing), sizeof(DeviceConfig));
        binOut.close();
        std::cout << "[Binary Write] Dumped " << sizeof(DeviceConfig) << " raw bytes to disk.\n";
    }

    // 2. Binary Deserialization via read()
    DeviceConfig incoming{};
    std::ifstream binIn("config.bin", std::ios::in | std::ios::binary);
    if (binIn.is_open()) {
        binIn.read(reinterpret_cast<char*>(&incoming), sizeof(DeviceConfig));
        binIn.close();

        std::cout << "[Binary Read] Loaded ID: " << incoming.deviceId 
                  << " | Rate: " << incoming.samplingRate 
                  << " | Tag: " << incoming.firmwareTag << "\n";
    }

    return 0;
}
04

Random-Access Files: seekg(), seekp(), tellg() & tellp()

In random-access files, the program can instantly reposition its read and write cursors without scanning intervening data. C++ maintains two independent file pointers inside stream objects:

Get Pointer (Input / Read Cursor)
  • tellg(): Returns current byte offset of read cursor (type std::streampos).
  • seekg(offset, direction): Repositions the read cursor.
Put Pointer (Output / Write Cursor)
  • tellp(): Returns current byte offset of write cursor (type std::streampos).
  • seekp(offset, direction): Repositions the write cursor.
File Pointer Positioning Mechanism:
Seeking: file.seekg(16, std::ios::beg); (Reposition to Record 3)
Bytes 0-7
Record 1
Bytes 8-15
Record 2
Bytes 16-23
→ seekg() HERE ←
Bytes 24-31
Record 4
Bytes 32-39
EOF
Seek Direction Flag Reference Point Description Example Syntax
std::ios::beg Beginning of file (Offset $\ge 0$) file.seekg(100, std::ios::beg);
std::ios::cur Current position (Offset can be positive or negative) file.seekg(-20, std::ios::cur);
std::ios::end End of file (Offset usually $\le 0$) file.seekg(0, std::ios::end); // Find file size!
random_access_records.cpp
#include <iostream>
#include <fstream>

struct AccountRecord {
    int id;
    double balance;
};

int main() {
    std::string filename = "accounts.dat";

    // 1. Populate fixed-size binary records
    std::ofstream out(filename, std::ios::out | std::ios::binary | std::ios::trunc);
    AccountRecord records[3] = {{101, 500.0}, {102, 1200.0}, {103, 350.0}};
    out.write(reinterpret_cast<const char*>(records), sizeof(records));
    out.close();

    // 2. Random-Access Read: Instantly fetch Record #2 without reading Record #1!
    std::fstream file(filename, std::ios::in | std::ios::out | std::ios::binary);
    
    // Calculate byte offset of record index 1 (the 2nd record)
    std::streampos offset = 1 * sizeof(AccountRecord);
    file.seekg(offset, std::ios::beg);

    AccountRecord target{};
    file.read(reinterpret_cast<char*>(&target), sizeof(AccountRecord));
    std::cout << "Direct Read Record 2 -> ID: " << target.id << " Balance: $" << target.balance << "\n";

    // 3. In-Place Random-Access Mutation: Update Record #2 balance
    target.balance += 300.0; // New balance = 1500.0
    file.seekp(offset, std::ios::beg); // Move write cursor back to Record #2
    file.write(reinterpret_cast<const char*>(&target), sizeof(AccountRecord));

    // 4. Determining Total File Size using seekg and tellg
    file.seekg(0, std::ios::end);
    std::streampos totalBytes = file.tellg();
    std::cout << "Total File Size: " << totalBytes << " bytes (" 
              << totalBytes / sizeof(AccountRecord) << " records)\n";

    file.close();
    return 0;
}
05

Viva Voce & Engineering Interview Questions

Q1 • Architecture What is the fundamental difference between the "Get" pointer and the "Put" pointer in C++ streams?
The Get pointer (seekg / tellg) controls the input read cursor, marking the memory location from which the next byte will be read into the application. The Put pointer (seekp / tellp) controls the output write cursor, designating the exact byte coordinate where the next insertion will occur on physical disk. In bidirectional streams (fstream), both pointers can be repositioned independently.
Q2 • Performance Why is binary file serialization significantly faster than formatted text file output?
Formatted text output (outFile << num) requires the CPU to parse data values into string representations, convert numbers digit-by-digit into ASCII character codes, and perform newline translation. Binary serialization (outFile.write()) executes a direct bitwise memory-to-disk DMA (Direct Memory Access) copy without parsing or formatting transformations, operating at maximum disk throughput.
Q3 • Safety & Invariants Why must raw pointers and classes containing dynamic heap allocations NEVER be serialized directly with write()?
Serializing an object containing raw pointers (e.g., char* or std::vector) dumps the raw 64-bit virtual memory address to the disk file instead of the underlying data buffer. When the file is read back into memory in a later session (or another process), that old pointer address is completely invalid, resulting in immediate segmentation faults and memory corruption when dereferenced.
Q4 • Stream Mechanics How do you determine the exact byte size of a file using seekg() and tellg()?
You reposition the get pointer to the very end of the file using file.seekg(0, std::ios::end); and then query the cursor coordinate using std::streampos size = file.tellg();. Finally, you restore the pointer to the beginning (file.seekg(0, std::ios::beg);) if subsequent reading is required.
Q5 • File Modes What is the operational difference between std::ios::app and std::ios::ate?
std::ios::ate ("at the end") opens the file and immediately positions the read/write cursor at the end, but allows the program to freely seek anywhere else in the file for subsequent operations. In contrast, std::ios::app ("append") forces every write operation to append to the end of file, regardless of any preceding seekp() repositioning calls.
Q6 • Error Handling What are stream state bits (eofbit, failbit, badbit), and how are they checked?
They are internal health status flags in std::ios_base: eofbit indicates the file pointer reached the end of file; failbit signals a logical formatting error (e.g., trying to read an integer from non-numeric text); and badbit denotes an unrecoverable low-level hardware I/O failure. They can be checked using file.eof(), file.fail(), and file.bad(), or tested directly using boolean evaluation if (!file).
Q7 • Stream Clearing Why must file.clear() be called before seeking if a stream has encountered EOF?
Once an input stream reaches the end of file, its internal eofbit and failbit flags are set. In this error state, all subsequent stream operations—including seekg() and read()—are silently rejected and ignored by the C++ runtime. Calling file.clear() resets the health flags, restoring the stream to a valid state so seeking can succeed.
Q8 • Modern C++ Why is explicit file.close() largely optional in modern C++ code blocks?
Under the RAII (Resource Acquisition Is Initialization) idiom, the destructors of std::ifstream, std::ofstream, and std::fstream automatically flush uncommitted buffers and close their associated OS file descriptors when the stream object goes out of scope. Explicit .close() is only necessary if you must release the file handle early or test for write failure during closing.