Object-Oriented Programming (OOPS): Core Concepts with a Worked C++ Example

Build one connected OOP model, then trace how two account objects respond differently through the same C++ base-class interface.

KnowledgeGate Team

Exam prep & CS education

Updated 19 Aug 20267 min read

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

balance_ is private and changes through applyMonth()

Merely making every field private

Abstraction

Expose the essential contract while suppressing implementation choice

Callers use monthlyChange() through Account

Vague "data hiding"

Inheritance

Derive a specialised type from a valid is-a relationship

SavingsAccount is an Account

Every form of reuse

Polymorphism

Let one common operation select different implementations

monthlyChange() returns 200 or -150

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.

Class diagram of an abstract Account inherited by SavingsAccount and CurrentAccount, with two objects: Asha savings and Ravi current.

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, &current};

    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) = 7850

Integer 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.

Runtime dispatch trace where one Account pointer calls SavingsAccount to reach 10200 and CurrentAccount to reach 7850.

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, &current} 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 Bank has Account objects through composition or aggregation

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

Account* -> SavingsAccount -> monthlyChange() = 200

Abstract class has no implemented methods

Abstractness is misunderstood

Account implements construction, accessors and applyMonth()

Private means derived code can never use state

Controlled access is overlooked

balance() offers protected read access without direct mutation

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:

  1. Mark the declared pointer or reference type.

  2. Mark the actual object type.

  3. Check whether the operation is virtual and overridden.

  4. Confirm access control and signature compatibility.

  5. 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.