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.
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.
std::ios_base & std::ioseofbit, failbit, badbit), and links to internal std::streambuf.std::istream & std::ostream>>) and insertion (<<) operators. Combined into std::iostream via multiple inheritance.<fstream>std::ifstream, std::ofstream, and std::fstream tie the stream buffers directly to OS disk file descriptors.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. |
#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;
}
The operating system distinguishes between human-readable text representations and raw binary dumps:
12.34 takes 5 bytes of ASCII).\n is converted to Carriage Return + Line Feed (\r\n).read() and write().int strictly takes 4 bytes; a double takes 8 bytes.#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;
}
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:
tellg(): Returns current byte offset of read cursor (type std::streampos).seekg(offset, direction): Repositions the read cursor.tellp(): Returns current byte offset of write cursor (type std::streampos).seekp(offset, direction): Repositions the write cursor.file.seekg(16, std::ios::beg); (Reposition to Record 3)
| 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! |
#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;
}
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.
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.
write()?
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.
seekg() and tellg()?
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.
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.
eofbit, failbit, badbit), and how are they checked?
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).
file.clear() be called before seeking if a stream has encountered EOF?
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.
file.close() largely optional in modern C++ code blocks?
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.