OOP Concepts explained using a simple banking example
11 Jan 2026OOP Concepts in the Account Example
This note summarizes the object-oriented programming concepts demonstrated using the simple banking example shown here. The intent is not to build a production-ready banking system, but to use a familiar domain to illustrate how core OOP ideas show up in real C++ code and at runtime. The focus is on class design and runtime behavior, with only brief mention of utilities like persistence or user prompts.
1) Encapsulation (data + behavior)
The Account
class wraps state and behavior together:
- Private data members (
m_Name,m_AccNo,m_Closed) are hidden from direct external access. - Public methods (
GetName,Deposit,Withdraw) expose controlled access.
This ensures that all invariants are enforced inside the class rather than scattered across call sites. The derived types Checking and Savings
also encapsulate their own private state:
Checkingownsm_MinimumBalanceSavingsownsm_Rate
Each class is responsible for protecting its own invariants, keeping the rules local and easy to reason about.
2) Inheritance (is-a relationships)
Checking and Savings inherit from Account:
class Checking : public Account { ... };
class Savings : public Account { ... };
This models an is-a relationship: a checking account is an account, and so is a savings account. Because inheritance is
public, a Checking or Savings object can be used anywhere an Account is expected, without the caller needing to know
the exact derived type.
3) Polymorphism (runtime dispatch)
Account declares virtual functions such as:
virtual void AccumulateInterest();virtual void Withdraw(float amount);virtual float GetInterestRate() const;
When an Account* or Account& points to a Checking or Savings instance, the overridden implementation is selected at runtime. This is classic runtime polymorphism. In this example, this shows up in Transact(Account*) and in the main loop, where accounts are stored uniformly as std::unique_ptr<Account> in main.cpp.
The calling code operates purely on the base interface while the correct behavior is dispatched dynamically.
4) Function overriding and specialization
Derived classes override base behavior to enforce account-specific rules:
Checking::Withdrawchecks the minimum balance before delegating toAccount::Withdraw.Savings::AccumulateInterestapplies its interest rate to the balance.
This demonstrates behavior specialization: common logic lives in the base class, while derived classes adjust or extend it without duplicating unrelated behavior.
5) Protected members for controlled reuse
Account keeps m_Balance in the protected section so derived classes can use it directly (for example, in
Savings::AccumulateInterest) while still hiding it from general external access.
This is a deliberate trade-off: tighter than public, but more flexible than private.
6) Static members (class-level state)
Account::s_ANGenerator is a static member that tracks the next account number. It belongs to the class rather than to any specific instance. All Account objects share this generator, and it is synchronized in the constructor and via Account::SyncAccountNumber. This is a simple example of class-level state that does not naturally belong to any single object.
7) RTTI and safe downcasting
RTTI (Run-Time Type Information) answers a specific question: given a base-class pointer or reference, what is the object’s actual dynamic type at runtime? In general, heavy reliance on RTTI is often considered a design smell, because well-designed polymorphic interfaces usually eliminate the need to ask an object what it “really is.” However, there are legitimate cases where RTTI is appropriate—typically when:
- You have a heterogeneous collection accessed uniformly via base-class pointers.
- Some behavior is type-specific, but does not naturally belong in the base-class interface.
- Adding another virtual function to the base class would be artificial or would pollute the abstraction.
This example fits that profile. Accounts are stored and processed uniformly as Account*, but certain transaction and reporting paths need to behave differently depending on whether the object is actually a Checking or Savings
account. Encoding that logic directly into Account would either complicate the interface or force derived-specific concerns into a common abstraction.
For that reason, RTTI is used selectively, at the edges of the system, rather than as a core control mechanism.
dynamic_cast in this example
dynamic_cast is used in
Transaction.cpp
and in account reporting code to safely downcast from Account* to Checking* or Savings*. The cast succeeds only if the object’s dynamic type matches the requested derived type; otherwise, it returns nullptr
(for pointer casts). This makes failure explicit and safe.
This mechanism relies on the presence of at least one virtual function in the base class, which allows the compiler to associate runtime type metadata with each object.
RTTI- and cast-related tools in C++
C++ provides several casting mechanisms, each intended for a different use case:
dynamic_cast— Runtime-checked casts across a polymorphic hierarchy.static_cast— Compile-time casts with no runtime checking; appropriate when the relationship is guaranteed by design.const_cast— Adds or removesconst/volatilequalifiers.reinterpret_cast— Low-level, implementation-defined bit reinterpretation; generally avoided in object hierarchies.typeid— Exposes runtime type information directly, often used for diagnostics or logging.
In this example, dynamic_cast is the correct tool because the code explicitly needs to ask the runtime what kind of
account it is dealing with, and do so safely.
8) Constructors and object initialization
Constructors initialize base and derived parts cleanly:
CheckingandSavingscall theAccountconstructor in their initializer lists.using Account::Account;inCheckingillustrates constructor inheritance.
This makes construction order explicit and ensures that base-class state is fully initialized before derived-class logic runs.
Passing references to non-OOP features
The example also includes persistence (saving accounts to a file) and user input prompts. These are helpful utilities, but they are intentionally kept outside the core class hierarchy so that the OOP concepts remain clear and focused.