5

Основы программирования на C++: Наследование и полиморфизм

Прежде чем читать мою статью - реши для себя, зачем ты это делаешь. Даже если ты просто нормальный человек, лишним не будет.

Если вы настоящий профессионал программирования, то зачем вы тут сидите и читайте статью на пикабу? Если вы ради интереса зашли почитать, то претензий ноль, но если вы просто захотели задушить нового пользователя Пикабу минусами, то немедленно покиньте статью, вам однозначно интересно не будет.

Здравствуйте, мои маленькие любители программирования!

Наследование в C++

Наследование — это механизм объектно-ориентированного программирования (ООП), который позволяет создавать иерархии классов, где класс-наследник (производный класс) наследует поля и методы базового класса , изменяя их область видимости. В C++ поддерживается публичное одиночное наследование , при котором производный класс может использовать публичные и защищённые члены базового класса.


Пример наследования

class A {

private:

int x;

public:

void Func1();

void Func2();

};

class B : public A {

private:

int y;

public:

void Func2(); // Переопределение функции

void Func3();

};

Основные моменты:

  • Класс B включает в себя подобъект класса A.

  • Методы и поля класса A доступны в B, за исключением приватных полей.

  • Приватное поле x из A хранится внутри объекта типа B, но недоступно напрямую.

Использование:

int main() {

B b;

b.Func1(); // Унаследовано от A

b.Func2(); // Переопределено в B

b.A::Func2(); // Версия из A

b.Func3(); // Определено в B

}


Приведение типов

Объект производного класса может быть приведён к типу базового класса . Это позволяет использовать объекты производного класса там, где ожидается объект базового класса.

void DoSomething(const A&);

int main() {

B b;

DoSomething(b); // OK

}


Жизненный цикл объектов

При создании объекта производного класса сначала вызывается конструктор базового класса, затем конструктор производного. Деструкторы вызываются в обратном порядке.

class InheritedLogger : public Logger {

public:

InheritedLogger() { std::cout << "InheritedLogger()\n"; }

~InheritedLogger() { std::cout << "~InheritedLogger()\n"; }

};

int main() {

InheritedLogger x;

}

Вывод программы:

Logger(): 1

InheritedLogger()

~InheritedLogger()

~Logger(): 1


Наследование vs Композиция

Наследование (is-a):

  • Класс-наследник является частным случаем базового класса.

  • Пример: Car является Vehicle.

Композиция (has-a):

  • Класс содержит объект другого класса как поле.

  • Пример: Car имеет Engine.

Пример композиции:

class C {

private:

A a; // Композиция

int y;

public:

void Func1() { a.Func1(); } // Обёртка

void Func2();

void Func3();

const A& GetA() const { return a; }

};


Полиморфизм

Полиморфизм позволяет переопределять поведение функций базового класса в производных классах. Для этого используются виртуальные функции .

Пример:

class Animal {

public:

virtual std::string Voice() const { return "Generic voice"; }

};

class Cat : public Animal {

public:

std::string Voice() const override { return "Meow!"; }

};

class Dog : public Animal {

public:

std::string Voice() const override { return "Woof!"; }

};

Виртуальные функции:

  • Позволяют выбирать реализацию во время выполнения (позднее связывание ).

  • Если функция объявлена как чисто виртуальная , класс становится абстрактным .

class Animal {

public:

virtual std::string Voice() const = 0; // Чисто виртуальная функция

};

  • Создать объект абстрактного класса нельзя.

  • Производные классы должны реализовать все чисто виртуальные функции.


Полиморфизм и контейнеры

Для хранения объектов разных типов в контейнере используются указатели. Однако важно учитывать управление памятью.

Пример использования указателей:

std::vector<Animal*> zoo;

zoo.push_back(new Cat("Tom"));

zoo.push_back(new Dog("Buffa"));

for (Animal* animal : zoo) {

Process(*animal); // Полиморфное поведение

delete animal; // Освобождение памяти

}

Важно:

  • Для корректного удаления объектов деструктор базового класса должен быть виртуальным .

class Animal {

public:

virtual ~Animal() {}

};


Умные указатели

Для безопасного управления памятью лучше использовать умные указатели , такие как std::unique_ptr или std::shared_ptr.

Пример с std::unique_ptr:

std::vector<std::unique_ptr<Animal>> zoo;

zoo.push_back(std::make_unique<Cat>("Tom"));

zoo.push_back(std::make_unique<Dog>("Buffa"));

for (const auto& animal : zoo) {

Process(*animal); // Полиморфное поведение

}

  • Умные указатели автоматически освобождают память при выходе из области видимости.


Итог

  1. Наследование позволяет создавать иерархии классов и переиспользовать код.

  2. Виртуальные функции обеспечивают полиморфное поведение.

  3. Для работы с полиморфными объектами в контейнерах используйте указатели или умные указатели .

  4. Абстрактные классы помогают определить общий интерфейс для производных классов.

  5. Композиция предпочтительна, когда отношение между классами выражается через "имеет", а не "является".

Эти принципы являются основой объектно-ориентированного программирования в C++ и позволяют создавать гибкие, расширяемые и безопасные программы.

Лига программистов

2K постов11.8K подписчиков

Правила сообщества

- Будьте взаимовежливы, аргументируйте критику

- Приветствуются любые посты по тематике программирования

- Если ваш пост содержит ссылки на внешние ресурсы - он должен быть самодостаточным. Вариации на тему "далее читайте в моей телеге" будут удаляться из сообщества

Вы смотрите срез комментариев. Показать все
0
Автор поста оценил этот комментарий

Наследование позволяет создавать иерархии классов и переиспользовать код.

накинь пару реальных примеров из жизни, где ты применял наследование и для чего.

раскрыть ветку (12)
2
Автор поста оценил этот комментарий

Накидываю:


Вот прямо сейчас пишу софтину, которая управляет технологическим процессом в мукомольном производстве. Там есть довольно хитроумный участок, который называется "система увлажнения зерна". (Чтобы из ситовейной машины посыпалась мука высшего сорта, важно сделать так, чтобы в вальцовую машину

засыпалось зерно строго определенной влажности. Какой именно влажности - это инженер-технолог знает. Если он ошибется, то сорт муки получится ниже. В худшем случае муку вообще придется не на хлебозавод отправлять, а отдавать почти задарма фермерам, на корм свиньям).


Так вот, что моя софтина делает:

1. Выполняет измерения разных величин (типа, влажность зерна в процентах, расход зерна в тоннах за час, и все такое прочее), с помощью различных датчиков.

2. Делает всякие вычисления с результатами этих измерений.

3. Отправляет команды разным исполнительным механизмам (типа, клапаны для регулировки подачи воды, инверторы для управления асинхронными электродвигателями, и все в таком духе.)


Проблема в том, что на одном и том же предприятии могут использоваться датчики совсем разных типов. Например, один датчик температуры подключается через интерфейс RS485, а другой требует подключения через ethernet. Датчик влажности зерна в потоке может быть емкостным, а может быть микроволновым. То есть, чтобы вычислить влажность зерна, нужно использовать разные формулы.

Инверторы разных моделей тоже могут использовать разные протоколы для работы: Одному нужен Modbus, другой использует свой протокол.


Более того, для работы с разным зерном требуются различные алгоритмы управления. Те формулы, которые отлично подходят для пшеницы твердых сортов, оказываются не очень подходящими для мягких сортов. А для ржи нужны совсем другие формулы.


И что делать программисту?


Переписывать весь софт каждый раз, когда заказчику придется поменять сломавшийся датчик, или когда зерновоз от агрохолдинга внезапно привезет другой сорт зерна?


Вот для этого и предназначено наследование с полиморфизмом.


Делаем базовый класс, называем его Датчик Температуры.

У него будет чисто виртуальная функция Измерить, которая возвращает значение температуры в градусах Цельсия.

Порождаем от него дочерние классы. Например, Термопара и Терморезистор. Перекрываем в каждом классе эту функцию, и реализуем все что нужно для вычисления температуры для датчика этого типа.


Далее, делаем базовый класс Зерно. В нем будет виртуальная функция, которая берет из параметров значения влажности (измеренную и целевую), температуру, расход зерна, и вычисляет количество воды, которое вот прямо сейчас нужно вылить в БШУ.

Потом делаем дочерние классы Пшеница, Рожь, Овес, и т.д. . И в них реализуем эту функцию отдельно, для каждого типа зерна.


Конечно, все эти порожденные классы реализуем не в теле основной программы, а в отдельных DLL.


Тогда, чтобы перейти на другое зерно, технологу нужно просто выбрать в настройках подходящий сорт зерна. И нажать кнопочку Ok. И все.


А если в будущем заказчику придется поставить новый клапан для регулировки подачи воды, про который никто и не слышал, когда софт делался?


Да фигня вопрос: Главный инженер тебе отправляет е-майл с описанием этого клапана. (Как подключить, какие командв понимает). Я просто реализую еще один класс, порожденный от класса Пропорциональный Клапан.


И отправлю ему в ответ еще одну DLL. Всё. Пусть дальше работает.


Так понятно?

раскрыть ветку (2)
1
Автор поста оценил этот комментарий

да было время, когда я со словами ООП ложился и с ними же вставал, так что конечно понятно. к тебе никаких претензий и быть не может, у тебя взрослый подход и описано понятно, в отличии от ТС. тс же просто кинул какую-то хуету на юнити просто на отъебись. зачем он эти "обучения" постит -- мне вообще непонятно.


была бы возможность я бы тебе ещё больше плюсов накинул.

раскрыть ветку (1)
2
Автор поста оценил этот комментарий
В нашей стране любой человек имеет право писать художественные произведения, мемуары, и обучающие материалы.

Но нужно учесть, что читать их не обязан никто...
Автор поста оценил этот комментарий

в паттернах проектирования

раскрыть ветку (8)
0
Автор поста оценил этот комментарий

ты считаешь, что это достаточный ответ?


паттерны проектирования и банда четырёх это всё читано-перечитано.


я просил реальные примеры из жизни, с которыми ты сталкивался.

раскрыть ветку (7)
Автор поста оценил этот комментарий

в Windows Forms часто применяется наследование

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

это понятно, там всё от окна вроде танцует, слава богу уже не помню.


ну так а твой пример пример где?

Автор поста оценил этот комментарий

мне наследование помогало избежать дублирование кода

раскрыть ветку (4)
0
Автор поста оценил этот комментарий

приведи пример. наследование чего от чего и какой код удалось "не дублировать".

раскрыть ветку (3)
Автор поста оценил этот комментарий

// Базовый класс для всех игровых объектов

class GameObject {

protected:

int x, y; // Координаты

int width, height; // Размеры

bool isActive; // Активен ли объект

public:

GameObject(int x, int y, int w, int h)

: x(x), y(y), width(w), height(h), isActive(true) {}

virtual void update() = 0; // Чисто виртуальная функция

virtual void render() = 0;

void setPosition(int newX, int newY) {

x = newX;

y = newY;

}

bool checkCollision(const GameObject& other) const {

// Проверка пересечения прямоугольников

return isActive && other.isActive &&

x < other.x + other.width &&

x + width > other.x &&

y < other.y + other.height &&

y + height > other.y;

}

virtual ~GameObject() {}

};

// Класс для персонажей

class Character : public GameObject {

protected:

int health;

int speed;

public:

Character(int x, int y, int w, int h, int hp)

: GameObject(x, y, w, h), health(hp), speed(5) {}

void move(int dx, int dy) {

if (!isActive) return;

x += dx * speed;

y += dy * speed;

}

virtual void takeDamage(int amount) {

health -= amount;

if (health <= 0) {

isActive = false;

}

}

};

// Класс игрока

class Player : public Character {

private:

int score;

public:

Player(int x, int y)

: Character(x, y, 50, 50, 100), score(0) {}

void update() override {

// Обработка ввода игрока

// Движение, стрельба и т.д.

}

void render() override {

if (isActive) {

// Отрисовка игрока

}

}

void addScore(int points) {

score += points;

}

};

// Класс врага

class Enemy : public Character {

private:

int attackPower;

public:

Enemy(int x, int y)

: Character(x, y, 40, 40, 50), attackPower(10) {}

void update() override {

// Логика ИИ врага

}

void render() override {

if (isActive) {

// Отрисовка врага

}

}

void attack(Player& player) {

if (checkCollision(player)) {

player.takeDamage(attackPower);

}

}

}; В этом примере мы избежали дублирования следующего кода:

Поля и методы, общие для всех игровых объектов (координаты, размеры, проверка коллизий):

int x, y;

int width, height;

bool isActive;

bool checkCollision(const GameObject& other);

Этот код находится в базовом классе GameObject и используется всеми игровыми объектами.

Общая логика для всех персонажей (здоровье, скорость, движение, получение урона):

int health;

int speed;

void move(int dx, int dy);

void takeDamage(int amount);

Эта логика находится в классе Character и наследуется как игроком, так и врагами.

Без наследования нам пришлось бы копировать этот код в каждый класс игрового объекта, что привело бы к:

Увеличению объема кода

Сложности внесения изменений (пришлось бы менять один и тот же код во многих местах)

Рискам появления ошибок из-за несогласованных изменений

Трудностям поддержки и расширения кода

раскрыть ветку (2)
0
Автор поста оценил этот комментарий

бля ну это какая-то средненькая обучалка по юнити, таких вагон и маленькая тележка насобирается. жаль, не буду смотреть, форматирования нет, а ради пятиднеевного аккаунта самому форматировать ну такое, не стоит напрягаться.


тоже самое у тебя будет с остальными постами.

нет форматирования -- нах никому не нужно.

там же хер что поймёшь, учитель!


всего наилучшего!

раскрыть ветку (1)
Автор поста оценил этот комментарий

ну не смотри, и тебе всего хорошего

Вы смотрите срез комментариев. Чтобы написать комментарий, перейдите к общему списку