Наследование и Is-A отношения
Изучите публичное наследование, цепь конструкторов, проблему нарезки, множественное наследование и проблему алмазов — и когда наследование уместно.
Наследственные модели - А.
Публичное наследование моделирует отношения «is-a»: Dog является Animal, SavingsAccount является BankAccount. Это регулируется принципом замены Лискова (LSP): где бы ни ожидался базовый класс, производный класс должен работать правильно без сюрпризов.
Наследование является одной из самых мощных и наиболее неправильно используемых функций ООП. Не все отношения являются «это-а». Stack не является Vector, хотя он использует его внутри. Неправильное использование наследственности там, где уместна композиция, приводит к хрупкому, трудно поддерживаемому коду.
Основное наследование и цепь конструкторов
Полученный класс наследует всех членов базового класса. Конструктор базового класса работает сначала , затем производный конструктор класса. Разрушение происходит в обратном порядке.
#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;
}Проблема срезов
Нарезка объекта происходит, когда производный объект присваивается или передается по значению переменной базового класса. Полученная часть «вырезана» — остается только базовая часть. Это тонкий и распространенный баг.
#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;
}Множественное наследование и проблема алмазов
C++ позволяет классу наследовать от нескольких оснований. Проблема алмазов возникает, когда два основания имеют общего предка, в результате чего производный класс содержит две копии данных предка. Виртуальное наследство решает эту проблему.
#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;
}Ковариантные типы возврата и защищенный доступ
Ковариантные типы возврата позволяют производному классу переопределить виртуальную функцию и вернуть более конкретный (производный) указатель или тип ссылки. Например, если Base::clone() возвращает Base*, Derived::clone() может вернуть Derived*.
Спецификатор доступа protected находится между public и private: защищенные члены доступны для самого класса и его производных классов, но не для внешнего кода. Используйте protected для членов, которые законно нуждаются в производных классах, но будьте осторожны, поскольку это создает связь между базовым и производным.
Распространенной ошибкой является использование наследования исключительно для повторного использования кода. Если Stack наследуется от std::vector, пользователи могут вызывать push_back, insert и другие векторные операции, нарушающие абстракцию стека. Наследование должно моделировать истинные отношения, удовлетворяющие принципу замены Лискова. Для повторного использования кода предпочтите композицию (Stack, которая * содержит * vector).
Всегда используйте наследование public — частное/защищенное наследование редко требуется и запутывает. Всегда объявляйте деструкторы базового класса как virtual. Передавать полиморфные объекты по ссылке или указателю , никогда по значению (чтобы избежать нарезки). Предпочитаете композицию над наследованием, если только отношения не являются действительно «я-а». Избегайте глубоких иерархий наследования (2-3 уровня обычно являются практическим пределом).
- Общественные модели наследования is-a — производный класс должен удовлетворять Лисковскому принципу замещения
- Базовые конструкторы работают первыми, деструкторы работают последними (обратный порядок строительства)
- ** Нарезка объекта происходит, когда производный объект передается по значению на базу - всегда используйте ссылки или указатели
- Бриллиантовая проблема решается с помощью наследования
virtual, но вместо этого предпочитают композицию. - Наследование — это замена, а не повторное использование кода — используйте композицию для последнего.
Тест — проверьте свои знания
(15 XP)1. Что такое объектный срез?
2. Как виртуальное наследство решает проблему алмазов?
3. Почему вы не должны наследовать от `std::vector`, чтобы создать класс `Stack`?