Inheritance & Is-A Relationships
Learn public inheritance, constructor chaining, the slicing problem, multiple inheritance, and the diamond problem — and when inheritance is appropriate.
Inheritance Models Is-A
Public inheritance models an "is-a" relationship: aDogis anAnimal, aSavingsAccountis aBankAccount. This is governed by the Liskov Substitution Principle (LSP): anywhere a base class is expected, a derived class must work correctly without surprises.
Inheritance is one of the most powerful — and most misused — features of OOP. Not every relationship is "is-a." AStackis not aVectoreven though it uses one internally. Misusing inheritance where composition is appropriate leads to fragile, hard-to-maintain code.
Basic Inheritance & Constructor Chaining
A derived class inherits all members of the base class. The base class constructor runs first, then the derived class constructor. Destruction happens in reverse order.
#include <iostream>
#include <string>
class Animal {
std::string name_;
int age_;
public:
Animal(const std::string& name, int age)
: name_{name}, age_{age}
{
std::cout << "Animal constructed: " << name_ << '\n';
}
virtual ~Animal() {
std::cout << "Animal destroyed: " << name_ << '\n';
}
const std::string& name() const { return name_; }
int age() const { return age_; }
virtual void speak() const {
std::cout << name_ << " makes a sound\n";
}
};
class Dog : public Animal { // public inheritance = is-a
std::string breed_;
public:
Dog(const std::string& name, int age, const std::string& breed)
: Animal{name, age} // base class constructor MUST be called first
, breed_{breed}
{
std::cout << "Dog constructed: " << name() << '\n';
}
~Dog() override {
std::cout << "Dog destroyed: " << name() << '\n';
}
void speak() const override {
std::cout << name() << " barks! (" << breed_ << ")\n";
}
};
int main() {
Dog rex{"Rex", 5, "German Shepherd"};
rex.speak(); // Rex barks! (German Shepherd)
// Destruction order: Dog destroyed, then Animal destroyed
return 0;
}The Slicing Problem
Tăierea obiectului are loc atunci când un obiect derivat este atribuit sau transmis după valoare unei variabile de clasă de bază. Partea derivată este „tăiată” - rămâne doar porțiunea de bază. Acesta este un bug subtil și comun.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual std::string type() const { return "Shape"; }
};
class Circle : public Shape {
double radius_;
public:
explicit Circle(double r) : radius_{r} {}
std::string type() const override { return "Circle"; }
};
// BUG: passing by value slices the object!
void print_type_bad(Shape s) {
std::cout << s.type() << '\n'; // always prints "Shape"!
}
// CORRECT: pass by reference to preserve polymorphism
void print_type_good(const Shape& s) {
std::cout << s.type() << '\n'; // prints actual type
}
int main() {
Circle c{5.0};
print_type_bad(c); // "Shape" — SLICED!
print_type_good(c); // "Circle" — correct
// Slicing also happens with containers of values:
// std::vector<Shape> shapes; // BAD: slicing on insert
// Use pointers instead:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(3.0));
std::cout << shapes[0]->type() << '\n'; // "Circle"
return 0;
}Moștenirea multiplă și problema diamantului
C++ allows a class to inherit from multiple bases. The diamond problem occurs when two bases share a common ancestor, causing the derived class to contain two copies of the ancestor's data. Virtual inheritance solves this.
#include <iostream>
// Diamond problem:
// Device
// / \
// Printer Scanner
// \ /
// AllInOne
class Device {
protected:
int id_;
public:
Device(int id) : id_{id} {
std::cout << "Device(" << id_ << ")\n";
}
virtual ~Device() = default;
};
// Virtual inheritance: only ONE copy of Device in AllInOne
class Printer : virtual public Device {
public:
Printer(int id) : Device{id} {
std::cout << "Printer\n";
}
void print_doc() { std::cout << "Printing on device " << id_ << '\n'; }
};
class Scanner : virtual public Device {
public:
Scanner(int id) : Device{id} {
std::cout << "Scanner\n";
}
void scan_doc() { std::cout << "Scanning on device " << id_ << '\n'; }
};
class AllInOne : public Printer, public Scanner {
public:
// With virtual inheritance, the most-derived class
// must initialize the virtual base directly
AllInOne(int id) : Device{id}, Printer{id}, Scanner{id} {
std::cout << "AllInOne\n";
}
};
int main() {
AllInOne aio{42};
aio.print_doc(); // Printing on device 42
aio.scan_doc(); // Scanning on device 42
// Only ONE Device sub-object exists — no ambiguity
return 0;
}Tipuri de returnare covariante și acces protejat
Tipurile returnate covariante permit unei clase derivate să suprascrie o funcție virtuală și să returneze un pointer sau un tip de referință mai specific (derivat). De exemplu, dacăBase::clone()returneazăBase*,Derived::clone()poate returnaDerived*.
Specificatorul de acces protected se află întrepublicșiprivate: membrii protejați sunt accesibili clasei în sine și claselor sale derivate, dar nu și codului extern. Utilizațiprotectedpentru membrii de care au nevoie în mod legitim clasele derivate - dar fiți precauți, deoarece creează o cuplare între bază și derivate.
O greșeală comună este utilizarea moștenirii doar pentru a reutiliza codul. Dacă unStackmoștenește de lastd::vector, utilizatorii pot apelapush_back,insertși alte operațiuni vectoriale care încalcă abstractizarea stivei. Moștenirea ar trebui să modeleze o relație adevărată este-a care să satisfacă Principiul Substituției Liskov. Pentru reutilizarea codului, preferați compoziția (unStackcare *conține* unvector).
Utilizați întotdeauna moștenirea public - moștenirea privată/protejată este rareori necesară și confuză. Declarați întotdeauna destructorii clasei de bază ca virtual. Treceți obiectele polimorfe prin referință sau indicator, niciodată după valoare (pentru a evita tăierea). Preferă compoziția față de moștenire decât dacă relația este cu adevărat „este-a”. Evitați ierarhiile profunde de moștenire (2-3 niveluri reprezintă de obicei limita practică).
- Modele de moștenire publică is-a — clasa derivată trebuie să satisfacă principiul substituției Liskov
- Constructorii de bază rulează primii, destructorii rulează ultimii (ordine inversă construcției)
- Tăierea obiectului are loc atunci când un obiect derivat este transmis prin valoare unei baze - folosiți întotdeauna referințe sau pointeri
- Problema diamantului este rezolvată cu moștenirea
virtual, dar preferați compoziția - Moștenirea este pentru substituție, nu reutilizarea codului - utilizați compoziția pentru aceasta din urmă
Test — Verifică-ți cunoștințele
(15 XP)1. Ce este tăierea obiectelor?
2. Cum rezolvă moștenirea virtuală problema diamantelor?
3. De ce NU ar trebui să moșteniți de la`std::vector`pentru a crea o clasă`Stack`?