-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
186: Building a Library Management System with File Persistence
When we first start thinking about "persistence" in C++, the instinct is usually to just dump our data structures into a text file as quickly as possible. You've got a std::vector<Book>, you've got a std::ofstream, and you just want the data to be there when you restart the program. It feels straightforward until you actually try to read that data back into your objects.
The Trap of Simple Stream Insertion
The naive approach I see most often is using the << and >> operators directly on a file stream in a loop. You might write a Book object like this: file << book.isbn << " " << book.title << " " << book.author << "\n";. On the surface, it works. If your book is "The Hobbit" by "Tolkien", it looks clean in the text file. But here is where it breaks: the moment a user enters a book title with a space in it, your std::cin >> title (or file >> title) will only grab the first word. "The Hobbit" becomes just "The", and the word "Hobbit" gets shoved into the author field, cascading a failure through every single record that follows.
// The "Wrong" Way: Fragile and prone to misalignment
void saveBooksNaive(const std::vector<Book>& library, const std::string& filename) {
std::ofstream outFile(filename);
for (const auto& book : library) {
// If title has spaces, loading this back is a nightmare
outFile << book.isbn << " " << book.title << " " << book.author << "\n";
}
}
Beyond the parsing nightmare, there's a bigger risk: data corruption. If your program crashes or the power goes out while that ofstream is halfway through overwriting your only copy of the library database, you haven't just lost the new books—you've corrupted the entire archive. I've spent far too many hours of my career recovering "half-written" files because a developer thought a simple std::ofstream was "safe enough."
Moving Toward Robust Serialization
To do this properly, you need two things: a reliable delimiter and an atomic write strategy. Instead of relying on whitespace, we use a character that is unlikely to appear in a book title, like a pipe (|) or a tab. More importantly, we use std::getline to read the entire field up to that delimiter. This ensures that "The Hobbit" stays as one unit, regardless of how many spaces it contains.
But the real professional touch is how we handle the file itself. Rather than opening the main database file and truncating it immediately, we write the current state of the library to a temporary file (e.g., library.db.tmp). Only once that file is successfully closed and flushed to disk do we rename it to the actual database filename. This way, if the system crashes mid-save, the original library.db remains untouched and valid. It's a bit more overhead, but it's the difference between a toy project and a tool people can actually trust with their data.
// The "Better" Way: Delimited fields and safer file handling
void saveBooksRobust(const std::vector<Book>& library, const std::string& filename) {
std::string tempFile = filename + ".tmp";
std::ofstream outFile(tempFile);
if (!outFile) throw std::runtime_error("Could not open temp file for writing");
for (const auto& book : library) {
outFile << book.isbn << "|" << book.title << "|" << book.author << "\n";
}
outFile.close();
// Atomic swap: Replace the old database with the new, verified one
if (std::rename(tempFile.c_str(), filename.c_str()) != 0) {
throw std::runtime_error("Failed to finalize database save");
}
}
When you load this back, you'll use std::getline(inFile, field, '|'). It’s a slightly more verbose pattern, but it removes the ambiguity of the data. You're essentially building a primitive CSV parser. While you could use a JSON or XML library for this, understanding how to handle raw streams and file system safety is a fundamental skill. It forces you to think about the "what if" scenarios—what if the disk is full? What if the file is read-only? What if the user enters a pipe character in the title? (In a real-world system, you'd either escape that character or validate the input to forbid it).
📋 Practical Task
Exercise: Implementing a Robust Book Record Serializer
You are tasked with completing the persistence layer for the Library Management System. You have a Book struct with std::string isbn, std::string title, and bool isBorrowed.
Create a class named LibraryPersistence that implements the following two methods:
void saveToDisk(const std::vector<Book>& books, const std::string& filePath): This must implement the "Atomic Write" pattern described in the lesson. Write the data to a.tmpfile using a pipe (|) delimiter, and only rename it to the finalfilePathif the write completes without error.std::vector<Book> loadFromDisk(const std::string& filePath): This must read the delimited file usingstd::getline. Ensure that you correctly convert theisBorrowedboolean from a string (e.g., "1" or "0") back into abooltype.
Test your implementation by creating a library containing at least one book with a multi-word title (e.g., "C++ Primer Fifth Edition"), saving it, clearing your vector, and loading it back to verify the title remained intact.
There are no comments for now.