Skip to Content
Course content

180: Building a Simple Banking System with File Persistence

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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::string member of the Account class is preserved correctly.
  • Requirement 2: The loadAccountsFromFile function must handle an empty file gracefully without crashing.
  • Requirement 3: Ensure that the saveAccountsToFile function 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.