-
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
180: Building a Simple Banking System with File Persistence
By now, you've got the hang of managing objects in memory, but a banking system that forgets every account the moment you hit Ctrl+C isn't much of a system. We need persistence. The goal is simple: save a list of Account objects to a disk and reload them when the program starts back up.
The Trap of Binary Dumps
When I first started with C++, I fell for a very seductive shortcut. I had a struct with some data and I thought, "Why bother writing a parser? I'll just cast my object to a char* and dump the raw memory bytes directly into a file." It looks like this:
// The "Naive" Way - DO NOT DO THIS
ofstream outFile("accounts.dat", ios::binary);
outFile.write(reinterpret_cast<char*>(&myAccount), sizeof(Account));
On the surface, this is incredibly fast. You're essentially taking a snapshot of your RAM and slapping it onto the hard drive. But here is where it breaks: std::string. If your Account class uses std::string for the account holder's name, you aren't actually saving the name. You're saving a pointer to a memory address on the heap where that string lived during that specific execution of the program. When you reload that file later, your program will try to read a memory address that no longer belongs to it, and you'll get a segmentation fault faster than you can say "bank run."
Even if you used fixed-size char arrays, you're still flirting with disaster. If you compile the program on one machine and move the data file to another with a different architecture or compiler padding, the offsets will be off and your data will be garbage. It's brittle, opaque, and dangerous.
The Reliability of Explicit Serialization
The professional way to handle this is explicit serialization. Instead of saving the memory layout, we save the data values. I prefer using a delimited text format (like CSV) for simple systems because it's human-readable—if a balance looks wrong, you can literally open the file in Notepad and see why.
The trade-off here is a bit of overhead. You have to write code to "flatten" your object into a string and "reconstitute" it back into an object. But in the context of a banking system, the millisecond spent parsing a string is nothing compared to the cost of corrupting a client's financial history. Here is how I'd approach it:
struct Account {
int id;
std::string owner;
double balance;
// Convert object to a single line of text
std::string serialize() const {
return std::to_string(id) + "," + owner + "," + std::to_string(balance);
}
// Static method to build an object from a line of text
static Account deserialize(const std::string& line) {
std::stringstream ss(line);
std::string segment;
std::vector<std::string> parts;
while (std::getline(ss, segment, ',')) {
parts.push_back(segment);
}
return { std::stoi(parts[0]), parts[1], std::stod(parts[2]) };
}
};
By overloading the way we save and load, we've decoupled the data from the memory architecture. Now, we can iterate through our std::vector<Account>, call serialize() on each one, and write those lines to a file. When loading, we simply read the file line-by-line and pass each string to our deserialize method.
I'll admit, if you're dealing with millions of records, you'd eventually move to something like Protocol Buffers or a proper SQL database. But for a standalone C++ tool, this explicit approach is the sweet spot between effort and reliability. You get a system that is portable, debuggable, and most importantly, it doesn't crash when you restart the app.
📋 Practical Task
Implementing the Account Ledger Persistence Layer
Your task is to extend a basic banking system to include a "Save and Load" feature. You are provided with an Account class. You must implement two functions: saveAccountsToFile(const std::vector<Account>& accounts, const std::string& filename) and loadAccountsFromFile(const std::string& filename).
- Requirement 1: Use a text-based format (CSV) to ensure the
std::stringmember of theAccountclass is preserved correctly. - Requirement 2: The
loadAccountsFromFilefunction must handle an empty file gracefully without crashing. - Requirement 3: Ensure that the
saveAccountsToFilefunction overwrites the previous ledger entirely to avoid duplicate entries upon multiple saves.
Test your implementation by creating three accounts, saving them, clearing your vector in memory, and then loading them back from the disk to verify the balances and names are intact.
There are no comments for now.