Bank Account Program in C++: A Complete OOP Project
A bank account is the textbook example of object-oriented programming for a good reason: it has data that must never be corrupted (the balance) and rules about how it can change (you cannot withdraw more than you have). That is exactly the problem classes were invented to solve.
This project builds one from scratch and explains why each design decision matters.
Why Not Just Use Variables?
You could write this with a double balance and a couple of functions. Here is what goes wrong:
double balance = 500.0;
// ... 200 lines later, in some unrelated function ...
balance = -9999; // nothing stops this
Nothing in the language prevents any part of your program from setting the balance to nonsense. When a bug appears, every line that touches balance is a suspect.
A class fixes this by making the balance private — unreachable from outside — and offering a small set of public methods as the only doors in. If the balance is ever wrong, the bug is inside one of those methods. That is encapsulation, and it is the whole point of the exercise.
The Account Class
#include <iostream>
#include <string>
#include <iomanip>
class Account {
private:
int accountNumber;
std::string holderName;
double balance;
public:
// Constructor: an account cannot exist without these three things.
Account(int number, const std::string& name, double initialDeposit)
: accountNumber(number), holderName(name), balance(0.0) {
if (initialDeposit > 0)
balance = initialDeposit;
}
bool deposit(double amount) {
if (amount <= 0) {
std::cout << "Deposit must be positive.\n";
return false;
}
balance += amount;
return true;
}
bool withdraw(double amount) {
if (amount <= 0) {
std::cout << "Withdrawal must be positive.\n";
return false;
}
if (amount > balance) {
std::cout << "Insufficient funds. Balance is "
<< balance << ".\n";
return false;
}
balance -= amount;
return true;
}
// const: this method promises not to change the object.
double getBalance() const { return balance; }
int getNumber() const { return accountNumber; }
std::string getName() const { return holderName; }
void display() const {
std::cout << std::fixed << std::setprecision(2);
std::cout << "#" << accountNumber << " "
<< holderName << " $" << balance << "\n";
}
};
Several decisions here are worth pausing on.
The constructor takes all three values. There is no default constructor, so it is impossible to create an account with no number and no owner. The compiler enforces your rules for you. Notice the member initializer list after the colon — that initialises members directly rather than assigning to them afterwards.
deposit and withdraw return bool. The caller can check whether the operation actually happened. Returning void would leave the caller guessing.
The getters are marked const. That tells the compiler — and every reader — that calling getBalance() cannot modify the account. It also means you can call these on a const Account&, which you will need the moment you pass accounts to functions efficiently. More on this in the const keyword guide.
Using One Account
int main() {
Account acc(1001, "Ana Sharma", 500.0);
acc.display();
acc.deposit(250.0);
acc.withdraw(100.0);
acc.withdraw(10000.0); // rejected
acc.deposit(-50.0); // rejected
acc.display();
// acc.balance = 999999; // compile error — balance is private
return 0;
}
Output:
#1001 Ana Sharma $500.00
Insufficient funds. Balance is 650.00.
Deposit must be positive.
#1001 Ana Sharma $650.00
That commented-out line is the payoff. It does not fail at runtime with a wrong number — it refuses to compile. The class makes an entire category of bug impossible to write.
Managing Many Accounts
A real system holds many accounts. A vector of Account objects handles that:
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
// ... Account class from above ...
class Bank {
private:
std::vector<Account> accounts;
int nextNumber = 1001;
public:
void openAccount(const std::string& name, double initialDeposit) {
accounts.push_back(Account(nextNumber, name, initialDeposit));
std::cout << "Opened account #" << nextNumber << " for " << name << "\n";
nextNumber++;
}
// Returns nullptr if no such account — the caller must check.
Account* find(int number) {
for (Account& a : accounts)
if (a.getNumber() == number)
return &a;
return nullptr;
}
void displayAll() const {
std::cout << "\n--- All Accounts ---\n";
for (const Account& a : accounts)
a.display();
double total = 0.0;
for (const Account& a : accounts)
total += a.getBalance();
std::cout << std::fixed << std::setprecision(2);
std::cout << "Total held: $" << total << "\n";
}
};
int main() {
Bank bank;
bank.openAccount("Ana Sharma", 500.0);
bank.openAccount("Ben Okafor", 1200.0);
bank.openAccount("Cara Diaz", 50.0);
Account* acc = bank.find(1002);
if (acc != nullptr) {
acc->deposit(300.0);
acc->withdraw(150.0);
} else {
std::cout << "Account not found.\n";
}
bank.displayAll();
return 0;
}
Output:
Opened account #1001 for Ana Sharma
Opened account #1002 for Ben Okafor
Opened account #1003 for Cara Diaz
--- All Accounts ---
#1001 Ana Sharma $500.00
#1002 Ben Okafor $1350.00
#1003 Cara Diaz $50.00
Total held: $1900.00
Three details to carry into your own code:
findreturnsAccount*, notAccount. Returning by value would hand back a copy, and depositing into a copy changes nothing. The pointer refers to the real object inside the vector.- It returns
nullptrwhen nothing matches, andmainchecks before dereferencing. Skipping that check is how you get a segmentation fault. for (const Account& a : accounts)indisplayAll—constbecause we are only reading,&because copying every account on every loop iteration is wasted work.
One caution on that pointer: push_back can reallocate the vector, which would leave any previously returned Account* dangling. In a program that opens accounts while holding pointers, store std::vector<std::unique_ptr<Account>> instead, or look accounts up by number each time.
Ideas to Extend It
The class is deliberately small so you can grow it:
- Transaction history — add a
std::vector<std::string>member and append a line in eachdeposit/withdraw. - Savings and checking accounts — make
Accounta base class and use inheritance with a virtualapplyMonthlyInterest(). - Save to a file — write each account as a CSV line so balances survive between runs.
- An interactive menu — wrap
Bankin a menu loop so a user can open accounts and transfer money. - Throw exceptions instead of printing —
throw std::runtime_error("Insufficient funds")lets the caller decide how to report the problem.
Number two is the natural next step, and it is what turns this from a class exercise into a genuine OOP project.
Related Articles
- Classes and Objects in C++
- Object-Oriented Programming in C++
- Constructors and Destructors in C++
- Inheritance in C++
- Blackjack in C++ Using Classes
Take Your C++ Further
If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.