OOP · The Four Pillars
Inheritance
Build on what already exists — but only when the sentence is true.
Sign in to track your score
What you'll learn
- Decide whether two classes should be linked by inheritance at all.
- Predict the order constructors and destructors run in, and say why.
- Explain what virtual changes, and what breaks without it.
- Tell an abstract class apart from a pure contract, and write both.
How to use this module. Wherever you see THINK, PREDICT or SPOT THE PROBLEM, stop and answer before reading on. The answer always follows immediately — cover it with your hand if you need to. Every code sample here compiles and was run to confirm its output.
A logistics company runs three kinds of vehicle: vans, bikes, and refrigerated vans for anything that has to stay cold.
Every one of them has a registration number. Every one can start and stop. Every one gets assigned to a route in the morning.
The differences are smaller than the similarities. A van carries more packages. A bike fits down alleys a van cannot. A refrigerated van is a van that also holds a temperature.
Now think about writing this in code. Three classes, each needing vehicleId, start(), stop(), assignRoute().
Think — You could copy that shared code into all three classes. It would work.
Six months later, a bug turns up in
stop(). What happens next?
You fix it three times, and you find out the hard way whether you remembered all three.
There is another way. Instead of every class starting from nothing, one class can build on another — starting with everything that class already has, then adding its own parts.
That idea is called inheritance, and it comes with two names worth learning straight away:
- The class being built on is the base class.
- The new class built from it is the derived class.
4.1 Inheritance: building on a parent
Write the shared part once.
class DeliveryVehicle {
public:
std::string vehicleId = "V00";
void start() { std::cout << vehicleId << " starting\n"; }
void stop() { std::cout << vehicleId << " stopped\n"; }
};
class DeliveryVan : public DeliveryVehicle {
public:
int packageCapacity = 0;
void loadPackages(int n) { std::cout << "Loaded " << n << " packages\n"; }
};
The line that does the work is class DeliveryVan: public DeliveryVehicle. Read it as: a DeliveryVan starts with everything a DeliveryVehicle has.
DeliveryVan van;
van.vehicleId = "V42";
van.start(); // V42 starting
van.loadPackages(12); // Loaded 12 packages
van.stop(); // V42 stopped
What is happening: DeliveryVan never declares vehicleId, start() or stop(). It got them from DeliveryVehicle and added loadPackages() on top.
Why it matters: that bug in stop() now has exactly one place to live, and fixing it fixes every vehicle type at once.

Each derived class is the base class plus its own additions.
| Base Class | Derived Class | |
|---|---|---|
| Plain English | the class being built on | the new class built from it |
| Also called | parent, superclass | child, subclass |
| In the example | DeliveryVehicle | DeliveryVan, DeliveryBike |
| Knows about | nothing below it | everything above it |
Notice that last row. DeliveryVehicle has no idea DeliveryVan exists. The knowledge only runs upward.
The test that stops most bad inheritance
Inheritance is often sold as a way to avoid copying code. That is a benefit, but using it as the reason leads people somewhere bad.
The real question is whether one thing genuinely is a kind of the other.
Spot the problem — A delivery platform has these classes. Both need
assignRoute()and both track an ID.
DeliveryVaninheriting fromDeliveryVehicleRestaurantinheriting fromDeliveryVehicleOne of those is fine and one is a mistake. Which, and why?
Answer — Say each one out loud as a sentence.
"A DeliveryVan is a DeliveryVehicle." True. Anywhere the program expects a vehicle, a van will behave sensibly.
"A Restaurant is a DeliveryVehicle." Nonsense. A restaurant does not start, stop or drive anywhere. It happens to need a route assigned and an ID, and that is all the two have in common.
Inherit from DeliveryVehicle and Restaurant silently gains start() and stop(). Someone will eventually call them, and nobody will be able to explain what a starting restaurant means.
This sentence test has a name: the is-a relationship. If the sentence is false, inheritance is the wrong tool.
When the answer is "has a" instead
Sometimes two classes are related but not in an is-a way. A delivery van contains a GPS tracker. The van is not a tracker — it has one.
class DeliveryVan : public DeliveryVehicle {
private:
GpsTracker gps; // the van owns one
};
Putting one object inside another like this is called composition, and it gets a module of its own later. For now, one line is enough to keep them apart:
- is-a → inheritance. A DeliveryVan is a DeliveryVehicle.
- has-a → composition. A DeliveryVan has a GpsTracker.

Say the sentence before you type the colon.
4.2 super and constructor chaining
A derived object is not two objects. It is one object with a base part inside it.
That base part needs setting up too. A DeliveryVan has a vehicleId — and vehicleId belongs to DeliveryVehicle, so DeliveryVehicle is the one that should initialise it.
class DeliveryVehicle {
public:
DeliveryVehicle(std::string id) : vehicleId(id) {
std::cout << "Vehicle created\n";
}
protected:
std::string vehicleId;
};
class DeliveryVan : public DeliveryVehicle {
public:
DeliveryVan(std::string id, int capacity)
: DeliveryVehicle(id), packageCapacity(capacity) {
std::cout << "Van created\n";
}
private:
int packageCapacity;
};
Look at the part after the colon in DeliveryVan's constructor:
: DeliveryVehicle(id), packageCapacity(capacity)
That list runs before the constructor's body. DeliveryVehicle(id) says "build my base part first, and here is the argument it needs."
One constructor calling another one up the chain like this is constructor chaining.
DeliveryVan van("V42", 120);
Output:
Vehicle created
Van created
Predict — Before reading on — would you have guessed that order?
Answer — base first, always The van cannot finish building itself until the vehicle underneath it exists. C++ guarantees the base part is fully constructed before the derived constructor's body starts running.
Destruction runs the other way round: derived first, then base. The object is taken apart in the reverse order it was assembled.

About super
If you have seen Java, you have seen super — a keyword meaning "the parent class."
Watch out — C++ has no
superkeyword. Writingsuper(id)will not compile.
C++ uses the base class's actual name instead, in two situations:
To build the base part, name it in the initialisation list:
DeliveryVan(std::string id, int cap) : DeliveryVehicle(id) { }
To call the base version of a function, write BaseClass::function():
void send() override {
Notification::send(); // run the parent's version first
std::cout << "...as an email\n";
}
That second form is genuinely useful — it lets a child add to what the parent does rather than replace it entirely. Output:
Generic notification
...as an email
Quick check — A base class has only a constructor that takes two arguments. A derived class's constructor does not mention the base class at all in its initialisation list.
What happens?
Answer — It fails to compile. With nothing specified, C++ tries to call the base class's no-argument constructor — and that class does not have one. You have to name the base explicitly and pass it what it needs.
4.3 Overriding: replacing a parent's method
A notification system sends messages. Every notification can be sent, so send() belongs on the parent. But an email and an SMS are sent in completely different ways, so the parent cannot write the real send() for either of them.
The parent supplies a general version. Each child supplies its own. Replacing a parent's function with your own version is called overriding.
class Notification {
public:
virtual void send() { std::cout << "Sending notification\n"; }
virtual ~Notification() = default;
};
class EmailNotification : public Notification {
public:
void send() override { std::cout << "Sending email\n"; }
};
class SMSNotification : public Notification {
public:
void send() override { std::cout << "Sending SMS\n"; }
};
Two new words appear there.
virtualon the parent's function means: a child is allowed to replace this, and if it does, the child's version is the one that should run.overrideon the child's function means: I intend to replace a virtual function from the base class. The compiler checks that you really are — misspell the name and you get an error instead of a silent bug.
Notification* a = new EmailNotification();
Notification* b = new SMSNotification();
a->send(); // Sending email
b->send(); // Sending SMS
What is happening: both pointers have type Notification*, and both lines of calling code look identical. The version that runs is decided by the object actually sitting at the other end of the pointer.
Why it matters: code that sends notifications never has to know which kind it is holding. Add PushNotification next month and that calling code does not change at all.

What happens without virtual
Spot the problem — The same classes, with one word removed:
The object really is an
EmailNotification. What prints?
> class Notification {
> public:
> void send() { std::cout << "Sending notification\n"; }
> };
>
> class EmailNotification : public Notification {
> public:
> void send() { std::cout << "Sending email\n"; }
> };
>
> Notification* n = new EmailNotification();
> n->send();
>
Answer — **** Sending notification Without virtual, C++ decides which function to call by looking at the pointer's type, not the object's. The pointer says Notification, so the parent's version runs and the email version is ignored.
This one bites almost everybody once. The fix is a single word: mark the base function virtual.
Overriding is not overloading
These two words look similar and mean unrelated things.
| Overloading | Overriding | |
|---|---|---|
| What it is | several functions with the same name, different parameters | a child replacing a parent's function |
| Needs inheritance? | no | yes |
| Signature | must differ | must match |
| Decided | at compile time, by the arguments | at run time, by the object |
| Example | send(string) and send(string, int) | EmailNotification::send() replacing Notification::send() |
4.4 Inheritance types & the diamond problem
Inheritance chains grow into a few recognisable shapes. Office devices make a good example.
- Single — one parent, one child.
SmartPlugfromDevice. - Multilevel — a child becomes a parent itself.
Device→Printer→LaserPrinter. - Hierarchical — one parent, several children.
Device→PrinterandScanner. - Multiple — one child, more than one parent.
AllInOnefrom bothPrinterandScanner.

The first three are ordinary. The fourth is where C++ gets interesting, because C++ allows it and many languages do not.
The diamond
An all-in-one office machine prints and scans. Both Printer and Scanner are devices. So:
class Device {
public:
std::string deviceId = "D1";
};
class Printer : public Device {};
class Scanner : public Device {};
class AllInOne : public Printer, public Scanner {};
Draw the four classes and the shape is a diamond: one class at the top, two in the middle, one at the bottom joining them again.
Think —
AllInOneinheritsDevicethroughPrinter, and also inheritsDevicethroughScanner.How many
deviceIdvalues does oneAllInOneobject contain?
Answer — two AllInOne ends up carrying two complete, separate Device parts. So this line has no single answer:
AllInOne machine;
std::cout << machine.deviceId;
The compiler agrees and refuses:
error: request for member 'deviceId' is ambiguous
That is the diamond problem — the same base class reached by two different paths, duplicated instead of shared.

The fix
Tell the two middle classes to share their Device part rather than each owning one. The keyword is virtual, used here in a different sense from the virtual functions in 4.3.
class Printer : virtual public Device {};
class Scanner : virtual public Device {};
class AllInOne : public Printer, public Scanner {};
Now machine.deviceId compiles and prints D1. One Device part, reached by both paths.
That is called virtual inheritance. You will not need it often — most designs avoid the diamond entirely rather than repair it — but when you meet multiple inheritance, this is the thing to watch for.
4.5 Abstract classes: half-finished parents
A payment system handles cards, UPI and wallet balances. All three must be payable. None of them pays the same way.
So what should the parent class write inside pay()?
Nothing sensible. A generic Payment knows the operation must exist, but has no idea how any particular method carries it out. Writing a default would just be a placeholder waiting to be forgotten.
C++ lets the parent say exactly that:
class Payment {
public:
Payment(double amt) : amount(amt) {}
virtual void pay() = 0; // unfinished on purpose
void printReceipt() const {
std::cout << "Receipt for " << amount << "\n";
}
virtual ~Payment() = default;
protected:
double amount;
};
The = 0 is the new piece. It does not mean "returns zero" — it means this function is deliberately left unfinished, and any class built from me must provide it. A function marked this way is a pure virtual function, and a class containing at least one is an abstract class.
A class that is half-finished cannot be used to make objects:
Payment p(100); // error: cannot declare variable 'p' to be of abstract type
Children finish the job:
class CardPayment : public Payment {
public:
CardPayment(double amt) : Payment(amt) {}
void pay() override { std::cout << "Paying " << amount << " by card\n"; }
};
class UpiPayment : public Payment {
public:
UpiPayment(double amt) : Payment(amt) {}
void pay() override { std::cout << "Paying " << amount << " by UPI\n"; }
};

Watch out — An abstract class is not a class with nothing in it. Look again at
Payment: it has a real data member (amount), a real constructor, and a fully writtenprintReceipt(). Onlypay()is unfinished.Abstract means cannot be created directly, not empty.
CardPayment c(250);
c.pay(); // Paying 250 by card
c.printReceipt(); // Receipt for 250
Predict — A new class
CryptoPayment: public Paymentis written with a constructor, but nobody adds apay()function.Does this compile?
> CryptoPayment cp(500);
>
Answer — no CryptoPayment never provided pay(), so the pure virtual function is still unfinished and CryptoPayment is abstract too. The requirement passes down the chain until some class actually satisfies it. The compiler enforces the contract for you.
4.6 Interfaces: pure contracts
An alerting system needs to notify the on-call engineer. It does not care whether that happens by email, by Slack message, or by paging a phone. It cares about one thing: whatever it is handed must be able to send().
That is a contract — an agreement about what must be available, with nothing said about how.
Watch out — Some languages have a dedicated
interfacekeyword. C++ does not. In C+ + you build the same thing out of a class whose functions are all pure virtual.
class Notifier {
public:
virtual void send() = 0;
virtual ~Notifier() = default;
};
Notifier holds no data and implements nothing. It states a requirement and stops.
class EmailNotifier : public Notifier {
public:
void send() override { std::cout << "Sending email\n"; }
};
class SlackNotifier : public Notifier {
public:
void send() override { std::cout << "Posting to Slack\n"; }
};
Now the alerting code can be written once against the contract:
void alertTeam(Notifier& n) { n.send(); }
EmailNotifier e;
SlackNotifier s;
alertTeam(e); // Sending email
alertTeam(s); // Posting to Slack
Why it matters: alertTeam was written without knowing a single notifier type. Add a SmsNotifier next year and that function stays exactly as it is.
Abstract class or interface-like class?
Both are classes with unfinished functions. The difference is how much the parent brings with it.
| Abstract Class | Interface-Like Class | |
|---|---|---|
| Shared data | yes, often | usually none |
| Ready-made methods | yes, often | usually none |
| Unfinished methods | at least one | all of them |
| Can you create one? | no | no |
| It mainly says | "here is shared machinery, plus something you must finish" | "here is what you must provide" |
| Example | Payment — keeps amount, provides printReceipt(), leaves pay() open | Notifier — says only that send() must exist |

There is no hard line between them in C++, and no keyword marking one as different from the other. "Interface" describes how you are using the class, not a separate language feature.
Common Misconceptions
❌ Inheritance is a way to avoid writing the same code twice. Why it sounds right: that is often the first benefit anyone notices. What is true: reuse is a side effect. Inheritance says one thing is a
kind of another, and that claim has consequences — the child can be used anywhere the parent is expected. Two classes sharing a few functions is not a reason to link them.
❌ A derived class gets everything the base class has. Why it sounds right: "inherits" sounds like receiving the whole estate. What is true: it gets the public and protected members. Private members of the base are still there inside the object, but the derived class cannot touch them directly. Constructors are not inherited either.
❌ C++ has a super keyword. Why it sounds right: Java, Python and several other languages have one, and tutorials mix languages constantly. What is true: C++ uses the base class's name instead — DeliveryVehicle(id) in the initialisation list, Notification::send() to call the parent's version. super will not compile.
❌ The child constructor runs first, since that is the object you asked for. Why it sounds right: you wrote DeliveryVan van("V42", 120);, so the van feels like the starting point. What is true: the base part is built first, every time. Destruction runs in reverse — derived first, then base.
❌ Overriding and overloading are two words for the same thing. Why it sounds right: the words are nearly identical and both involve functions with the same name. What is true: overloading is several functions with different parameter lists, resolved at compile time, no inheritance needed. Overriding is a child replacing a parent's virtual function, resolved at run time by the object.
❌ Every inherited function has to be overridden. Why it sounds right: examples usually override everything, so it looks compulsory. What is true: you override only what genuinely differs. DeliveryVan uses the inherited start() and stop() unchanged. Only pure virtual functions must be provided.
❌ An abstract class cannot contain normal, finished methods. Why it sounds right: "abstract" sounds like "nothing concrete inside." What is true: an abstract class can hold data members, constructors and fully written functions. It only takes one pure virtual function to make the class abstract. Payment proves it.
❌ C++ has a separate interface keyword. Why it sounds right: Java and C# both have one, and the word gets used constantly in C++ discussions. What is true: in C++ an interface is a design idea, not a language feature. You build one from a class where every function is pure virtual.
Interview Corner
Q1. What is the difference between a base class and a derived class? The base class is the one being built on; the derived class starts with everything the base has and adds or changes things. The relationship only runs one way — the derived class knows about the base, but the base has no idea the derived class exists.
Q2. Why is "it avoids duplicate code" an incomplete reason to use inheritance? Because inheritance also claims that the child is a kind of the parent, and the rest of the program will rely on that claim. If the sentence is not true, the child inherits functions that make no sense for it. When you only want shared code, composition is usually the better tool.
Q3. What does this print, and why?
class Sensor {
public:
Sensor() { std::cout << "S"; }
~Sensor() { std::cout << "s"; }
};
class MotionSensor : public Sensor {
public:
MotionSensor() { std::cout << "M"; }
~MotionSensor() { std::cout << "m"; }
};
int main() { MotionSensor m; }
It prints SMms. Construction goes base first, then derived — the derived part cannot be built on top of something that does not exist yet. Destruction runs in the opposite order, derived then base.
Q4. Why would you mark a base function virtual before overriding it?
Player* p = new Archer();
p->attack();
Without virtual, C++ picks the function using the pointer's type, so Player::attack() runs even though the object is an Archer. With virtual, the object decides and Archer::attack() runs. The override is only honoured through a base pointer or reference if the base function is virtual.
Q5. Which of these two lines compiles?
class Vehicle {
private:
int engineHours = 0;
protected:
std::string plate = "XX-00";
};
class Truck : public Vehicle {
public:
void report() {
std::cout << plate; // A
std::cout << engineHours; // B
}
};
A compiles, B does not. plate is protected, so a derived class may use it. engineHours is private — it still exists inside every Truck object and Vehicle's own functions can reach it, but Truck cannot touch it by name. protected is exactly the level that opens a member to derived classes while keeping ordinary outside code away.
Q6. What is the diamond problem, and how does C++ handle it?
class Printer : public Device {};
class Scanner : public Device {};
class AllInOne : public Printer, public Scanner {};
AllInOne reaches Device through two paths, so it ends up with two separate Device parts and any reference to an inherited member is ambiguous. Declaring the middle classes with virtual public Device makes them share one Device part instead.
Q7. How is an abstract class different from an interface-like class in C++? An abstract class can carry shared data and fully written functions alongside at least one unfinished one — it hands the child machinery plus a requirement. An interface-like class is all pure virtual functions and no state, so it hands over only the requirement. C++ has no interface keyword; both are ordinary classes.
Q8. A DeliveryVan needs a GPS tracker. Inheritance or composition? Composition. "A DeliveryVan has a GpsTracker" is true; "a DeliveryVan is a GpsTracker" is not. Inheriting would give the van every tracker function as part of its own public surface, which is both wrong and awkward — the van should hold a tracker as a member and expose only what it actually needs.
- 1.
class Sensor { public: Sensor() { std::cout << "S"; } ~Sensor() { std::cout << "s"; } }; class MotionSensor : public Sensor { public: MotionSensor() { std::cout << "M"; } ~MotionSensor() { std::cout << "m"; } }; int main() { MotionSensor m; }What does this print?
- 2.
class Player { public: void attack() { std::cout << "Basic attack\n"; } }; class Archer : public Player { public: void attack() { std::cout << "Fires an arrow\n"; } }; int main() { Player* p = new Archer(); p->attack(); delete p; }What does this print?
- 3.
class Report { public: virtual void render() = 0; virtual ~Report() = default; }; class PdfReport : public Report { public: void render() override { std::cout << "PDF\n"; } }; int main() { Report r; r.render(); }What happens?
- 4.
The scenario
A smart home app manages devices around a house. Every device has an ID, sits in a room, and can report its status — though what "status" means differs completely between a light and a plug.
Some devices can also be put on a schedule. A light can. A plug in this first version cannot.
Design the classes. Around twenty minutes; this is about the shape of the design, not volume of code.
Work through these:
- What belongs in the shared parent, and what belongs to each specific device?
- Which function should the parent require but refuse to write?
- Should the parent be creatable on its own?
- How does each device get its ID and room set?
- How do you express "can be scheduled" when only some devices can?
- Where is overriding happening, and where is constructor chaining happening?
Hint — Two questions will get you most of the way.
Can the parent write a sensible default for this function? If not, leave it unfinished with
= 0.Do all devices have this ability, or only some? Things that all of them share belong in the parent. Something only some of them can do is a separate contract they opt into.
A possible solution
class Schedulable { // a contract, nothing else
public:
virtual void runAt(int hour) = 0;
virtual ~Schedulable() = default;
};
class SmartDevice { // abstract parent with real shared parts
public:
SmartDevice(std::string id, std::string room)
: deviceId(id), room(room) {}
virtual void status() const = 0; // every device must answer, none the same way
void identify() const { // written once, shared by all
std::cout << deviceId << " in the " << room << ": ";
}
virtual ~SmartDevice() = default;
protected:
std::string deviceId;
std::string room;
bool on = false;
};
class SmartLight : public SmartDevice, public Schedulable {
public:
SmartLight(std::string id, std::string room)
: SmartDevice(id, room) {}
void status() const override {
identify();
std::cout << (on ? "on" : "off") << " at " << brightness << "%\n";
}
void runAt(int hour) override {
on = true;
brightness = (hour >= 20) ? 30 : 100;
}
private:
int brightness = 0;
};
SmartLight takes both parents: the abstract device for what every device has, and the contract for the one thing only some devices can do. The plug takes just the device.
class SmartPlug : public SmartDevice {
public:
SmartPlug(std::string id, std::string room)
: SmartDevice(id, room) {}
void status() const override {
identify();
std::cout << (on ? "drawing power" : "idle") << "\n";
}
void toggle() { on = !on; }
};