Files, Streams, Exceptions, and Resource ManagementIntroductionMost useful programs must communicate with resources outside their immediate memory: files, terminals, networks, databases, devices, and operating-system services. These resources are slower, failure-prone, and governed by rules different from ordinary variables. A program that reads a file must handle missing paths, permissions, malformed data, partial reads, and eventual closure. A program that acquires memory, a lock, or a socket must release it reliably, including when an error occurs. C and C++ provide complementary mechanisms for this work. C exposes file operations through the FILE* interface and error-status functions. C++ provides type-safe stream classes, exceptions, assertions, and Resource Acquisition Is Initialization (RAII). Modern C++ further improves safety through smart pointers, standard containers, and deterministic destruction. This chapter develops the principles and practices required to implement reliable text and binary file processing, stream-based input/output, serialization fundamentals, exception handling, assertions, and deterministic resource management. Learning ObjectivesAfter studying this chapter, you should be able to: - distinguish text files, binary files, streams, and serialization;
- open, read, write, append, flush, and close files safely;
- use C and C++ file interfaces appropriately;
- detect and recover from stream errors;
- design basic binary serialization while considering portability;
- distinguish exceptions, assertions, return codes, and error values;
- explain RAII and deterministic cleanup;
- use
std::unique_ptr, std::shared_ptr, and std::weak_ptr appropriately; - identify common resource-management defects and prevent them;
- apply these ideas in a realistic file-processing application.
1. Files and StreamsA file is persistent data identified by a path and managed by an operating system or storage system. A stream is an abstraction representing a sequence of data transferred between a program and a source or destination. A stream may represent a disk file, keyboard, terminal, memory buffer, or network connection. The stream model separates the operation from the physical device. A program can read characters from standard input or a file using similar concepts. Input generally moves toward the program; output moves away from it. Important stream states include: - good: the previous operation succeeded;
- end-of-file: no more input is available;
- fail: an operation could not interpret or transfer data as requested;
- bad: a serious I/O failure occurred.
End-of-file is not normally a character stored in the file. It is a condition reported after an attempted read goes beyond available input. Text and Binary RepresentationsA text file stores characters according to an encoding or text convention. Examples include UTF-8 source files, CSV files, and configuration files. Text is relatively easy to inspect, edit, and exchange, but it may require parsing and may represent numbers with more bytes than a binary format. A binary file stores bytes according to an application-defined layout. Binary data can be compact and fast to process, but it is not automatically portable or self-describing. A binary representation may depend on integer size, byte order, padding, floating-point representation, compiler, or structure alignment. The choice is an engineering decision: | Requirement | Usually suitable choice |
|---|
| Human inspection and editing | Text | | Interoperability and long-term readability | Defined text format | | Compact local storage | Binary | | High-speed transfer between identical systems | Binary, with documented layout | | Untrusted or cross-platform exchange | Defined, validated format |
2. C File ProcessingThe C standard library represents an open file with FILE*. The essential functions are declared in <stdio.h>. #include <stdio.h>
int main(void) {
FILE *file = fopen("report.txt", "w");
if (file == NULL) {
perror("report.txt");
return 1;
}
if (fprintf(file, "Total: %d\n", 42) < 0) {
perror("write");
fclose(file);
return 1;
}
if (fclose(file) != 0) {
perror("close");
return 1;
}
return 0;
}Common modes include: "r": read an existing text file;"w": write, truncating an existing file or creating a new one;"a": append, creating the file if necessary;"rb", "wb", and "ab": corresponding binary modes;"r+", "w+", and "a+": reading and writing combinations.
The mode "w" is potentially destructive because it removes previous contents. A program should use it only when replacement is intended. Useful C functions include fgetc, fputc, fgets, fputs, fread, fwrite, fprintf, fscanf, fseek, ftell, rewind, feof, ferror, and fflush. A reliable input loop tests the operation itself, not feof in advance: char line[256];
while (fgets(line, sizeof line, file) != NULL) {
fputs(line, stdout);
}
if (ferror(file)) {
perror("read");
}The expression while (!feof(file)) is incorrect as a general reading pattern because end-of-file becomes true only after a read attempt fails. C Binary I/Ofread and fwrite transfer raw bytes. Their return value is the number of objects successfully transferred, not necessarily the number requested.
struct Record {
int id;
double score;
};
struct Record r = {7, 91.5};
FILE *file = fopen("records.bin", "wb");
if (file != NULL) {
size_t count = fwrite(&r, sizeof r, 1, file);
if (count != 1) {
perror("binary write");
}
fclose(file);
}Writing a structure directly is safe only under restricted assumptions. Padding bytes, integer widths, byte order, floating-point representation, and compiler choices can vary. It is unsuitable as a portable interchange format without a documented representation. 3. C++ StreamsC++ stream classes are declared mainly in <iostream>, <fstream>, and <sstream>. std::cin, std::cout, std::cerr, and std::clog support standard input and output;std::ifstream reads files;std::ofstream writes files;std::fstream supports both directions;std::istringstream and std::ostringstream process strings as streams.
A basic text-writing example is: #include <fstream>
#include <stdexcept>
void write_report(const std::string& path) {
std::ofstream out(path);
if (!out) {
throw std::runtime_error("cannot open output file: " + path);
}
out << "Revenue,Units\n";
out << "1250.50,18\n";
if (!out) {
throw std::runtime_error("write failure");
}
}The insertion operator << formats values for output. The extraction operator >> parses formatted input and normally skips leading whitespace. std::getline reads an entire line, including spaces except for the line-ending delimiter. A common mixed-input issue occurs after operator>>: it leaves the newline in the input buffer. Calling getline immediately may therefore read an empty line. One solution is: int age;
std::string name;
std::cin >> age;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::getline(std::cin, name); For robust parsing, read a complete line and parse it with std::istringstream. This keeps malformed input localized and makes validation clearer. Stream Error HandlingC++ provides state tests such as good(), eof(), fail(), and bad(). Streams also have exception masks: std::ifstream in("data.txt");
in.exceptions(std::ios::failbit | std::ios::badbit);With this configuration, operations setting the selected state throw std::ios_base::failure. Exception-based stream handling is useful when an I/O failure makes the current operation impossible. For ordinary user input, explicit state testing may be more convenient because invalid input can be corrected. The open function accepts modes such as std::ios::in, out, app, trunc, binary, and ate. Modes can be combined with the bitwise OR operator: std::ifstream in("image.dat", std::ios::binary);C++ streams close automatically when their objects are destroyed. Explicit close() can still be useful when a program must release a file before the end of a scope or must check a close-related failure at a particular point. 4. Text Processing and ValidationText input should be treated as untrusted data, even when it comes from an internal file. A sound pipeline is: - acquire a line or token;
- parse it into candidate values;
- validate syntax and semantic constraints;
- process only validated values;
- report the location and nature of failures.
For a CSV-like record, validation may include the correct number of fields, a valid integer range, a nonnegative quantity, and a recognized date format. Parsing and validation are different: a string may be syntactically numeric but semantically invalid. Example: #include <charconv>
#include <string_view>
int parse_quantity(std::string_view text) {
int value{};
const char* first = text.data();
const char* last = first + text.size();
auto result = std::from_chars(first, last, value);
if (result.ec != std::errc{} || result.ptr != last || value < 0) {
throw std::invalid_argument("invalid quantity");
}
return value;
}A production parser should also define how it treats empty fields, whitespace, quotation marks, delimiters inside quoted fields, and encoding errors. Splitting a CSV line merely at commas is insufficient for fully conforming CSV data. 5. Serialization FundamentalsSerialization converts an object or data structure into a representation suitable for storage or transmission. Deserialization reconstructs usable data from that representation. A good format defines: - field order and meaning;
- integer sizes and signedness;
- byte order for multi-byte values;
- text encoding;
- length representation;
- version information;
- validation rules;
- behavior for unknown or missing fields.
Text serialization might produce JSON, XML, CSV, or a custom line format. Binary serialization may use fixed-width fields, length-prefixed strings, checksums, and explicit version numbers. A length-prefixed binary string can be conceptualized as: [4-byte unsigned length][that many UTF-8 bytes] The reader must impose a maximum acceptable length before allocating memory. Otherwise, a malicious length field could cause excessive allocation. Serialization formats crossing trust boundaries require strict bounds checking, integer-overflow checks, and rejection of trailing or inconsistent data where appropriate. Do not confuse serialization with copying memory. Pointer values are addresses meaningful only within a particular process and execution. Serializing a pointer value does not serialize the object to which it points. Instead, serialize the pointed-to data or a stable identifier. 6. Exceptions and Error PoliciesAn exception is an object transferred from the point of failure to a handler capable of responding. In C++, throwing is written with throw, and handling uses try and catch. try {
write_report("report.txt");
} catch (const std::ios_base::failure& e) {
std::cerr << "I/O error: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Application error: " << e.what() << '\n';
}Catch exceptions by const reference to avoid unnecessary copying and preserve polymorphism. Catch more specific types before general types. std::exception is a useful common base for many standard exceptions. Exceptions are appropriate when: - the operation cannot meet its contract;
- the failure is exceptional relative to normal control flow;
- the caller can recover, translate, or terminate safely;
- cleanup must occur while control unwinds.
Return values or status types may be preferable for frequent expected outcomes, such as “user entered invalid input.” Modern C++ can also use std::optional for absence and std::expected where supported or provided by a library for a value-or-error result. An exception should communicate failure, not replace ordinary branching. Avoid throwing from destructors, because a destructor called during stack unwinding can cause program termination if another exception is already active. Exception Safety GuaranteesThe principal exception-safety guarantees are: - No-throw guarantee: the operation does not throw;
- Strong guarantee: if an exception occurs, program state is unchanged;
- Basic guarantee: invariants remain valid and resources are not leaked, although state may change;
- No guarantee: the operation may leak or corrupt state.
Standard containers and RAII make the basic and strong guarantees practical. For example, constructing a replacement object before modifying a managed object can implement a commit-style strong guarantee. 7. Assertions and Contract ThinkingAn assertion expresses a condition that the programmer believes must hold at a particular program point. #include <cassert>
int divide(int numerator, int denominator) {
assert(denominator != 0);
return numerator / denominator;
}Assertions are suitable for programmer errors and internal invariants, not for validating ordinary external input. They may be disabled by defining NDEBUG, so an assertion must not be the only security or correctness check for user-controlled data. A useful distinction is: - precondition: what a caller must provide;
- postcondition: what a successful function promises;
- invariant: what remains true throughout an abstraction's valid lifetime.
For public interfaces, validate preconditions that can be violated by external callers. Use assertions to detect violated internal assumptions during development and testing. 8. RAII and Deterministic Resource ManagementRAII means that resource acquisition is tied to object construction and resource release is tied to destruction. C++ destructors run when objects leave scope, including during exception unwinding. void process() {
std::ifstream input("orders.txt");
if (!input) {
throw std::runtime_error("orders cannot be opened");
}
// The file is closed automatically at function exit or during unwinding.
}RAII applies to files, dynamic memory, mutex locks, transactions, temporary directories, and operating-system handles. It makes ownership visible in types and reduces the number of cleanup paths a programmer must manage manually. A resource-owning class should generally follow the Rule of Zero: use member objects that already manage resources, avoiding custom copy and destruction operations. If a class directly owns a resource, the Rule of Five may require deliberate definitions or deletions of destructor, copy constructor, copy assignment, move constructor, and move assignment. 9. Smart PointersSmart pointers express ownership of dynamically allocated objects. std::unique_ptr
std::unique_ptr<T> has exclusive ownership. It cannot be copied but can be moved. It is the default smart pointer for a single owner.
#include <memory>
auto customer = std::make_unique<Customer>(42);
// customer is destroyed automatically at scope exit. Use std::unique_ptr when an object has one clear owner or when ownership is transferred between components. std::shared_ptr
std::shared_ptr<T> uses shared ownership through a reference-counted control block. The object is destroyed when the final owning shared_ptr is destroyed or reset.
auto a = std::make_shared<Node>();
std::shared_ptr<Node> b = a; Shared ownership has overhead and can obscure lifetime reasoning. It should not be used merely because ownership is inconvenient to design. std::weak_ptr
std::weak_ptr<T> observes an object managed by shared_ptr without extending its lifetime. It is useful for caches, parent links, and breaking ownership cycles.
if (auto locked = weak_node.lock()) {
locked->visit();
}A cycle of shared_ptr objects never reaches a zero reference count. Replacing at least one ownership edge with weak_ptr prevents this leak. Raw pointers and references remain useful as non-owning access paths. A raw pointer should not silently imply ownership. Prefer a clear convention or a type that communicates borrowing and lifetime expectations. 10. Worked Application: Transaction Log ImporterBackgroundConsider an illustrative university finance application that imports a text file containing transaction records: 1001,STUDENT_FEE,1250
1002,LIBRARY_FINE,50
1003,STUDENT_FEE,-10 The third record has an invalid negative amount. ProblemThe importer must open the file, parse each line, validate identifiers and amounts, retain valid records, and report line-specific errors without leaking resources or silently accepting corrupt data. Approach#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <stdexcept>
struct Transaction {
int id;
std::string category;
long long amount_cents;
};
std::vector<Transaction> import_transactions(const std::string& path) {
std::ifstream in(path);
if (!in) {
throw std::runtime_error("cannot open: " + path);
}
std::vector<Transaction> result;
std::string line;
std::size_t line_number = 0;
while (std::getline(in, line)) {
++line_number;
std::istringstream parser(line);
std::string id_text, category, amount_text;
if (!std::getline(parser, id_text, ',') ||
!std::getline(parser, category, ',') ||
!std::getline(parser, amount_text)) {
throw std::runtime_error("malformed record at line " +
std::to_string(line_number));
}
int id{};
long long amount{};
try {
id = std::stoi(id_text);
amount = std::stoll(amount_text);
} catch (const std::exception&) {
throw std::runtime_error("non-numeric field at line " +
std::to_string(line_number));
}
if (id <= 0 || category.empty() || amount < 0) {
throw std::runtime_error("invalid values at line " +
std::to_string(line_number));
}
result.push_back(Transaction{id, category, amount});
}
if (in.bad()) {
throw std::runtime_error("read failure");
}
return result;
}AnalysisThe file object owns the open stream and closes it automatically. Parsing is isolated through istringstream. Validation rejects the negative amount. The vector owns its elements and cleans them up automatically. If an exception occurs, already-created objects are destroyed deterministically. A production system might choose a tolerant policy: accept valid records, collect all errors, and produce a rejected-record report. That policy should be explicit because silently skipping bad records would compromise financial accuracy. Learning Points- Resource lifetime should follow scope.
- Syntax and semantic validation are separate responsibilities.
- Errors should identify the failing record.
- A file format requires documented assumptions.
- Recovery policy depends on business risk; financial imports generally require auditability.
11. Limitations, Security, and Professional PracticeFile paths can be dangerous when constructed from untrusted input. Path traversal, symbolic links, unauthorized overwrites, and race conditions must be considered. Applications should use least-privilege permissions, validate paths, avoid predictable temporary filenames, and define whether existing files may be replaced. Never trust a file extension as proof of content. Validate structure and, where relevant, encoding and signatures. Binary length fields must be bounded before allocation. Deserializers should avoid invoking arbitrary code or constructing unexpected types from untrusted data. For important data, use atomic-update strategies: write a temporary file in the same directory, flush as required by the application’s durability policy, close successfully, and rename it into place. Exact durability guarantees depend on the operating system and storage device; closing a stream is not universally equivalent to guaranteeing physical persistence. Logging should avoid recording passwords, access tokens, or unnecessary personal information. Error messages should help operators without exposing sensitive paths or data. Deterministic cleanup is both a reliability practice and a security practice because unreleased locks, descriptors, and temporary files can enable denial of service. Contemporary C++ development favors standard containers, RAII wrappers, make_unique, make_shared when shared ownership is justified, filesystem facilities from <filesystem>, and explicit serialization libraries or documented formats. Automated testing should include empty files, very large lines, malformed encodings, permission failures, truncated binary records, duplicate identifiers, and simulated exceptions. Chapter SummaryFiles provide persistent storage, while streams provide a uniform abstraction for sequential data transfer. Text formats are readable and interoperable when carefully specified; binary formats can be compact but require explicit layout and portability rules. C uses FILE* and status-returning functions, whereas C++ streams integrate formatting, state flags, and exception options. Reliable input tests the result of each operation, validates both syntax and meaning, and reports failures precisely. Serialization is a defined conversion between data and a storage representation; it is not equivalent to copying object memory or pointer values. Exceptions transfer failures to suitable handlers, while assertions detect violated internal assumptions. RAII binds resource lifetime to object lifetime and supports deterministic cleanup during normal execution and exception unwinding. Smart pointers communicate ownership: unique_ptr for exclusive ownership, shared_ptr for justified shared ownership, and weak_ptr for non-owning observation or cycle prevention. Professional implementations also address security, portability, durability, privacy, testing, and recovery policy. Correct resource management is therefore not merely a coding detail; it is part of dependable system design. Key TermsFile: Persistent named data managed by a storage system. Stream: An abstraction for sequential input or output. Text file: A file whose contents represent characters in a defined encoding. Binary file: A file whose bytes follow an application-defined representation. Serialization: Conversion of data into a storable or transferable representation. Deserialization: Reconstruction of data from a serialized representation. RAII: Resource Acquisition Is Initialization; ownership is tied to object lifetime. Exception: An object transferred to signal an abnormal condition. Assertion: A checked expression representing a programmer assumption or invariant. EOF: The end-of-file condition reported by an input operation. Stream state: Flags describing success, end-of-file, failure, or serious I/O failure. Buffer: Temporary memory used to hold data during transfer. Ownership: Responsibility for controlling a resource’s lifetime. unique_ptr: A smart pointer with exclusive ownership.
shared_ptr: A reference-counted smart pointer supporting shared ownership.
weak_ptr: A non-owning observer of an object managed by shared_ptr.
Exception safety: Guarantees describing program state and resource behavior during failure. Byte order: The ordering of bytes used to represent multi-byte values. Invariant: A condition that remains true for a valid abstraction. Review QuestionsShort Answer Questions- Why is
while (!feof(file)) an unsafe general pattern for reading a C file? - Distinguish a text file from a binary file.
- What problem does RAII solve?
- When should
std::weak_ptr be used? - Why should assertions not replace validation of user input?
Long Answer Questions- Explain C++ stream states and compare explicit state testing with exception-based stream handling.
- Describe the design requirements of a portable binary serialization format.
- Explain the no-throw, strong, and basic exception-safety guarantees.
- Compare
unique_ptr, shared_ptr, and weak_ptr, including their ownership implications. - Discuss how text parsing, validation, error reporting, and resource management should be combined in a file-import program.
Analytical / Application Questions- Design an import policy for a financial transaction file that contains both valid and invalid records. Explain whether processing should stop at the first error or continue, and justify your decision.
- A program writes a C++ structure directly to a binary file and later reads it on a different compiler and processor. Analyze at least four portability risks and propose a safer format.
- An object graph contains parent and child objects connected entirely by
shared_ptr. Explain why a memory leak may occur and redesign the ownership relationships using weak_ptr where appropriate.
Multiple Choice Questions with AnswersQuestion 1Which loop correctly reads lines from a C file? A. while (!feof(file)) { fgets(line, size, file); } B. while (fgets(line, size, file) != NULL) { process(line); } C. while (ferror(file)) { fgets(line, size, file); } D. while (file != NULL) { process(file); } Correct Answer: B. while (fgets(line, size, file) != NULL) { process(line); } Question 2What does opening a file with mode "w" generally do? A. Reads only and preserves all contents B. Appends new data without changing existing contents C. Writes and may truncate an existing file D. Opens only binary files Correct Answer: C. Writes and may truncate an existing file Question 3Which smart pointer expresses exclusive ownership? A. std::weak_ptr B. std::shared_ptr C. std::auto_ptr D. std::unique_ptr Correct Answer: D. std::unique_ptr Question 4Why is directly serializing a pointer value generally incorrect? A. Pointers cannot be stored in memory B. The address is meaningful only within a particular process context C. Pointers always contain text D. Serialization automatically deletes pointers Correct Answer: B. The address is meaningful only within a particular process context Question 5What is the primary purpose of an assertion? A. To validate all untrusted user input B. To replace exception handling C. To detect violated programmer assumptions or invariants D. To guarantee a function never fails Correct Answer: C. To detect violated programmer assumptions or invariants Question 6What happens when a C++ object owning an fstream leaves scope? A. The stream is normally closed by its destructor B. The file is converted to binary C. The object is copied to global memory D. The operating system permanently locks the file Correct Answer: A. The stream is normally closed by its destructor Question 7Which exception-safety guarantee ensures that state is unchanged if an operation fails? A. Basic guarantee B. Strong guarantee C. Partial guarantee D. Input guarantee Correct Answer: B. Strong guarantee Question 8What is a principal risk of a shared_ptr ownership cycle? A. The objects may never be destroyed B. The compiler changes all objects to stack objects C. The pointers become unique automatically D. The file system is corrupted Correct Answer: A. The objects may never be destroyed Question 9Which operation is most appropriate for reading a complete line including spaces? A. std::getline B. std::hex C. std::flush D. std::move Correct Answer: A. std::getline Question 10Why should a binary deserializer limit a length field before allocating memory? A. To force all strings to be empty B. To prevent excessive or malicious allocation C. To convert binary data to source code D. To disable exception handling Correct Answer: B. To prevent excessive or malicious allocation
|