Memorising the four OOP pillars is not enough when a question asks which method runs through a base-class pointer, or an interviewer asks why Account should be abstract while SavingsAccount is concrete. You need one connected model of classes, objects, encapsulation, abstraction, inheritance, composition, polymorphism and binding. One banking model carries all of it: an abstract Account, a SavingsAccount that adds 2 percent of its balance each month, a CurrentAccount that deducts a flat 150, and a C++ trace that ends at 10200 and 7850. For the wider map around this topic, start with Programming Languages.
Classes, objects, state, behaviour and identity
A class is a programmer-defined type or blueprint that specifies permitted state and behaviour. An object is a concrete instance with its own identity and current state. In our model, Account declares owner_, balance_, monthlyChange() and applyMonth(). The object savings has owner "Asha" and balance 10000, while current is a different object with owner "Ravi" and balance 8000.
SavingsAccount savings("Asha", 10000, 2) constructs a concrete C++ object and initialises its state. The class definition is not itself an object, and the two instances do not share their ordinary instance fields. Before any update, the objects have different identities and balances. Both support the common Account behaviour applyMonth(), but their monthly-change rules differ. Learners looking for broader programming and coding routes can explore the Coding & Skills category.
The four OOP pillars, with clear boundaries
Idea | Meaning | Account example | Not the same as |
|---|---|---|---|
Encapsulation | Bundle state with operations and control direct mutation |
| Merely making every field private |
Abstraction | Expose the essential contract while suppressing implementation choice | Callers use | Vague "data hiding" |
Inheritance | Derive a specialised type from a valid is-a relationship |
| Every form of reuse |
Polymorphism | Let one common operation select different implementations |
| Several differently named methods |
Account is abstract because monthlyChange() and kind() are pure virtual operations. It states what every supported account must provide, but it cannot be instantiated directly. SavingsAccount and CurrentAccount complete that contract.
The pillars cooperate but remain separable. Private state gives encapsulation, the base contract gives abstraction, derived classes use inheritance, and a base pointer dispatching to a derived override gives runtime polymorphism.

Worked example: one interface, two account behaviours
The program below stores base-class pointers in a vector, while each pointer still refers to its original concrete object.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Account {
private:
string owner_;
int balance_;
protected:
int balance() const { return balance_; }
public:
Account(string owner, int opening)
: owner_(owner), balance_(opening) {}
virtual int monthlyChange() const = 0;
virtual string kind() const = 0;
void applyMonth() { balance_ += monthlyChange(); }
int currentBalance() const { return balance_; }
string owner() const { return owner_; }
virtual ~Account() = default;
};
class SavingsAccount : public Account {
int ratePercent_;
public:
SavingsAccount(string owner, int opening, int rate)
: Account(owner, opening), ratePercent_(rate) {}
int monthlyChange() const override {
return balance() * ratePercent_ / 100;
}
string kind() const override { return "Savings"; }
};
class CurrentAccount : public Account {
int fee_;
public:
CurrentAccount(string owner, int opening, int fee)
: Account(owner, opening), fee_(fee) {}
int monthlyChange() const override { return -fee_; }
string kind() const override { return "Current"; }
};
int main() {
SavingsAccount savings("Asha", 10000, 2);
CurrentAccount current("Ravi", 8000, 150);
vector<Account*> accounts{&savings, ¤t};
for (Account* account : accounts) {
int before = account->currentBalance();
int change = account->monthlyChange();
account->applyMonth();
cout << account->owner() << " " << account->kind()
<< ": " << before << " + (" << change << ") = "
<< account->currentBalance() << '\n';
}
}For Asha, 10000 * 2 / 100 = 200, then 10000 + 200 = 10200. For Ravi, the fixed change is -150, so 8000 + (-150) = 7850. The output is exactly:
Asha Savings: 10000 + (200) = 10200
Ravi Current: 8000 + (-150) = 7850Integer arithmetic is exact for these values, because 10000 * 2 / 100 divides without a remainder. Real money handling needs a fixed-point or minor-unit representation instead, since int truncates any fractional paisa without warning. For structured follow-on learning in the language used here, see C++ Programming: Concepts, MCQs and Coding Questions.

Inheritance, composition and object relationships
Use the substitution test for inheritance: every SavingsAccount in this model can be used where an Account is expected because it honours the base operations. Single inheritance has one direct base, multilevel inheritance forms a chain, hierarchical inheritance gives one base several derived classes, and multiple inheritance gives a class several direct bases. Languages support these forms differently. Our model is hierarchical because two concrete classes share one base.
SavingsAccount is an Account. A Bank that stores vector<Account*> accounts{&savings, ¤t} has accounts, so Bank : Account would model the wrong relationship. Association is a general relationship. Aggregation is a whole-part link in which parts may outlive the whole. Composition usually means strong ownership with tied lifetimes, although the implementation determines actual ownership. Inheritance suits a stable, substitutable contract. Composition is often more flexible when behaviour must be assembled or replaced.
Overloading, overriding and binding time
Compile-time polymorphism and runtime polymorphism answer different questions. Overloading chooses among same-named functions with different valid parameter lists, such as print(int) and print(string), using compile-time information. Overriding supplies a derived implementation with a compatible signature, as SavingsAccount::monthlyChange() overrides the virtual base operation.
Static binding resolves a call without selecting by runtime dynamic type. Dynamic binding selects an overridden virtual method from the actual object at runtime. In Account* p = &savings;, the static pointer type is Account*, the dynamic object type is SavingsAccount, and p->monthlyChange() returns 200.
The base destructor is virtual so that deleting a derived object through an owning base pointer can run the derived destructor chain. The accounts vector here does not own anything. It only observes two stack objects. Java uses references and different syntax, but its contract, override and runtime-dispatch ideas are comparable. The Java: Concepts, MCQs and Coding Questions course provides that syntax path.
Common OOP traps and their corrections
Trap | What goes wrong | Correction |
|---|---|---|
Class = object | A type specification is confused with an instance | A class defines permitted structure; an object is concrete |
Encapsulation = abstraction | Two different boundaries are merged | Encapsulation controls state; abstraction exposes an essential contract |
Has-a should inherit | The relationship becomes false | A |
Overloading = overriding | Compile-time choice is confused with virtual replacement | Overloads differ by parameters; an override replaces a derived virtual operation |
Base pointer means base method | Dynamic type is ignored |
|
Abstract class has no implemented methods | Abstractness is misunderstood |
|
Private means derived code can never use state | Controlled access is overlooked |
|
Two code checks make the boundaries concrete. Account a("X", 1000); is invalid because Account remains abstract. Also retain override: if a signature is wrong, the compiler can report that it does not override the intended base operation. Avoid reasoning from a supposed vtable layout, because C++ does not require one exact memory representation.
How objective exams and interviews probe OOP
Objective papers and interviews reuse a small set of question shapes: distinguish class from object, classify abstraction versus encapsulation, choose is-a or has-a, identify valid construction, separate overload from override, trace a base-pointer call, or predict constructor and destructor order for given code.
Use this 60-second routine:
Mark the declared pointer or reference type.
Mark the actual object type.
Check whether the operation is virtual and overridden.
Confirm access control and signature compatibility.
Trace every state change numerically.
For Account* p = &savings, those steps give declared type Account*, dynamic type SavingsAccount, virtual result 200, and balance 10200 after applyMonth(). KnowledgeGate's question bank carries over 180 practice questions on object-oriented programming, spanning classes and objects, constructors and destructors, inheritance and abstract classes. To run the same reasoning in a language without pointers, work through Object-Oriented Programming: Classes, Objects and Inheritance with a Worked Java Example, where a Vehicle base class and an ElectricCar override finish at 1600 and 1220.
Object-oriented programming: the short version and next step
Remember five points: a class defines a type and objects carry instance state; encapsulation and abstraction solve different boundary problems; inheritance needs a valid is-a relationship; composition models has-a; virtual overriding enables runtime polymorphism. Keep the anchors Asha: 10000 + 200 = 10200, Ravi: 8000 - 150 = 7850, and Account* -> dynamic object type -> override.
Now redraw both diagrams. Add a ZeroFeeAccount whose monthlyChange() returns 0, and predict 5000 + 0 = 5000. Continue with the C++ course for this example's language, or the Java course for the same ideas in Java.




