Přeskočit na obsah

Dědičnost a vztahy Is-A

Naučte se veřejnou dědičnost, řetězení konstruktorů, problém krájení, vícenásobnou dědičnost a problém diamantu – a kdy je dědičnost vhodná.

Modely dědičnosti Is-A

Veřejná dědičnost modeluje vztah „je-a“:DogjeAnimal,SavingsAccountjeBankAccount. To se řídí Liskovským substitučním principem (LSP): kdekoli se očekává základní třída, musí odvozená třída fungovat správně bez překvapení.

Dědičnost je jednou z nejvýkonnějších – a nejvíce zneužívaných – funkcí OOP. Ne každý vztah je „je-a“.StackneníVector, i když jej interně používá. Zneužití dědičnosti tam, kde je vhodné složení, vede ke křehkému a obtížně udržovatelnému kódu.

Základní dědičnost a řetězení konstruktorů

Odvozená třída dědí všechny členy základní třídy. nejprve se spustí konstruktor základní třídy a poté konstruktor odvozené třídy. Zničení probíhá v opačném pořadí.

inheritance_basic.cpp
#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;
}

Problém krájení

Rozřezávání objektů nastává, když je odvozený objekt přiřazen nebo předán podle hodnoty proměnné základní třídy. Odvozená část je „odříznuta“ – zůstane pouze základní část. Toto je nenápadná a běžná chyba.

slicing.cpp
#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;
}

Multiple Inheritance & the Diamond Problem

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.

diamond.cpp
#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;
}

Covariant Return Types & Protected Access

Covariant return types let a derived class override a virtual function and return a more specific (derived) pointer or reference type. For example, ifBase::clone()returnsBase*,Derived::clone()can returnDerived*.

The protected access specifier sits betweenpublicandprivate: protected members are accessible to the class itself and its derived classes, but not to outside code. Useprotectedfor members that derived classes legitimately need — but be cautious, as it creates a coupling between base and derived.

Úskalí

A common mistake is using inheritance solely to reuse code. If aStackinherits fromstd::vector, users can callpush_back,insert, and other vector operations that violate the stack abstraction. Inheritance should model a true is-a relationship satisfying the Liskov Substitution Principle. For code reuse, prefer composition (aStackthat *contains* avector).

Osvědčený postup

Always use public inheritance — private/protected inheritance is rarely needed and confusing. Always declare base class destructors as virtual. Pass polymorphic objects by reference or pointer, never by value (to avoid slicing). Prefer composition over inheritance unless the relationship is truly "is-a." Avoid deep inheritance hierarchies (2-3 levels is usually the practical limit).

Klíčové poznatky
  • Public inheritance models is-a — the derived class must satisfy the Liskov Substitution Principle
  • Base constructors run first, destructors run last (reverse order of construction)
  • Object slicing occurs when a derived object is passed by value to a base — always use references or pointers
  • problém s diamantem je vyřešen dědičnostívirtual, ale místo toho upřednostňujte kompozici
  • Dědičnost je pro zastupitelnost, ne pro opětovné použití kódu – pro druhé použití použijte složení

Kvíz — Otestujte své znalosti

(15 XP)

1. Co je krájení objektů?

2. Jak virtuální dědictví řeší diamantový problém?

3. Proč byste neměli dědit z`std::vector`, abyste vytvořili třídu`Stack`?