Build a Banking System with C++ OOP

Apply all 22 core C++ OOP concepts by building a multi-account banking system.

Introduction

30 Second Summary

Every time you check your bank balance or transfer money, you're interacting with software built on object-oriented principles. Different account types, transaction rules, and access controls all map naturally to OOP concepts.

In this project, you will build a complete Banking System in C++ that exercises every core OOP concept through one cohesive application. By the end, you'll have applied all 22 topics from classes through polymorphism to file I/O in a single working codebase.

What You'll Build

A multi-file C++ banking application with an abstract base class, multiple derived account types, transaction logging, exception handling, and file persistence.

By the end of this project, you'll have:

  • A class hierarchy with an abstract Account base class and derived types like SavingsAccount, CurrentAccount, and PremiumAccount demonstrating inheritance and polymorphism.
  • A working system demonstrating all four OOP pillars: encapsulation, abstraction, inheritance, and polymorphism applied to a real problem domain.
  • File persistence that saves and loads account data using C++ file streams, plus I/O redirection to generate bank statements to a log file.
  • Secret Mission: a TransferService that handles inter-account transfers with full transaction logging and rollback on failure.

Are there any prerequisites?

You should be comfortable with basic C++ syntax including variables, loops, and functions. A small familiarity with pointers is helpful, but the project reinforces pointer concepts where needed.

Before We Start

You are about to build a complete Banking System in C++ that covers every core Object-Oriented Programming concept through one cohesive project.

Before writing any banking code, you need to confirm your C++ compiler is ready and set up a clean project directory. This step gets your environment prepared so you can focus on OOP concepts from Step 2 onwards.

In this step, get ready to:
  • Verify your C++ compiler is installed and working.
  • Create your project directory on the Desktop.
  • Compile and run a test file to confirm C++17 support.
Open Terminal and verify the compiler

macOS comes with Apple Clang as its default C++ compiler, bundled inside Xcode Command Line Tools. You need to confirm it is installed before you can compile any C++ code.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal and press Enter to open it.
  • Check that the C++ compiler is available by running this command:
clang++ --version

✔️ I see a version number

You should see output starting with something like Apple clang version followed by a version number. This confirms Apple Clang is installed and ready to compile C++ code.

ⓧ Command not found

This means Xcode Command Line Tools are not installed yet. Install them by running this command:

xcode-select --install

A dialog will appear asking you to confirm the installation. Click Install and wait for the download to complete. This may take a few minutes depending on your internet speed.

  • Once the installation finishes, run clang++ --version again to confirm it works.

What is Apple Clang?

Apple Clang is Apple's version of the Clang/LLVM compiler, included with Xcode Command Line Tools. It supports modern C++ standards including C++17, which is what this project uses.

Create the project directory

Your banking system will live in a dedicated folder on your Desktop. This keeps all your project files organized in one place.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir banking-system
cd banking-system
  • Confirm the folder was created by running:
pwd

You should see /Users/yourname/Desktop/banking-system (with your actual username) in the output.

Compile and run a test file

This project uses the -std=c++17 flag to enable C++17 features. You will create a simple test file to confirm everything compiles and runs correctly.

  • Press Cmd+Space to open Spotlight.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the banking-system folder, then click Open.
  • Create a new file by clicking the new file icon in the Explorer sidebar (or press Cmd+N).
  • Name the file hello.cpp.
  • Paste the following code into hello.cpp:
#include <iostream>

int main() {
    // Print a message to confirm C++17 compilation works
    std::cout << "Hello, C++17 Banking System!" << std::endl;
    return 0;
}

What does this code do?

This is a minimal C++ program that prints a message to the terminal. If it compiles and runs, your compiler and C++17 flag are both working correctly.

  • Save hello.cpp by pressing Cmd+S.
  • Switch back to your Terminal window.
  • Compile the test file by running this command:
clang++ -std=c++17 hello.cpp -o hello

What does this command do?

  • clang++ invokes the C++ compiler.
  • -std=c++17 tells the compiler to use the C++17 standard. Apple Clang defaults to C++14, so you need this flag for modern features.
  • hello.cpp is the source file to compile.
  • -o hello names the output executable hello.
  • Run the compiled program by running:
./hello

You should see Hello, C++17 Banking System! printed in your Terminal.

Seeing an error when compiling?

  • If you see "no such file or directory", make sure your Terminal is inside the banking-system folder. Run pwd to check.
  • If you see a syntax error, double-check that you copied the code exactly as shown into hello.cpp and saved the file.

Help me debug my clang++ compilation error

  • Clean up the test file now that you have confirmed compilation works. Run:
rm hello.cpp hello

Your compiler is working and your project directory is ready. Next up, you will start building the Account class and learn how classes, objects, and access modifiers protect data in C++.

Create the Account Class with Encapsulation

Every banking system needs to protect sensitive data like account balances from being changed directly. In C++, you achieve this through encapsulation and access modifiers.

In this step, you will create the core Account class with private data that can only be accessed through controlled public methods. You will also build the supporting classes that the Account relies on.

In this step, get ready to:
  • Create the Account.h header file with private, protected, and public access modifiers.
  • Implement the Account class methods in Account.cpp with getter/setter encapsulation.
  • Set up main.cpp and verify the project compiles.
Create the Account header file

A header file in C++ declares the structure of your classes so other files know what's available. You will define the full Account class here, along with supporting exception classes and a transaction log.

  • In VS Code, create a new file called Account.h inside your banking-system folder.
  • Add the include guards and standard library includes by pasting this code:
#ifndef ACCOUNT_H
#define ACCOUNT_H

#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream>
#include <cstring>
#include <cstdio>

// Forward declarations - these classes will be defined later
class Auditor;
class TransferService;

What does this code do?

The #ifndef / #define / #endif pattern is called an include guard. It prevents the header from being included multiple times during compilation, which would cause "redefinition" errors.

The forward declarations tell the compiler that Auditor and TransferService are class names that will be fully defined elsewhere. This lets us reference them before they exist.

  • Below the forward declarations, add the custom exception classes:
// Custom Exceptions
class InsufficientFundsException : public std::runtime_error {
public:
    InsufficientFundsException(double requested, double available)
        : std::runtime_error("Insufficient funds"), requested(requested), available(available) {}
    double getRequested() const { return requested; }
    double getAvailable() const { return available; }
private:
    double requested;
    double available;
};

class InvalidAmountException : public std::runtime_error {
public:
    InvalidAmountException(const std::string& msg) : std::runtime_error(msg) {}
};

class AccountNotFoundException : public std::runtime_error {
public:
    AccountNotFoundException(int id) : std::runtime_error("Account not found"), accountId(id) {}
    int getAccountId() const { return accountId; }
private:
    int accountId;
};

What are these exception classes?

Each exception class inherits from std::runtime_error so they integrate with C++'s standard exception system. They store specific context about what went wrong (like the requested amount vs available balance).

Notice how requested and available are private but exposed through public getters. This is encapsulation in action.

  • Next, add the TransactionLog class below the exception classes. This class manages a dynamic array of transaction entries:
// TransactionLog - manages transaction history with dynamic memory
class TransactionLog {
public:
    TransactionLog() : entries(nullptr), capacity(0), count(0) {}
    TransactionLog(int initialCapacity) : capacity(initialCapacity), count(0) {
        entries = new std::string[capacity];
    }
    // Deep copy constructor - creates independent copy of dynamic array
    TransactionLog(const TransactionLog& other) : capacity(other.capacity), count(other.count) {
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
    }
    // Deep copy assignment using this pointer for self-check
    TransactionLog& operator=(const TransactionLog& other) {
        if (this == &other) return *this;
        delete[] entries;
        capacity = other.capacity;
        count = other.count;
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
        return *this;
    }
    ~TransactionLog() { delete[] entries; }
    // Returns *this for method chaining
    TransactionLog& addEntry(const std::string& entry) {
        if (count >= capacity) {
            int newCapacity = (capacity == 0) ? 4 : capacity * 2;
            std::string* newEntries = new std::string[newCapacity];
            for (int i = 0; i < count; i++) newEntries[i] = entries[i];
            delete[] entries;
            entries = newEntries;
            capacity = newCapacity;
        }
        entries[count++] = entry;
        return *this;
    }
    void display() const {
        for (int i = 0; i < count; i++)
            std::cout << "  [" << i + 1 << "] " << entries[i] << "\n";
    }
    int getCount() const { return count; }
private:
    std::string* entries;  // dynamically allocated array
    int capacity;
    int count;
};

Why use a dynamic array here?

The TransactionLog uses new std::string[] to allocate memory at runtime. This means it can grow as transactions happen, rather than being a fixed size.

The deep copy constructor and assignment operator allocate fresh memory and copy each entry individually. Without these, copying a TransactionLog would just copy the pointer, making two objects share the same array.

The this pointer appears in the self-assignment check (if (this == &other)) and in the return statements (return *this) which enables method chaining.

  • Now add the Account class declaration and close the include guard. This is where access modifiers define the encapsulation boundaries:
// Abstract base class - private data, protected for derived classes, public interface
class Account {
public:
    Account();
    Account(const std::string& name, double initialBalance);
    Account(const Account& other);
    virtual ~Account();
    // Pure virtual functions - make Account abstract
    virtual double calculateInterest() const = 0;
    virtual std::string getAccountType() const = 0;
    // Function overloading - two versions of deposit
    void deposit(double amount);
    void deposit(double amount, const std::string& description);
    void withdraw(double amount);
    // Display overloading
    void display() const;
    void display(bool detailed) const;
    // Public getters - controlled access to private data
    int getAccountNumber() const { return accountNumber; }
    std::string getHolderName() const { return holderName; }
    double getBalance() const { return balance; }
    const TransactionLog& getLog() const { return log; }
    // Static member functions
    static int getTotalAccounts();
    static int getNextId();
    virtual void saveToFile(std::ofstream& out) const;
    // Friend declarations - controlled access violation
    friend class Auditor;
    friend class TransferService;
    friend void displayAccountSecret(const Account& acc);
protected:
    double balance;       // derived classes can access directly
    TransactionLog log;   // derived classes can access directly
private:
    int accountNumber;    // only Account itself can access
    std::string holderName;
    static int totalAccounts;  // shared across all Account objects
    static int nextId;         // auto-generates unique IDs
};

#endif

How do the access modifiers create encapsulation?

The three access levels each serve a specific purpose in this banking system:

  • private: accountNumber and holderName can only be read through getters. No outside code can modify them directly.
  • protected: balance and log are accessible to derived classes (like SavingsAccount) but hidden from external code.
  • public: The methods like deposit(), withdraw(), and getBalance() form the controlled interface that all outside code must use.
  • Save Account.h.

Seeing red underlines in VS Code?

VS Code may show errors because it cannot find the implementations yet. These will disappear after you create Account.cpp in the next substep.

help me fix red underlines in my Account.h header file

✔️ Awesome, I've got everything!

Great. Make sure you have saved Account.h before moving on.

ⓧ I'd like to double check the full code

#ifndef ACCOUNT_H
#define ACCOUNT_H

#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream>
#include <cstring>
#include <cstdio>

class Auditor;
class TransferService;

class InsufficientFundsException : public std::runtime_error {
public:
    InsufficientFundsException(double requested, double available)
        : std::runtime_error("Insufficient funds"), requested(requested), available(available) {}
    double getRequested() const { return requested; }
    double getAvailable() const { return available; }
private:
    double requested;
    double available;
};

class InvalidAmountException : public std::runtime_error {
public:
    InvalidAmountException(const std::string& msg) : std::runtime_error(msg) {}
};

class AccountNotFoundException : public std::runtime_error {
public:
    AccountNotFoundException(int id) : std::runtime_error("Account not found"), accountId(id) {}
    int getAccountId() const { return accountId; }
private:
    int accountId;
};

class TransactionLog {
public:
    TransactionLog() : entries(nullptr), capacity(0), count(0) {}
    TransactionLog(int initialCapacity) : capacity(initialCapacity), count(0) {
        entries = new std::string[capacity];
    }
    TransactionLog(const TransactionLog& other) : capacity(other.capacity), count(other.count) {
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
    }
    TransactionLog& operator=(const TransactionLog& other) {
        if (this == &other) return *this;
        delete[] entries;
        capacity = other.capacity;
        count = other.count;
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
        return *this;
    }
    ~TransactionLog() { delete[] entries; }
    TransactionLog& addEntry(const std::string& entry) {
        if (count >= capacity) {
            int newCapacity = (capacity == 0) ? 4 : capacity * 2;
            std::string* newEntries = new std::string[newCapacity];
            for (int i = 0; i < count; i++) newEntries[i] = entries[i];
            delete[] entries;
            entries = newEntries;
            capacity = newCapacity;
        }
        entries[count++] = entry;
        return *this;
    }
    void display() const {
        for (int i = 0; i < count; i++)
            std::cout << "  [" << i + 1 << "] " << entries[i] << "\n";
    }
    int getCount() const { return count; }
private:
    std::string* entries;
    int capacity;
    int count;
};

class Account {
public:
    Account();
    Account(const std::string& name, double initialBalance);
    Account(const Account& other);
    virtual ~Account();
    virtual double calculateInterest() const = 0;
    virtual std::string getAccountType() const = 0;
    void deposit(double amount);
    void deposit(double amount, const std::string& description);
    void withdraw(double amount);
    void display() const;
    void display(bool detailed) const;
    int getAccountNumber() const { return accountNumber; }
    std::string getHolderName() const { return holderName; }
    double getBalance() const { return balance; }
    const TransactionLog& getLog() const { return log; }
    static int getTotalAccounts();
    static int getNextId();
    virtual void saveToFile(std::ofstream& out) const;
    friend class Auditor;
    friend class TransferService;
    friend void displayAccountSecret(const Account& acc);
protected:
    double balance;
    TransactionLog log;
private:
    int accountNumber;
    std::string holderName;
    static int totalAccounts;
    static int nextId;
};

#endif
Implement the Account class methods

The header declared what the Account class can do. Now you will implement the actual logic in a separate .cpp file.

  • Create a new file called Account.cpp in your banking-system folder.
  • Add the include, static member initialization, and all constructor implementations:
#include "Account.h"

// Static member initialization - shared by all Account objects
int Account::totalAccounts = 0;
int Account::nextId = 1000;

// Default constructor - creates account with placeholder values
Account::Account()
    : accountNumber(++nextId), holderName("Unknown"), balance(0.0), log(10) {
    totalAccounts++;
}

// Parameterized constructor - uses this-> to clarify member access
Account::Account(const std::string& name, double initialBalance)
    : accountNumber(++nextId), holderName(name), balance(initialBalance), log(10) {
    if (initialBalance < 0) throw InvalidAmountException("Initial balance cannot be negative");
    totalAccounts++;
    this->log.addEntry("Account created with balance: " + std::to_string(this->balance));
}

// Copy constructor - deep copies the TransactionLog
Account::Account(const Account& other)
    : accountNumber(++nextId), holderName(other.holderName), balance(other.balance), log(other.log) {
    totalAccounts++;
    log.addEntry("Account copied from account #" + std::to_string(other.accountNumber));
}

// Destructor - decrements the shared counter
Account::~Account() { totalAccounts--; }

What does this code do?

  • Static members (totalAccounts, nextId) must be defined outside the class. They exist once in memory, shared by all Account objects.
  • The parameterized constructor uses this->log and this->balance to explicitly reference the current object's members.
  • Each constructor increments totalAccounts and auto-generates a unique accountNumber using ++nextId.
  • The copy constructor gets a NEW account number (not a duplicate) but copies the balance and transaction history.
  • Below the destructor, add the deposit and withdraw method implementations:
// Deposit overload - amount only
void Account::deposit(double amount) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount));
}

// Deposit overload - amount with description
void Account::deposit(double amount, const std::string& description) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount) + " (" + description + ")");
}

// Withdraw with validation
void Account::withdraw(double amount) {
    if (amount <= 0) throw InvalidAmountException("Withdrawal amount must be positive");
    if (amount > balance) throw InsufficientFundsException(amount, balance);
    balance -= amount;
    log.addEntry("Withdrew: " + std::to_string(amount));
}

How does encapsulation protect the balance?

The balance variable is protected, meaning external code cannot write to it directly. The only way to change the balance is through deposit() and withdraw(), which enforce business rules.

Negative deposits are rejected. Withdrawals exceeding the balance throw an exception. Every change is logged. This is the core value of encapsulation.

  • Finally, add the display methods, static functions, and file handling:
// Brief display
void Account::display() const {
    std::cout << "[" << getAccountType() << "] #" << accountNumber
              << " | " << holderName << " | Balance: $" << balance << "\n";
}

// Detailed display with transaction history
void Account::display(bool detailed) const {
    if (!detailed) { display(); return; }
    std::cout << "=== Account Details ===\n";
    std::cout << "Type: " << getAccountType() << "\n";
    std::cout << "Account #: " << accountNumber << "\n";
    std::cout << "Holder: " << holderName << "\n";
    std::cout << "Balance: $" << balance << "\n";
    std::cout << "Interest: $" << calculateInterest() << "\n";
    std::cout << "Transaction History:\n";
    log.display();
    std::cout << "=======================\n";
}

// Static functions - work without an object
int Account::getTotalAccounts() { return totalAccounts; }
int Account::getNextId() { return nextId; }

// Save account data to a file stream
void Account::saveToFile(std::ofstream& out) const {
    out << getAccountType() << "|" << accountNumber << "|" << holderName << "|" << balance << "\n";
}

What does this code do?

  • The two display() versions demonstrate function overloading. The compiler picks which one to call based on whether you pass a bool argument.
  • Static functions getTotalAccounts() and getNextId() can be called without creating an Account object, using Account::getTotalAccounts().
  • The saveToFile() method writes account data in a pipe-delimited format for later parsing.
  • Save Account.cpp.

Seeing errors about undefined references?

Make sure your #include "Account.h" is the very first line in Account.cpp. Also confirm that Account.h and Account.cpp are in the same folder.

help me fix undefined reference errors in my Account.cpp file

✔️ Awesome, I've got everything!

Great. Make sure you have saved Account.cpp before moving on.

ⓧ I'd like to double check the full code

#include "Account.h"

int Account::totalAccounts = 0;
int Account::nextId = 1000;

Account::Account()
    : accountNumber(++nextId), holderName("Unknown"), balance(0.0), log(10) {
    totalAccounts++;
}

Account::Account(const std::string& name, double initialBalance)
    : accountNumber(++nextId), holderName(name), balance(initialBalance), log(10) {
    if (initialBalance < 0) throw InvalidAmountException("Initial balance cannot be negative");
    totalAccounts++;
    this->log.addEntry("Account created with balance: " + std::to_string(this->balance));
}

Account::Account(const Account& other)
    : accountNumber(++nextId), holderName(other.holderName), balance(other.balance), log(other.log) {
    totalAccounts++;
    log.addEntry("Account copied from account #" + std::to_string(other.accountNumber));
}

Account::~Account() { totalAccounts--; }

void Account::deposit(double amount) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount));
}

void Account::deposit(double amount, const std::string& description) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount) + " (" + description + ")");
}

void Account::withdraw(double amount) {
    if (amount <= 0) throw InvalidAmountException("Withdrawal amount must be positive");
    if (amount > balance) throw InsufficientFundsException(amount, balance);
    balance -= amount;
    log.addEntry("Withdrew: " + std::to_string(amount));
}

void Account::display() const {
    std::cout << "[" << getAccountType() << "] #" << accountNumber
              << " | " << holderName << " | Balance: $" << balance << "\n";
}

void Account::display(bool detailed) const {
    if (!detailed) { display(); return; }
    std::cout << "=== Account Details ===\n";
    std::cout << "Type: " << getAccountType() << "\n";
    std::cout << "Account #: " << accountNumber << "\n";
    std::cout << "Holder: " << holderName << "\n";
    std::cout << "Balance: $" << balance << "\n";
    std::cout << "Interest: $" << calculateInterest() << "\n";
    std::cout << "Transaction History:\n";
    log.display();
    std::cout << "=======================\n";
}

int Account::getTotalAccounts() { return totalAccounts; }
int Account::getNextId() { return nextId; }

void Account::saveToFile(std::ofstream& out) const {
    out << getAccountType() << "|" << accountNumber << "|" << holderName << "|" << balance << "\n";
}
Set up main.cpp and verify compilation

With the class defined and implemented, you need a main.cpp entry point. Since Account is abstract (it has pure virtual functions), you cannot instantiate it directly yet.

  • Create a new file called main.cpp in your banking-system folder.
  • Add the following starter code that includes the Account header and prints a banner:
#include "Account.h"
#include <vector>
#include <memory>

int main() {
    std::cout << "========================================\n";
    std::cout << "    C++ OOP Banking System\n";
    std::cout << "========================================\n\n";
    return 0;
}

Why can't we create an Account object yet?

The Account class has two pure virtual functions: calculateInterest() and getAccountType(). A class with any pure virtual function is abstract and cannot be instantiated.

In the next step, you will create derived classes like SavingsAccount that implement these functions. Then you will be able to create real account objects and interact with them through the public interface.

  • Save main.cpp.
  • Switch to your terminal and compile the project by running this command:
clang++ -std=c++17 Account.cpp main.cpp -o banking
  • Run the compiled program:
./banking

You should see the banner printed in your terminal:

========================================
    C++ OOP Banking System
========================================

Compilation failed?

Make sure all three files (Account.h, Account.cpp, main.cpp) are in the same banking-system folder. Confirm you are running the command from inside that folder with pwd.

If you see "no such file" errors, check that filenames match exactly (case-sensitive). If you see syntax errors, compare your code with the full file in the double-check tabs above.

help me fix compilation errors in my C++ banking system project

Your class structure is in place. Next up, you will create derived account types that inherit from this base class and bring the system to life with real account objects.

Add Constructors, the this Pointer, and Static Members

Your Account class is abstract. It has constructors, the this pointer, and static members built in, but you cannot create an Account object directly because of its pure virtual functions.

To actually demonstrate these OOP features, you need concrete derived classes that inherit from Account. In this step, you will build a full class hierarchy and then exercise constructor overloading, static members, and deep copy in main.cpp.

In this step, get ready to:
  • Declare derived account classes with multiple constructor variants in the header.
  • Implement the derived classes showing constructor chaining and friend access.
  • Demonstrate constructor overloading, static member functions, and deep copy in main.
Declare the derived account classes in the header

Since Account is abstract, you need concrete classes that implement its pure virtual functions. You will add savings accounts, current accounts, and more advanced types that demonstrate hierarchical, multilevel, and multiple inheritance.

  • In Account.h, find the closing }; of the Account class, then add the following class declarations below it:
class SavingsAccount : public Account {
public:
    SavingsAccount();
    SavingsAccount(const std::string& name, double initialBalance, double rate);
    SavingsAccount(const SavingsAccount& other);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getInterestRate() const { return interestRate; }
    void saveToFile(std::ofstream& out) const override;
protected:
    double interestRate;
};

class CurrentAccount : public Account {
public:
    CurrentAccount();
    CurrentAccount(const std::string& name, double initialBalance, double overdraftLimit);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getOverdraftLimit() const { return overdraftLimit; }
    void saveToFile(std::ofstream& out) const override;
private:
    double overdraftLimit;
};

class SeniorSavingsAccount : public SavingsAccount {
public:
    SeniorSavingsAccount(const std::string& name, double initialBalance, double rate, double bonus);
    double calculateInterest() const override;
    std::string getAccountType() const override;
private:
    double bonusRate;
};

What does this code do?

These three classes form an inheritance hierarchy demonstrating constructor overloading and multilevel inheritance.

  • SavingsAccount has three constructors (default, parameterized, copy). Its interestRate is protected so derived classes like SeniorSavingsAccount can access it.
  • CurrentAccount keeps overdraftLimit private because no class inherits from it.
  • SeniorSavingsAccount inherits from SavingsAccount (multilevel inheritance). Its constructor chains upward through SavingsAccount to Account.
  • Directly below the SeniorSavingsAccount class, add the interface and multiple-inheritance classes:
class Insurable {
public:
    virtual ~Insurable() {}
    virtual double getInsurancePremium() const = 0;
    virtual std::string getInsuranceType() const = 0;
};

class PremiumAccount : public SavingsAccount, public Insurable {
public:
    PremiumAccount(const std::string& name, double initialBalance, double rate, double coverageAmount);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getInsurancePremium() const override;
    std::string getInsuranceType() const override;
private:
    double coverageAmount;
};

What is multiple inheritance?

PremiumAccount inherits from two base classes: SavingsAccount (for banking functionality) and Insurable (for insurance features). When it is constructed, both base class constructors run in left-to-right declaration order.

Insurable acts as an interface. It has only pure virtual functions and a virtual destructor, providing a contract without implementation.

  • Below PremiumAccount, add the friend class and friend function declarations:
class Auditor {
public:
    void inspectAccount(const Account& acc) const;
    void auditBalance(const Account& acc) const;
};

class JuniorAuditor : public Auditor {
public:
    void tryInspect(const Account& acc) const;
};

void displayAccountSecret(const Account& acc);

Why is JuniorAuditor here?

Auditor is declared as a friend of Account (you set that up in the previous step), so it can access private members. JuniorAuditor inherits from Auditor, but friendship is NOT inherited in C++. This demonstrates that access granted to a base class does not flow down to its children.

  • Make sure all these new declarations sit between the closing }; of the Account class and the #endif at the bottom of the file.
  • Save Account.h.

Seeing red squiggles in VS Code?

The project will not compile yet because you have declared classes without implementing them. That is expected. You will add the implementations in the next substep.

If you see errors about Account being undefined, make sure your new class declarations are placed after the Account class closing brace, not before it.

Help me fix errors in my Account.h class declarations.

Implement the derived classes

Each derived class needs constructor implementations that chain to their parent using initializer lists. This is where constructor overloading and the this pointer pattern from Account propagate through the hierarchy.

  • In Account.cpp, scroll to the bottom of the file (after the saveToFile implementation) and add the SavingsAccount and CurrentAccount implementations:
SavingsAccount::SavingsAccount() : Account(), interestRate(0.04) {}
SavingsAccount::SavingsAccount(const std::string& name, double initialBalance, double rate)
    : Account(name, initialBalance), interestRate(rate) {}
SavingsAccount::SavingsAccount(const SavingsAccount& other)
    : Account(other), interestRate(other.interestRate) {}
double SavingsAccount::calculateInterest() const { return balance * interestRate; }
std::string SavingsAccount::getAccountType() const { return "Savings"; }
void SavingsAccount::saveToFile(std::ofstream& out) const { Account::saveToFile(out); }

CurrentAccount::CurrentAccount() : Account(), overdraftLimit(500.0) {}
CurrentAccount::CurrentAccount(const std::string& name, double initialBalance, double overdraftLimit)
    : Account(name, initialBalance), overdraftLimit(overdraftLimit) {}
double CurrentAccount::calculateInterest() const { return 0.0; }
std::string CurrentAccount::getAccountType() const { return "Current"; }
void CurrentAccount::saveToFile(std::ofstream& out) const { Account::saveToFile(out); }

What does this code do?

  • Each SavingsAccount constructor uses an initializer list (: Account(name, initialBalance), interestRate(rate)) to chain to Account's parameterized constructor. This triggers the this pointer usage and static counter logic you wrote in the base class.
  • The copy constructor passes the other object up to Account(other), which triggers Account's copy constructor and its deep copy of the TransactionLog.
  • CurrentAccount returns 0.0 for interest because current accounts typically do not earn interest.
  • Below the CurrentAccount implementations, add the multilevel and multiple inheritance classes:
SeniorSavingsAccount::SeniorSavingsAccount(const std::string& name, double initialBalance, double rate, double bonus)
    : SavingsAccount(name, initialBalance, rate), bonusRate(bonus) {}
double SeniorSavingsAccount::calculateInterest() const { return balance * (interestRate + bonusRate); }
std::string SeniorSavingsAccount::getAccountType() const { return "Senior Savings"; }

PremiumAccount::PremiumAccount(const std::string& name, double initialBalance, double rate, double coverageAmount)
    : SavingsAccount(name, initialBalance, rate), coverageAmount(coverageAmount) {}
double PremiumAccount::calculateInterest() const { return balance * interestRate * 1.5; }
std::string PremiumAccount::getAccountType() const { return "Premium"; }
double PremiumAccount::getInsurancePremium() const { return coverageAmount * 0.02; }
std::string PremiumAccount::getInsuranceType() const { return "Deposit Insurance"; }

How does constructor chaining work in multilevel inheritance?

When you create a SeniorSavingsAccount, the constructor chain is: SeniorSavingsAccount calls SavingsAccount, which calls Account. Each level initializes its own members. The static totalAccounts counter increments once in Account's constructor because the chain ultimately reaches the base.

  • Finally, add the friend class and friend function implementations at the bottom of Account.cpp:
void Auditor::inspectAccount(const Account& acc) const {
    std::cout << "[AUDIT] Account #" << acc.accountNumber
              << " | Holder: " << acc.holderName
              << " | Balance: $" << acc.balance << "\n";
}
void Auditor::auditBalance(const Account& acc) const {
    std::cout << "[AUDIT] Private balance of " << acc.holderName
              << " is $" << acc.balance << "\n";
}
void JuniorAuditor::tryInspect(const Account& acc) const {
    std::cout << "[JUNIOR AUDIT] Account #" << acc.getAccountNumber()
              << " | Balance via getter: $" << acc.getBalance() << "\n";
    std::cout << "  (Note: Cannot access private members directly - friendship not inherited)\n";
}
void displayAccountSecret(const Account& acc) {
    std::cout << "[FRIEND FUNC] Secret view - Account #" << acc.accountNumber
              << " internal balance: $" << acc.balance << "\n";
}

What does this code do?

  • Auditor::inspectAccount() directly accesses acc.accountNumber and acc.balance (both private). This works because Auditor is a friend class of Account.
  • JuniorAuditor::tryInspect() must use public getters (getAccountNumber(), getBalance()) because friendship is not inherited. Even though JuniorAuditor inherits from Auditor, it does not inherit Auditor's friend access.
  • displayAccountSecret() is a standalone friend function (not a member of any class) that can also access private members directly.
  • Save Account.cpp.
  • Compile to verify there are no syntax errors by running:
clang++ -std=c++17 -o bank Account.cpp main.cpp
  • You should see no output (no errors). The binary is not useful to run yet because main.cpp only prints the header banner.

Getting linker errors about undefined references?

Make sure every function you declared in the header has a matching implementation in Account.cpp. A common cause is misspelling the class scope (e.g. SavingAccount:: instead of SavingsAccount::).

Help me fix linker errors in my C++ banking project.

Demonstrate constructors, static members, and deep copy in main

Now that you have concrete classes, you can prove that constructor overloading, static tracking, and deep copy all work together.

  • In main.cpp, replace everything inside main() (after the banner output lines) with the following demo code:
    // --- Classes & Encapsulation ---
    std::cout << "--- Classes & Encapsulation ---\n";
    SavingsAccount sa1("Alice", 5000.0, 0.05);
    sa1.display();
    std::cout << "Balance via getter: $" << sa1.getBalance() << "\n\n";

    // --- Constructors & Overloading ---
    std::cout << "--- Constructors & Overloading ---\n";
    SavingsAccount sa2; // default constructor
    SavingsAccount sa3("Bob", 3000.0, 0.04); // parameterized
    SavingsAccount sa4(sa1); // copy constructor (deep copy of TransactionLog)
    std::cout << "Default: "; sa2.display();
    std::cout << "Parameterized: "; sa3.display();
    std::cout << "Copied from Alice: "; sa4.display();
    std::cout << "\n";

    // --- Static Members ---
    std::cout << "--- Static Members ---\n";
    std::cout << "Total accounts created: " << Account::getTotalAccounts() << "\n";
    std::cout << "Next ID will be: " << Account::getNextId() + 1 << "\n\n";

    // --- Deep Copy Demo ---
    std::cout << "--- Deep Copy vs Shallow Copy ---\n";
    TransactionLog log1(5);
    log1.addEntry("Transfer $100").addEntry("Deposit $500"); // method chaining via this
    TransactionLog log2 = log1; // deep copy
    log2.addEntry("New entry only in log2");
    std::cout << "Log1 (" << log1.getCount() << " entries):\n"; log1.display();
    std::cout << "Log2 (" << log2.getCount() << " entries):\n"; log2.display();
    std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";

    return 0;

What does this code do?

  • The first section creates a SavingsAccount using the parameterized constructor and shows that you interact with it through the public interface (getBalance()) rather than touching private members directly.
  • The constructors section demonstrates all three constructor variants. sa4(sa1) triggers the copy constructor, which deep copies the internal TransactionLog.
  • Account::getTotalAccounts() is a static member function called without an object. It reports how many accounts exist because the static totalAccounts counter is shared across ALL instances.
  • The deep copy demo uses method chaining (log1.addEntry(...).addEntry(...)) which works because addEntry() returns *this. After copying log1 into log2, adding to log2 does not affect log1 because each has its own dynamically allocated array.
  • Save main.cpp.
  • Compile and run the program:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • You should see output showing Alice's account, the three constructor variants (default, parameterized, copied), the total accounts count, and the deep copy demo proving log1 has 2 entries while log2 has 3.

Program crashes or shows garbage output?

If you see a segfault or corrupted text, check that the TransactionLog copy constructor in Account.h allocates a new array (entries = new std::string[capacity]) and copies each element individually. A shallow copy would share the same pointer, causing double-free corruption.

Help me debug my banking system output.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the complete state of each file at this point.

#ifndef ACCOUNT_H
#define ACCOUNT_H

#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream>
#include <cstring>
#include <cstdio>

class Auditor;
class TransferService;

class InsufficientFundsException : public std::runtime_error {
public:
    InsufficientFundsException(double requested, double available)
        : std::runtime_error("Insufficient funds"), requested(requested), available(available) {}
    double getRequested() const { return requested; }
    double getAvailable() const { return available; }
private:
    double requested;
    double available;
};

class InvalidAmountException : public std::runtime_error {
public:
    InvalidAmountException(const std::string& msg) : std::runtime_error(msg) {}
};

class AccountNotFoundException : public std::runtime_error {
public:
    AccountNotFoundException(int id) : std::runtime_error("Account not found"), accountId(id) {}
    int getAccountId() const { return accountId; }
private:
    int accountId;
};

class TransactionLog {
public:
    TransactionLog() : entries(nullptr), capacity(0), count(0) {}
    TransactionLog(int initialCapacity) : capacity(initialCapacity), count(0) {
        entries = new std::string[capacity];
    }
    TransactionLog(const TransactionLog& other) : capacity(other.capacity), count(other.count) {
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
    }
    TransactionLog& operator=(const TransactionLog& other) {
        if (this == &other) return *this;
        delete[] entries;
        capacity = other.capacity;
        count = other.count;
        entries = new std::string[capacity];
        for (int i = 0; i < count; i++) entries[i] = other.entries[i];
        return *this;
    }
    ~TransactionLog() { delete[] entries; }
    TransactionLog& addEntry(const std::string& entry) {
        if (count >= capacity) {
            int newCapacity = (capacity == 0) ? 4 : capacity * 2;
            std::string* newEntries = new std::string[newCapacity];
            for (int i = 0; i < count; i++) newEntries[i] = entries[i];
            delete[] entries;
            entries = newEntries;
            capacity = newCapacity;
        }
        entries[count++] = entry;
        return *this;
    }
    void display() const {
        for (int i = 0; i < count; i++)
            std::cout << "  [" << i + 1 << "] " << entries[i] << "\n";
    }
    int getCount() const { return count; }
private:
    std::string* entries;
    int capacity;
    int count;
};

class Account {
public:
    Account();
    Account(const std::string& name, double initialBalance);
    Account(const Account& other);
    virtual ~Account();
    virtual double calculateInterest() const = 0;
    virtual std::string getAccountType() const = 0;
    void deposit(double amount);
    void deposit(double amount, const std::string& description);
    void withdraw(double amount);
    void display() const;
    void display(bool detailed) const;
    int getAccountNumber() const { return accountNumber; }
    std::string getHolderName() const { return holderName; }
    double getBalance() const { return balance; }
    const TransactionLog& getLog() const { return log; }
    static int getTotalAccounts();
    static int getNextId();
    virtual void saveToFile(std::ofstream& out) const;
    friend class Auditor;
    friend class TransferService;
    friend void displayAccountSecret(const Account& acc);
protected:
    double balance;
    TransactionLog log;
private:
    int accountNumber;
    std::string holderName;
    static int totalAccounts;
    static int nextId;
};

class SavingsAccount : public Account {
public:
    SavingsAccount();
    SavingsAccount(const std::string& name, double initialBalance, double rate);
    SavingsAccount(const SavingsAccount& other);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getInterestRate() const { return interestRate; }
    void saveToFile(std::ofstream& out) const override;
protected:
    double interestRate;
};

class CurrentAccount : public Account {
public:
    CurrentAccount();
    CurrentAccount(const std::string& name, double initialBalance, double overdraftLimit);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getOverdraftLimit() const { return overdraftLimit; }
    void saveToFile(std::ofstream& out) const override;
private:
    double overdraftLimit;
};

class SeniorSavingsAccount : public SavingsAccount {
public:
    SeniorSavingsAccount(const std::string& name, double initialBalance, double rate, double bonus);
    double calculateInterest() const override;
    std::string getAccountType() const override;
private:
    double bonusRate;
};

class Insurable {
public:
    virtual ~Insurable() {}
    virtual double getInsurancePremium() const = 0;
    virtual std::string getInsuranceType() const = 0;
};

class PremiumAccount : public SavingsAccount, public Insurable {
public:
    PremiumAccount(const std::string& name, double initialBalance, double rate, double coverageAmount);
    double calculateInterest() const override;
    std::string getAccountType() const override;
    double getInsurancePremium() const override;
    std::string getInsuranceType() const override;
private:
    double coverageAmount;
};

class Auditor {
public:
    void inspectAccount(const Account& acc) const;
    void auditBalance(const Account& acc) const;
};

class JuniorAuditor : public Auditor {
public:
    void tryInspect(const Account& acc) const;
};

void displayAccountSecret(const Account& acc);

#endif
#include "Account.h"

int Account::totalAccounts = 0;
int Account::nextId = 1000;

Account::Account()
    : accountNumber(++nextId), holderName("Unknown"), balance(0.0), log(10) {
    totalAccounts++;
}

Account::Account(const std::string& name, double initialBalance)
    : accountNumber(++nextId), holderName(name), balance(initialBalance), log(10) {
    if (initialBalance < 0) throw InvalidAmountException("Initial balance cannot be negative");
    totalAccounts++;
    this->log.addEntry("Account created with balance: " + std::to_string(this->balance));
}

Account::Account(const Account& other)
    : accountNumber(++nextId), holderName(other.holderName), balance(other.balance), log(other.log) {
    totalAccounts++;
    log.addEntry("Account copied from account #" + std::to_string(other.accountNumber));
}

Account::~Account() { totalAccounts--; }

void Account::deposit(double amount) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount));
}

void Account::deposit(double amount, const std::string& description) {
    if (amount <= 0) throw InvalidAmountException("Deposit amount must be positive");
    balance += amount;
    log.addEntry("Deposited: " + std::to_string(amount) + " (" + description + ")");
}

void Account::withdraw(double amount) {
    if (amount <= 0) throw InvalidAmountException("Withdrawal amount must be positive");
    if (amount > balance) throw InsufficientFundsException(amount, balance);
    balance -= amount;
    log.addEntry("Withdrew: " + std::to_string(amount));
}

void Account::display() const {
    std::cout << "[" << getAccountType() << "] #" << accountNumber
              << " | " << holderName << " | Balance: $" << balance << "\n";
}

void Account::display(bool detailed) const {
    if (!detailed) { display(); return; }
    std::cout << "=== Account Details ===\n";
    std::cout << "Type: " << getAccountType() << "\n";
    std::cout << "Account #: " << accountNumber << "\n";
    std::cout << "Holder: " << holderName << "\n";
    std::cout << "Balance: $" << balance << "\n";
    std::cout << "Interest: $" << calculateInterest() << "\n";
    std::cout << "Transaction History:\n";
    log.display();
    std::cout << "=======================\n";
}

int Account::getTotalAccounts() { return totalAccounts; }
int Account::getNextId() { return nextId; }

void Account::saveToFile(std::ofstream& out) const {
    out << getAccountType() << "|" << accountNumber << "|" << holderName << "|" << balance << "\n";
}

SavingsAccount::SavingsAccount() : Account(), interestRate(0.04) {}
SavingsAccount::SavingsAccount(const std::string& name, double initialBalance, double rate)
    : Account(name, initialBalance), interestRate(rate) {}
SavingsAccount::SavingsAccount(const SavingsAccount& other)
    : Account(other), interestRate(other.interestRate) {}
double SavingsAccount::calculateInterest() const { return balance * interestRate; }
std::string SavingsAccount::getAccountType() const { return "Savings"; }
void SavingsAccount::saveToFile(std::ofstream& out) const { Account::saveToFile(out); }

CurrentAccount::CurrentAccount() : Account(), overdraftLimit(500.0) {}
CurrentAccount::CurrentAccount(const std::string& name, double initialBalance, double overdraftLimit)
    : Account(name, initialBalance), overdraftLimit(overdraftLimit) {}
double CurrentAccount::calculateInterest() const { return 0.0; }
std::string CurrentAccount::getAccountType() const { return "Current"; }
void CurrentAccount::saveToFile(std::ofstream& out) const { Account::saveToFile(out); }

SeniorSavingsAccount::SeniorSavingsAccount(const std::string& name, double initialBalance, double rate, double bonus)
    : SavingsAccount(name, initialBalance, rate), bonusRate(bonus) {}
double SeniorSavingsAccount::calculateInterest() const { return balance * (interestRate + bonusRate); }
std::string SeniorSavingsAccount::getAccountType() const { return "Senior Savings"; }

PremiumAccount::PremiumAccount(const std::string& name, double initialBalance, double rate, double coverageAmount)
    : SavingsAccount(name, initialBalance, rate), coverageAmount(coverageAmount) {}
double PremiumAccount::calculateInterest() const { return balance * interestRate * 1.5; }
std::string PremiumAccount::getAccountType() const { return "Premium"; }
double PremiumAccount::getInsurancePremium() const { return coverageAmount * 0.02; }
std::string PremiumAccount::getInsuranceType() const { return "Deposit Insurance"; }

void Auditor::inspectAccount(const Account& acc) const {
    std::cout << "[AUDIT] Account #" << acc.accountNumber
              << " | Holder: " << acc.holderName
              << " | Balance: $" << acc.balance << "\n";
}
void Auditor::auditBalance(const Account& acc) const {
    std::cout << "[AUDIT] Private balance of " << acc.holderName
              << " is $" << acc.balance << "\n";
}
void JuniorAuditor::tryInspect(const Account& acc) const {
    std::cout << "[JUNIOR AUDIT] Account #" << acc.getAccountNumber()
              << " | Balance via getter: $" << acc.getBalance() << "\n";
    std::cout << "  (Note: Cannot access private members directly - friendship not inherited)\n";
}
void displayAccountSecret(const Account& acc) {
    std::cout << "[FRIEND FUNC] Secret view - Account #" << acc.accountNumber
              << " internal balance: $" << acc.balance << "\n";
}
#include "Account.h"
#include <vector>
#include <memory>

int main() {
    std::cout << "========================================\n";
    std::cout << "    C++ OOP Banking System\n";
    std::cout << "========================================\n\n";

    // --- Classes & Encapsulation ---
    std::cout << "--- Classes & Encapsulation ---\n";
    SavingsAccount sa1("Alice", 5000.0, 0.05);
    sa1.display();
    std::cout << "Balance via getter: $" << sa1.getBalance() << "\n\n";

    // --- Constructors & Overloading ---
    std::cout << "--- Constructors & Overloading ---\n";
    SavingsAccount sa2;
    SavingsAccount sa3("Bob", 3000.0, 0.04);
    SavingsAccount sa4(sa1);
    std::cout << "Default: "; sa2.display();
    std::cout << "Parameterized: "; sa3.display();
    std::cout << "Copied from Alice: "; sa4.display();
    std::cout << "\n";

    // --- Static Members ---
    std::cout << "--- Static Members ---\n";
    std::cout << "Total accounts created: " << Account::getTotalAccounts() << "\n";
    std::cout << "Next ID will be: " << Account::getNextId() + 1 << "\n\n";

    // --- Deep Copy Demo ---
    std::cout << "--- Deep Copy vs Shallow Copy ---\n";
    TransactionLog log1(5);
    log1.addEntry("Transfer $100").addEntry("Deposit $500");
    TransactionLog log2 = log1;
    log2.addEntry("New entry only in log2");
    std::cout << "Log1 (" << log1.getCount() << " entries):\n"; log1.display();
    std::cout << "Log2 (" << log2.getCount() << " entries):\n"; log2.display();
    std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";

    return 0;
}

Your class hierarchy is in place and the core OOP features are proven. Next up, you will exercise these classes through runtime polymorphism, function overloading, and exception handling.

Implement Friend Classes, Inheritance, and Polymorphism

Your banking system now has a solid class hierarchy with constructors, static members, and deep copy logic. The next challenge is controlling who can access private data and making the system behave differently depending on the account type at runtime.

In this step, you will use friend classes to grant special access to an auditor, then use runtime polymorphism and multiple inheritance to make your system handle different account types through a single interface.

In this step, get ready to:
  • Use a friend class and friend function to access private account data.
  • Demonstrate runtime polymorphism by calling virtual functions through base pointers.
  • Create a PremiumAccount that uses multiple inheritance.
Use friend classes and functions to access private data

In a real bank, external auditors need to inspect accounts without going through the normal customer-facing interface. C++ models this with the friend keyword, which grants a specific class or function direct access to another class's private members.

The Auditor class and displayAccountSecret() friend function are already declared and implemented. You just need to use them in main.cpp.

  • In main.cpp, find the line that says std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";.
  • Add the following code below that line (before return 0;):
    std::cout << "--- Friend Class & Functions ---\n";
    Auditor auditor;
    auditor.inspectAccount(sa1); // Auditor accesses private members
    auditor.auditBalance(sa3);
    displayAccountSecret(sa1); // friend function accesses private members

    JuniorAuditor junior;
    junior.tryInspect(sa1); // Cannot access private - friendship not inherited
    std::cout << "\n";

What does this code do?

The Auditor class is declared as a friend of Account, so it can read accountNumber, holderName, and balance directly without getters.

The standalone function displayAccountSecret() is also a friend of Account, proving that friend access works for both classes and standalone functions.

The JuniorAuditor inherits from Auditor but cannot access Account's private members. Friendship is not inherited in C++.

  • Save main.cpp.
  • Compile and run your project by running this command in Terminal:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • You should see output starting with [AUDIT] Account # showing private data accessed directly, followed by [JUNIOR AUDIT] showing it can only use public getters.

Not seeing the audit output?

Make sure you placed the new code after the deep copy demo and before return 0;. Check that you have the closing \n on the final std::cout line for proper spacing.

Help me debug my friend class output.

Demonstrate runtime polymorphism with virtual functions

Polymorphism lets you treat different account types through a single Account* pointer. When you call a virtual function through that pointer, C++ figures out the correct derived-class version at runtime. This is called dynamic dispatch.

  • In main.cpp, add the following code directly after the friend class section you just wrote (before return 0;):
    std::cout << "--- Polymorphism & Virtual Functions ---\n";
    CurrentAccount ca1("Charlie", 10000.0, 2000.0);
    SeniorSavingsAccount ssa1("Diana", 20000.0, 0.06, 0.02);

    // Runtime polymorphism - base pointer, derived behavior
    Account* accounts[] = { &sa1, &ca1, &ssa1 };
    for (int i = 0; i < 3; i++) {
        std::cout << accounts[i]->getAccountType()
                  << " - Interest: $" << accounts[i]->calculateInterest() << "\n";
    }
    std::cout << "\n";

What does this code do?

The array Account* accounts[] holds pointers to three different account types: a SavingsAccount, a CurrentAccount, and a SeniorSavingsAccount.

Because calculateInterest() and getAccountType() are virtual, the loop calls the correct derived-class version for each pointer. The Savings account uses balance * interestRate, Current returns 0.0, and Senior Savings adds a bonus rate on top.

Why is this called runtime polymorphism?

The compiler does not know which calculateInterest() to call when it compiles the loop. It only knows at runtime which derived type each pointer actually points to. This is why virtual functions are also called late-binding or dynamic dispatch.

  • Save main.cpp and compile by running:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • You should see three lines showing different interest values: Savings calculates $250, Current shows $0, and Senior Savings shows $1600 (the combined base + bonus rate on Diana's $20000 balance).

Seeing a compile error about abstract class?

If you see an error mentioning you cannot instantiate an abstract class, check that you are creating a CurrentAccount (not a bare Account). The Account class is abstract because it has pure virtual functions.

Help me fix my polymorphism code.

Create a PremiumAccount using multiple inheritance

Some accounts in a bank need capabilities from more than one source. A premium account earns interest like a savings account AND provides deposit insurance. C++ handles this with multiple inheritance, where a class derives from two or more base classes simultaneously.

  • In main.cpp, add the following code after the polymorphism section (before return 0;):
    std::cout << "--- Multiple Inheritance ---\n";
    PremiumAccount pa1("Eve", 50000.0, 0.05, 100000.0);
    pa1.display();
    std::cout << "Insurance Premium: $" << pa1.getInsurancePremium() << "\n";
    std::cout << "Insurance Type: " << pa1.getInsuranceType() << "\n";
    std::cout << "Interest: $" << pa1.calculateInterest() << "\n\n";

What does this code do?

PremiumAccount inherits from both SavingsAccount and Insurable. It gets deposit/withdrawal capabilities from the savings side, and insurance methods from the Insurable interface.

When the constructor runs, C++ calls base class constructors in the order they appear in the inheritance list: SavingsAccount first (which calls Account first), then Insurable. The coverage amount of 100000.0 produces a $2000 insurance premium at the 2% rate.

  • Save main.cpp and compile by running:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • You should see Eve's Premium account displayed with an Insurance Premium of $2000, Insurance Type of Deposit Insurance, and Interest of $3750 (the 50% bonus on the base 5% rate applied to $50000).

Seeing ambiguous function call errors?

If you see errors about ambiguous calls, make sure PremiumAccount overrides getAccountType() and calculateInterest() in your Account.h and Account.cpp. Without explicit overrides, the compiler cannot resolve which inherited version to use.

Help me fix my multiple inheritance errors.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp with all three sections (friend class, polymorphism, and multiple inheritance).

ⓧ I'd like to double check the full code

Here is the complete main.cpp at the end of this step:

#include "Account.h"
#include <vector>
#include <memory>

int main() {
    std::cout << "========================================\n";
    std::cout << "    C++ OOP Banking System\n";
    std::cout << "========================================\n\n";

    std::cout << "--- Classes & Encapsulation ---\n";
    SavingsAccount sa1("Alice", 5000.0, 0.05);
    sa1.display();
    std::cout << "Balance via getter: $" << sa1.getBalance() << "\n\n";

    std::cout << "--- Constructors & Overloading ---\n";
    SavingsAccount sa2;
    SavingsAccount sa3("Bob", 3000.0, 0.04);
    SavingsAccount sa4(sa1);
    std::cout << "Default: "; sa2.display();
    std::cout << "Parameterized: "; sa3.display();
    std::cout << "Copied from Alice: "; sa4.display();
    std::cout << "\n";

    std::cout << "--- Static Members ---\n";
    std::cout << "Total accounts created: " << Account::getTotalAccounts() << "\n";
    std::cout << "Next ID will be: " << Account::getNextId() + 1 << "\n\n";

    std::cout << "--- Deep Copy vs Shallow Copy ---\n";
    TransactionLog log1(5);
    log1.addEntry("Transfer $100").addEntry("Deposit $500");
    TransactionLog log2 = log1;
    log2.addEntry("New entry only in log2");
    std::cout << "Log1 (" << log1.getCount() << " entries):\n"; log1.display();
    std::cout << "Log2 (" << log2.getCount() << " entries):\n"; log2.display();
    std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";

    std::cout << "--- Friend Class & Functions ---\n";
    Auditor auditor;
    auditor.inspectAccount(sa1);
    auditor.auditBalance(sa3);
    displayAccountSecret(sa1);
    JuniorAuditor junior;
    junior.tryInspect(sa1);
    std::cout << "\n";

    std::cout << "--- Polymorphism & Virtual Functions ---\n";
    CurrentAccount ca1("Charlie", 10000.0, 2000.0);
    SeniorSavingsAccount ssa1("Diana", 20000.0, 0.06, 0.02);
    Account* accounts[] = { &sa1, &ca1, &ssa1 };
    for (int i = 0; i < 3; i++) {
        std::cout << accounts[i]->getAccountType()
                  << " - Interest: $" << accounts[i]->calculateInterest() << "\n";
    }
    std::cout << "\n";

    std::cout << "--- Multiple Inheritance ---\n";
    PremiumAccount pa1("Eve", 50000.0, 0.05, 100000.0);
    pa1.display();
    std::cout << "Insurance Premium: $" << pa1.getInsurancePremium() << "\n";
    std::cout << "Insurance Type: " << pa1.getInsuranceType() << "\n";
    std::cout << "Interest: $" << pa1.calculateInterest() << "\n\n";

    return 0;
}

Your banking system now demonstrates controlled access with friends, runtime polymorphism through virtual functions, and multiple inheritance combining capabilities from two base classes. Next up, you will add function overloading and exception handling to make the system more robust.

Add Function Overloading and Exception Handling

Your banking system has a full class hierarchy with polymorphism and multiple inheritance working. Now it is time to make the interface more flexible and the code more robust.

Function overloading lets you offer multiple versions of the same method, each accepting different arguments. Exception handling protects your system from crashing when users attempt invalid operations like withdrawing more money than they have.

In this step, get ready to:
  • Demonstrate function overloading by calling different versions of deposit() and display().
  • Add exception handling with try-catch blocks to gracefully handle invalid operations.
  • Test exception scenarios including insufficient funds, invalid amounts, and constructor failures.
Demonstrate function overloading with deposit and display

Function overloading means defining multiple functions with the same name but different parameter lists. The compiler figures out which version to call based on the arguments you pass.

Your Account class already has two versions of deposit() and two versions of display() defined in Account.cpp. Now you will call them in main.cpp to see overloading in action.

  • In main.cpp, find the line that says return 0; at the bottom of your main() function.
  • Add the following function overloading demo directly above return 0;:
    // --- Step 9: Function Overloading ---
    std::cout << "--- Function Overloading ---\n";
    sa1.deposit(1000.0); // deposit(double)
    sa1.deposit(500.0, "Birthday gift"); // deposit(double, string)
    sa1.display(); // brief display
    sa1.display(true); // detailed display with transaction history
    std::cout << "\n";

What does this code do?

You are calling the same function names with different arguments, and the compiler picks the correct version automatically.

  • sa1.deposit(1000.0) calls deposit(double amount) because you passed one double.
  • sa1.deposit(500.0, "Birthday gift") calls deposit(double, const std::string&) because you passed a double and a string.
  • sa1.display() calls the no-argument version for a brief summary. sa1.display(true) calls the bool version for full details including transaction history.
  • Save main.cpp.
  • Compile and run your project by running this command in Terminal:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • Scroll to the --- Function Overloading --- section in the output. You should see a brief one-line display of Alice's account followed by the full detailed view including transaction history entries.

Not seeing the Function Overloading section?

  • Make sure you placed the new code above return 0; but below the Multiple Inheritance section.
  • If you see a compiler error about sa1 not being declared, check that your new code is still inside the main() function's curly braces.
  • Help me debug my function overloading code in main.cpp
Add exception handling with try-catch blocks

A real banking system cannot crash when a user tries to withdraw more than their balance. Exception handling lets you anticipate these failures and respond gracefully instead of terminating the program.

Your custom exception classes (InsufficientFundsException, InvalidAmountException) are already defined in Account.h. The throw statements are already inside withdraw() and deposit(). Now you will wrap calls in try-catch blocks to catch those exceptions.

  • In main.cpp, add the following exception handling demo directly below the function overloading section you just added (still above return 0;):
    // --- Step 10: Exception Handling ---
    std::cout << "--- Exception Handling ---\n";
    try {
        sa1.withdraw(999999.0);
    } catch (const InsufficientFundsException& e) {
        std::cout << "Caught: " << e.what()
                  << " (Requested: $" << e.getRequested()
                  << ", Available: $" << e.getAvailable() << ")\n";
    }

    try {
        sa1.deposit(-100.0);
    } catch (const InvalidAmountException& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }

    try {
        SavingsAccount bad("Test", -500.0, 0.05);
    } catch (const InvalidAmountException& e) {
        std::cout << "Constructor exception: " << e.what() << "\n";
    }
    std::cout << "\n";

What does this code do?

Each try-catch block wraps an operation that might fail, then catches the specific exception type thrown.

  • sa1.withdraw(999999.0) triggers InsufficientFundsException because Alice does not have that much money. The catch block prints the requested and available amounts using custom getter methods.
  • sa1.deposit(-100.0) triggers InvalidAmountException because deposit amounts must be positive.
  • SavingsAccount bad("Test", -500.0, 0.05) shows that exceptions can fire inside constructors too. A negative initial balance throws before the object is fully created.

Why catch by const reference?

Catching exceptions with const ExceptionType& e avoids copying the exception object and preserves polymorphic behavior. If you caught by value, a derived exception could be sliced down to its base class.

  • Save main.cpp.
  • Compile and run again:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank
  • Scroll to the --- Exception Handling --- section. You should see three caught exceptions printed with their messages, and the program continues running without crashing.

Program crashes instead of catching the exception?

  • Make sure each risky call is inside its own try block. If the try wrapping is missing, the exception propagates up and terminates the program.
  • Check that the exception class name in your catch matches exactly what is thrown. A misspelled type means the catch will not match and the exception goes uncaught.
  • Help me fix my exception handling code

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp and your program compiles without errors.

ⓧ I'd like to double check the full code

Here is the complete main.cpp with everything up to this point:

#include "Account.h"
#include <vector>
#include <memory>

int main() {
    std::cout << "========================================\n";
    std::cout << "    C++ OOP Banking System\n";
    std::cout << "========================================\n\n";

    std::cout << "--- Classes & Encapsulation ---\n";
    SavingsAccount sa1("Alice", 5000.0, 0.05);
    sa1.display();
    std::cout << "Balance via getter: $" << sa1.getBalance() << "\n\n";

    std::cout << "--- Constructors & Overloading ---\n";
    SavingsAccount sa2;
    SavingsAccount sa3("Bob", 3000.0, 0.04);
    SavingsAccount sa4(sa1);
    std::cout << "Default: "; sa2.display();
    std::cout << "Parameterized: "; sa3.display();
    std::cout << "Copied from Alice: "; sa4.display();
    std::cout << "\n";

    std::cout << "--- Static Members ---\n";
    std::cout << "Total accounts created: " << Account::getTotalAccounts() << "\n";
    std::cout << "Next ID will be: " << Account::getNextId() + 1 << "\n\n";

    std::cout << "--- Deep Copy vs Shallow Copy ---\n";
    TransactionLog log1(5);
    log1.addEntry("Transfer $100").addEntry("Deposit $500");
    TransactionLog log2 = log1;
    log2.addEntry("New entry only in log2");
    std::cout << "Log1 (" << log1.getCount() << " entries):\n"; log1.display();
    std::cout << "Log2 (" << log2.getCount() << " entries):\n"; log2.display();
    std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";

    std::cout << "--- Friend Class & Functions ---\n";
    Auditor auditor;
    auditor.inspectAccount(sa1);
    auditor.auditBalance(sa3);
    displayAccountSecret(sa1);
    JuniorAuditor junior;
    junior.tryInspect(sa1);
    std::cout << "\n";

    std::cout << "--- Polymorphism & Virtual Functions ---\n";
    CurrentAccount ca1("Charlie", 10000.0, 2000.0);
    SeniorSavingsAccount ssa1("Diana", 20000.0, 0.06, 0.02);
    Account* accounts[] = { &sa1, &ca1, &ssa1 };
    for (int i = 0; i < 3; i++) {
        std::cout << accounts[i]->getAccountType()
                  << " - Interest: $" << accounts[i]->calculateInterest() << "\n";
    }
    std::cout << "\n";

    std::cout << "--- Multiple Inheritance ---\n";
    PremiumAccount pa1("Eve", 50000.0, 0.05, 100000.0);
    pa1.display();
    std::cout << "Insurance Premium: $" << pa1.getInsurancePremium() << "\n";
    std::cout << "Insurance Type: " << pa1.getInsuranceType() << "\n";
    std::cout << "Interest: $" << pa1.calculateInterest() << "\n\n";

    std::cout << "--- Function Overloading ---\n";
    sa1.deposit(1000.0);
    sa1.deposit(500.0, "Birthday gift");
    sa1.display();
    sa1.display(true);
    std::cout << "\n";

    std::cout << "--- Exception Handling ---\n";
    try {
        sa1.withdraw(999999.0);
    } catch (const InsufficientFundsException& e) {
        std::cout << "Caught: " << e.what()
                  << " (Requested: $" << e.getRequested()
                  << ", Available: $" << e.getAvailable() << ")\n";
    }
    try {
        sa1.deposit(-100.0);
    } catch (const InvalidAmountException& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
    try {
        SavingsAccount bad("Test", -500.0, 0.05);
    } catch (const InvalidAmountException& e) {
        std::cout << "Constructor exception: " << e.what() << "\n";
    }
    std::cout << "\n";

    return 0;
}

Your banking system now handles bad input gracefully and offers flexible interfaces through overloading. Next up, you will add file persistence so account data survives between program runs.

Implement File Handling and I/O Redirection

Your banking system works great while it is running, but the moment you close the program, all account data disappears. Real banking systems persist data to files so it survives between sessions.

In this step, you will write account data to files using C++ file streams, read that data back, and use I/O redirection to generate a formatted bank statement as a text file. These techniques give your program permanent memory on disk.

In this step, get ready to:
  • Save account data to a file using std::ofstream with different open modes.
  • Read file data back with std::ifstream and append to a transaction log.
  • Redirect stdout to a file using std::freopen to generate a bank statement.
Save account data to a file with std::ofstream

File streams in C++ work like std::cout but direct output to a file on disk instead of the terminal. The <fstream> header (already included in Account.h) provides std::ofstream for writing and std::ifstream for reading.

  • In main.cpp, find the return 0; line at the very bottom of main().
  • Add the following code above return 0; to write four accounts to a file:
    // --- Step 11: File Handling ---
    std::cout << "--- File Handling ---\n";
    {
        std::ofstream outFile("accounts.txt", std::ios::out | std::ios::trunc);
        if (outFile.is_open()) {
            sa1.saveToFile(outFile);
            ca1.saveToFile(outFile);
            ssa1.saveToFile(outFile);
            pa1.saveToFile(outFile);
            outFile.close();
            std::cout << "Accounts saved to accounts.txt\n";
        }
    }

What does this code do?

This block opens a file called accounts.txt for writing. The open mode std::ios::out | std::ios::trunc means "open for output and discard any existing contents".

  • std::ios::out opens the file for writing.
  • std::ios::trunc truncates (clears) any existing file contents so you start fresh each run.
  • is_open() checks that the file opened successfully before writing.
  • The curly braces {} create a scope block. The outFile variable is destroyed at the closing brace, keeping it separate from other file operations.

Do not save yet. You will add more file handling code in the next chunk before compiling.

Append a session log and read data back

Writing to a file with trunc mode overwrites everything. Sometimes you want to add to an existing file without erasing it. The std::ios::app (append) mode solves this by writing new data at the end.

  • Still in main.cpp, add this code directly below the file-write block you just added (still above return 0;):
    // Append transaction log
    {
        std::ofstream logFile("transactions.log", std::ios::app);
        if (logFile.is_open()) {
            logFile << "=== Session Log ===\n";
            logFile << "Total accounts: " << Account::getTotalAccounts() << "\n";
            logFile.close();
            std::cout << "Transaction log appended to transactions.log\n";
        }
    }

    // Read back
    {
        std::ifstream inFile("accounts.txt", std::ios::in);
        if (inFile.is_open()) {
            std::cout << "Reading from file:\n";
            std::string line;
            while (std::getline(inFile, line)) {
                std::cout << "  " << line << "\n";
            }
            inFile.close();
        }
    }
    std::cout << "\n";

What does this code do?

This adds two more file operations that demonstrate the other side of file I/O.

  • The std::ios::app mode opens transactions.log for appending. Each time you run the program, a new session entry is added without erasing previous ones.
  • std::ifstream with std::ios::in opens accounts.txt for reading. The std::getline loop reads each line and prints it to the console.
  • Every file operation checks is_open() first and calls close() when done. This prevents data corruption from writing to a failed handle.
  • Save main.cpp.
  • Compile and run your program by running this command in your terminal:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank

Near the bottom of the output, you should see "Accounts saved to accounts.txt", "Transaction log appended to transactions.log", and then the file contents printed back line by line.

  • Confirm the files were created on disk by running:
ls accounts.txt transactions.log

You should see both filenames listed in the output.

Don't see the files?

  • Make sure you are running ./bank from inside your banking-system folder. Files are created relative to where you run the command.
  • Check for typos in the filename strings. The file names must match exactly: "accounts.txt" and "transactions.log".
  • If you see compilation errors, make sure both scoped blocks are placed after the exception handling section and before return 0;.

Help me debug my file handling code.

Redirect stdout to generate a bank statement

C++ can redirect where stdout points so that all output goes to a file instead of the terminal. The std::freopen function (from <cstdio>) swaps the destination of a standard stream at runtime.

  • In main.cpp, add the following I/O redirection code directly below the std::cout << "\n"; line you just added (still above return 0;):
    // --- Step 12: I/O Redirection ---
    std::cout << "--- I/O Redirection ---\n";
    std::cout << "Generating bank statement to file...\n";

    // Redirect stdout to file
    std::freopen("bank_statement.txt", "w", stdout);
    std::printf("=== BANK STATEMENT ===\n");
    std::printf("Account: %s (#%d)\n", sa1.getHolderName().c_str(), sa1.getAccountNumber());
    std::printf("Balance: $%.2f\n", sa1.getBalance());
    std::printf("Interest Rate: %.1f%%\n", sa1.getInterestRate() * 100);
    std::printf("Monthly Interest: $%.2f\n", sa1.calculateInterest());
    std::printf("======================\n");
    std::fclose(stdout);

    // Reopen stdout to terminal (macOS specific)
    std::freopen("/dev/tty", "w", stdout);
    std::cout << "Bank statement written to bank_statement.txt\n";
    std::cout << "\n========================================\n";
    std::cout << "    Banking System Complete!\n";
    std::cout << "========================================\n";

What does this code do?

This demonstrates I/O redirection at the system level.

  • std::freopen("bank_statement.txt", "w", stdout) redirects the standard output stream to a file. After this call, every std::printf and std::cout writes to bank_statement.txt instead of the terminal.
  • std::fclose(stdout) flushes and closes the redirected file to ensure all data is written.
  • std::freopen("/dev/tty", "w", stdout) reopens stdout pointing back at the terminal. On macOS, /dev/tty is the special device file that represents the user's terminal.
  • After reopening, std::cout works normally again and prints the confirmation message to your screen.

Why use std::printf instead of std::cout?

Both work after redirection. This code uses std::printf to demonstrate that the redirection affects ALL standard output, not just std::cout. The format specifiers (%.2f, %s) also make it easy to format currency values with exactly two decimal places.

  • Save main.cpp.
  • Compile and run the program:
clang++ -std=c++17 -o bank Account.cpp main.cpp && ./bank

At the very end of the terminal output, you should see "Bank statement written to bank_statement.txt".

  • View the generated bank statement by running:
cat bank_statement.txt

You should see a formatted bank statement with Alice's account number, balance, interest rate, and monthly interest amount.

Seeing garbled output or no file?

  • If the terminal output cuts off after "Generating bank statement to file...", make sure you have the std::freopen("/dev/tty", "w", stdout) line after std::fclose(stdout). Without it, stdout stays redirected and nothing prints to the screen.
  • If bank_statement.txt is empty, check that std::fclose(stdout) is called before reopening. This flushes the buffer to disk.
  • If you see a compilation error about getInterestRate(), confirm that sa1 is declared as a SavingsAccount (not a base Account pointer).

Help me debug my I/O redirection.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.cpp and that all three output files (accounts.txt, transactions.log, bank_statement.txt) exist after running.

ⓧ I'd like to double check the full code

Here is the complete main.cpp file with all file handling and I/O redirection code included:

#include "Account.h"
#include <vector>
#include <memory>

int main() {
    std::cout << "========================================\n";
    std::cout << "    C++ OOP Banking System\n";
    std::cout << "========================================\n\n";

    std::cout << "--- Classes & Encapsulation ---\n";
    SavingsAccount sa1("Alice", 5000.0, 0.05);
    sa1.display();
    std::cout << "Balance via getter: $" << sa1.getBalance() << "\n\n";

    std::cout << "--- Constructors & Overloading ---\n";
    SavingsAccount sa2;
    SavingsAccount sa3("Bob", 3000.0, 0.04);
    SavingsAccount sa4(sa1);
    std::cout << "Default: "; sa2.display();
    std::cout << "Parameterized: "; sa3.display();
    std::cout << "Copied from Alice: "; sa4.display();
    std::cout << "\n";

    std::cout << "--- Static Members ---\n";
    std::cout << "Total accounts created: " << Account::getTotalAccounts() << "\n";
    std::cout << "Next ID will be: " << Account::getNextId() + 1 << "\n\n";

    std::cout << "--- Deep Copy vs Shallow Copy ---\n";
    TransactionLog log1(5);
    log1.addEntry("Transfer $100").addEntry("Deposit $500");
    TransactionLog log2 = log1;
    log2.addEntry("New entry only in log2");
    std::cout << "Log1 (" << log1.getCount() << " entries):\n"; log1.display();
    std::cout << "Log2 (" << log2.getCount() << " entries):\n"; log2.display();
    std::cout << "(Deep copy: log2 has extra entry, log1 unaffected)\n\n";

    std::cout << "--- Friend Class & Functions ---\n";
    Auditor auditor;
    auditor.inspectAccount(sa1);
    auditor.auditBalance(sa3);
    displayAccountSecret(sa1);
    JuniorAuditor junior;
    junior.tryInspect(sa1);
    std::cout << "\n";

    std::cout << "--- Polymorphism & Virtual Functions ---\n";
    CurrentAccount ca1("Charlie", 10000.0, 2000.0);
    SeniorSavingsAccount ssa1("Diana", 20000.0, 0.06, 0.02);
    Account* accounts[] = { &sa1, &ca1, &ssa1 };
    for (int i = 0; i < 3; i++) {
        std::cout << accounts[i]->getAccountType()
                  << " - Interest: $" << accounts[i]->calculateInterest() << "\n";
    }
    std::cout << "\n";

    std::cout << "--- Multiple Inheritance ---\n";
    PremiumAccount pa1("Eve", 50000.0, 0.05, 100000.0);
    pa1.display();
    std::cout << "Insurance Premium: $" << pa1.getInsurancePremium() << "\n";
    std::cout << "Insurance Type: " << pa1.getInsuranceType() << "\n";
    std::cout << "Interest: $" << pa1.calculateInterest() << "\n\n";

    std::cout << "--- Function Overloading ---\n";
    sa1.deposit(1000.0);
    sa1.deposit(500.0, "Birthday gift");
    sa1.display();
    sa1.display(true);
    std::cout << "\n";

    std::cout << "--- Exception Handling ---\n";
    try {
        sa1.withdraw(999999.0);
    } catch (const InsufficientFundsException& e) {
        std::cout << "Caught: " << e.what()
                  << " (Requested: $" << e.getRequested()
                  << ", Available: $" << e.getAvailable() << ")\n";
    }
    try {
        sa1.deposit(-100.0);
    } catch (const InvalidAmountException& e) {
        std::cout << "Caught: " << e.what() << "\n";
    }
    try {
        SavingsAccount bad("Test", -500.0, 0.05);
    } catch (const InvalidAmountException& e) {
        std::cout << "Constructor exception: " << e.what() << "\n";
    }
    std::cout << "\n";

    // --- Step 11: File Handling ---
    std::cout << "--- File Handling ---\n";
    {
        std::ofstream outFile("accounts.txt", std::ios::out | std::ios::trunc);
        if (outFile.is_open()) {
            sa1.saveToFile(outFile);
            ca1.saveToFile(outFile);
            ssa1.saveToFile(outFile);
            pa1.saveToFile(outFile);
            outFile.close();
            std::cout << "Accounts saved to accounts.txt\n";
        }
    }
    {
        std::ofstream logFile("transactions.log", std::ios::app);
        if (logFile.is_open()) {
            logFile << "=== Session Log ===\n";
            logFile << "Total accounts: " << Account::getTotalAccounts() << "\n";
            logFile.close();
            std::cout << "Transaction log appended to transactions.log\n";
        }
    }
    {
        std::ifstream inFile("accounts.txt", std::ios::in);
        if (inFile.is_open()) {
            std::cout << "Reading from file:\n";
            std::string line;
            while (std::getline(inFile, line)) {
                std::cout << "  " << line << "\n";
            }
            inFile.close();
        }
    }
    std::cout << "\n";

    // --- Step 12: I/O Redirection ---
    std::cout << "--- I/O Redirection ---\n";
    std::cout << "Generating bank statement to file...\n";

    // Redirect stdout to file
    std::freopen("bank_statement.txt", "w", stdout);
    std::printf("=== BANK STATEMENT ===\n");
    std::printf("Account: %s (#%d)\n", sa1.getHolderName().c_str(), sa1.getAccountNumber());
    std::printf("Balance: $%.2f\n", sa1.getBalance());
    std::printf("Interest Rate: %.1f%%\n", sa1.getInterestRate() * 100);
    std::printf("Monthly Interest: $%.2f\n", sa1.calculateInterest());
    std::printf("======================\n");
    std::fclose(stdout);

    // Reopen stdout to terminal (macOS specific)
    std::freopen("/dev/tty", "w", stdout);
    std::cout << "Bank statement written to bank_statement.txt\n";
    std::cout << "\n========================================\n";
    std::cout << "    Banking System Complete!\n";
    std::cout << "========================================\n";

    return 0;
}

Your banking system now persists data between runs and can generate formatted reports as files. Next up, you will tackle the Secret Mission and build a TransferService that handles inter-account transfers with rollback logic.

Secret mission

Add a TransferService with Rollback

Real banks don't just move money. They guarantee that if anything goes wrong mid-transfer, both accounts return to their original state. Can you build a TransferService that combines exception handling, polymorphism, file logging, and deep-copied state into a single atomic operation?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files on disk, or delete them entirely.

Resources you used:

  • The banking-system/ project directory on your Desktop (source files, compiled binary, and generated data files).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the banking system or extending it with new features.

  • Your source files, compiled binary, and generated data files (accounts.txt, bank_statement.txt, transactions.log, transfers.log) remain on disk.
  • Apple Clang (Xcode Command Line Tools) and VS Code remain installed on your system.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files stay on disk until you delete them.

  • No background processes are running. The compiled binary only executes when you run it manually.
  • You can recompile and run the project any time by returning to the banking-system/ directory.

Delete - I don't want to use this again

Remove all project files from your system. This deletes your source code, compiled binary, and all generated data files.

  • Delete the project directory by running this command in Terminal:

macOS

rm -rf ~/Desktop/banking-system

Windows

rmdir /s /q %USERPROFILE%\Desktop\banking-system
  • Confirm the folder is gone by running:
ls ~/Desktop/banking-system

You should see a "No such file or directory" message confirming the deletion.

Nice Work!

Nice Work!

Nice work! You just built a complete Banking System in C++ that exercises every major OOP concept from classes through polymorphism to file I/O.

Here's what you learned:

  • Built a full class hierarchy with an abstract base class, multiple derived types (SavingsAccount, CurrentAccount, PremiumAccount), and multilevel, hierarchical, and multiple inheritance.
  • Applied all four pillars of OOP: Encapsulation (private data with public interface), Abstraction (pure virtual functions), Inheritance (multilevel, hierarchical, multiple), and Polymorphism (virtual functions with runtime dispatch).
  • Persisted data with file streams, redirected I/O for logging with freopen, and built robust exception handling with custom exception classes.
  • Built a TransferService with atomic rollback logic, combining polymorphism, deep copy, exception handling, and file I/O into a single feature.

Ready to quiz yourself?