C++ Полиморфизм

ПолиморфизмБуквально это означает множество форм.

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

В C++ полиморфизм является одной из важных особенностей объектно-ориентированного программирования.

Полиморфизм в C++ позволяет использовать указатели или ссылки на базовый класс для вызова переопределённых методов производных классов, так что один и тот же интерфейс может демонстрировать различное поведение.

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

Вот несколько ключевых моментов о полиморфизме:

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

  • Объявление функции в базовом классе как виртуальной использует ключевое словоvirtual。
  • Производный класс может переопределить эту виртуальную функцию.
  • При вызове виртуальной функции то, какая версия функции будет вызвана, определяется фактическим типом объекта.

Динамическое связывание:

  • Также известно как позднее связывание: конкретная реализация вызова функции определяется во время выполнения.
  • Для вызова виртуальных функций требуется использовать указатель или ссылку на базовый класс; во время выполнения компилятор решает, какую функцию вызвать, основываясь на фактическом типе объекта.

Чистые виртуальные функции:

  • Класс, содержащий чистые виртуальные функции, называется абстрактным классом, и его нельзя напрямую создавать как экземпляр.
  • Чисто виртуальная функция не имеет тела функции; при объявлении она использует ключевое слово= 0。
  • Она заставляет производные классы предоставлять конкретные реализации.

Механизм реализации полиморфизма:

  • Таблица виртуальных функций (V-Table): среда выполнения C++ использует таблицу виртуальных функций для реализации полиморфизма. Каждый класс, содержащий виртуальные функции, имеет таблицу виртуальных функций, которая хранит указатели на все виртуальные функции класса.
  • Указатель на виртуальную функцию (V-Ptr): объект содержит указатель на таблицу виртуальных функций класса.

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

  • Повторное использование кодаЧерез указатели или ссылки на базовый класс можно работать с объектами различных типов производных классов, обеспечивая повторное использование кода.
  • РасширяемостьПри добавлении нового производного класса код, зависящий от базового класса, не нужно изменять; достаточно лишь убедиться, что новый класс правильно переопределяет виртуальные функции.
  • Ослабление связанностиПолиморфизм позволяет сделать проектирование программы более модульным и снижает связанность между классами.

Примечания:

  • Полиморфизм возникает только тогда, когда виртуальная функция вызывается через указатель или ссылку на базовый класс.
  • Если объект производного класса используется напрямую для вызова функции, то вызывается версия из производного класса, а не версия из базового класса.
  • Полиморфизм требует информации о типах во время выполнения (RTTI), что может увеличить накладные расходы программы.

Пример 1

Давайте рассмотрим применение полиморфизма на простом примере:

Пример 1

#include <iostream>
using namespace std;

// Базовый класс Animal
class Animal {
public:
    // Виртуальная функция sound, предоставляющая интерфейс для разных животных, чтобы издавать звуки
    virtual void sound() const {
        cout << "Животное издаёт звук" << endl;
    }
   
    // Виртуальный деструктор гарантирует, что объекты производного класса корректно уничтожаются
    virtual ~Animal() {
        cout << "Животное уничтожено" << endl;
    }
};

// Производный класс Dog, наследующий Animal
class Dog : public Animal {
public:
    // Переопределение метода sound
    void sound() const override {
        cout << "Собака лает" << endl;
    }
   
    ~Dog() {
        cout << "Собака уничтожена" << endl;
    }
};

// Производный класс Cat, наследующий от Animal
class Cat : public Animal {
public:
    // Переопределение метода sound
    void sound() const override {
        cout << "Кошка мяукает" << endl;
    }
   
    ~Cat() {
        cout << "Кошка уничтожена" << endl;
    }
};

// Проверка полиморфизма
int main() {
    Animal* animalPtr;  // Указатель на базовый класс

    // Создать объект Dog и указать на него с помощью указателя Animal
    animalPtr = new Dog();
    animalPtr->sound();  // Вызов метода sound объекта Dog
    delete animalPtr;    // Освобождение памяти, вызов деструкторов Dog и Animal

    // Создать объект Cat и указать на него указателем Animal
    animalPtr = new Cat();
    animalPtr->sound();  // Вызвать метод sound класса Cat
    delete animalPtr;    // Освобождение памяти, вызов деструкторов Cat и Animal

    return 0;
}

Вывод выполнения программы:

Dog barks
Dog destroyed
Animal destroyed
Cat meows
Cat destroyed
Animal destroyed

Объяснение кода

базовый классAnimal:

  • AnimalКласс определяет виртуальную функциюsound(), которая является виртуальной функцией (virtual), используемой для представления поведения животных, издающих звуки.
  • ~Animal()Наличие виртуального деструктора гарантирует, что когда объект производного класса, на который указывает указатель базового класса, освобождается, деструктор производного класса вызывается правильно, предотвращая утечки памяти.

производный классDogиCat:

  • DogиCatВсе классы наследуют отAnimalклассы наследуются от него, и каждый реализуетsound()метод.
  • Dogизsound()Выводит "Dog barks";Catизsound()Выводит "Cat meows". Это заставляет один и тот же метод (sound()) демонстрировать разное поведение в разных классах.

функция mainmain():

  • Создайте указатель на базовый классanimalPtr。
  • Использованиеnew Dog()СоздайтеDogобъект, присвойте его адресanimalPtr. На этом этапе вызовanimalPtr->sound()Он выведет "Dog barks", потому чтоanimalPtrфактически указывает наDogОбъект.
  • освободитьDogкогда объект, сначала вызватьDogдеструктор, затем вызватьAnimalдеструктор для.
  • Использованиеnew Cat()СоздайтеCatобъект и присвоитьanimalPtr, затем вызватьanimalPtr->sound(), выводит "Cat meows", демонстрируя полиморфное поведение.

Ключевые концепции

  • виртуальная функция: Используя в базовом классеvirtualключевое слово для объявления виртуальных функций, производные классы могут переопределить эту функцию, чтобы во время выполнения вызывалась правильная функция в зависимости от типа объекта.

  • динамическое связывание: Полиморфизм C++ реализуется через динамическое связывание. Во время выполнения указатель базового классаanimalPtrбудет, исходя из фактического типа объекта, на который он указывает (DogилиCat) вызывать соответствующуюsound()метод.

  • виртуальный деструкторВ базовом классе с полиморфным поведением деструктор должен быть объявлен какvirtual, чтобы гарантировать, что при удалении объекта производного класса вызывается деструктор производного класса, предотвращая утечки ресурсов.


Пример 2

В приведённом ниже примере мы реализуем общий базовый класс Shape и два производных класса, Rectangle и Triangle, посредством полиморфизма.

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

Пример 2

#include <iostream> using namespace std; // Базовый класс Shape, представляющий фигуру class Shape { protected: int width, height; // Ширина и высота public: // Конструктор с параметрами по умолчанию Shape(int a = 0, int b = 0) : width(a), height(b) { } // Виртуальная функция area, используемая для вычисления площади // Использование ключевого слова virtual для реализации полиморфизма virtual int area() { cout << "Площадь класса Shape: " << endl; return 0; } }; // Производный класс Rectangle, представляет прямоугольник class Rectangle : public Shape { public: // Конструктор, использует конструктор базового класса для инициализации width и height Rectangle(int a = 0, int b = 0) : Shape(a, b) { } // Переопределить функцию area для вычисления площади прямоугольника int area() override { cout << "Площадь класса Rectangle: " << endl; return width * height; } }; // Производный класс Triangle, представляет треугольник class Triangle : public Shape { public: // Конструктор, использует конструктор базового класса для инициализации width и height Triangle(int a = 0, int b = 0) : Shape(a, b) { } // Переопределить функцию area для вычисления площади треугольника int area() override { cout << "Площадь класса Triangle: " << endl; return (width * height / 2); } }; // Главная функция int main() { Shape *shape; // Указатель на базовый класс Rectangle rec(10, 7); // Объект Rectangle Triangle tri(10, 5); // Объект Triangle // Направьте указатель базового класса на объект прямоугольника и вызовите функцию area shape = &rec; cout << "Площадь Rectangle: " << shape->area() << endl; // Направьте указатель базового класса на объект треугольника и вызовите функцию area shape = &tri; cout << "Площадь Triangle: " << shape->area() << endl; return 0; }

Когда приведенный выше код скомпилирован и выполнен, он выдает следующие результаты:

Rectangle Area: Rectangle class area: 
70
Triangle Area: Triangle class area: 
25

Анализ кода

Определение класса Shape:

  • Shapeявляется абстрактным базовым классом, который определяет виртуальную функциюarea()。area()является виртуальной функцией, используемой для вычисления площади, и используетvirtualключевое слово, чтобы в производных классах функцию можно было переопределить, тем самым достигая полиморфизма.
  • widthиheightДаprotectedсвойство, доступ к которому возможен толькоShapeв классе и его производных классах.
// 基类 Shape,表示形状
class Shape {
   protected:
      int width, height; // 宽度和高度

   public:
      // 构造函数,带有默认参数
      Shape(int a = 0, int b = 0) : width(a), height(b) { }

      // 虚函数 area,用于计算面积
      virtual int area() {
         cout << "Shape class area: " << endl;
         return 0;
      }
};

Определение класса Rectangle:

  • RectangleнаследуетShapeкласс и переопределилarea()метод, вычисляет площадь прямоугольника.
  • area()Метод используетoverrideключевое слово, указывающее, что это переопределение базового классаShapeизarea()Переопределение методов.
  • Rectangle::area()returnwidth * height, то есть площадь прямоугольника.
// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Rectangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算矩形面积
      int area() override { 
         cout << "Rectangle class area: " << endl;
         return width * height;
      }
};

Определение класса Triangle:

  • Triangleкласс также наследует отShape, и переопределяетarea()метод, используемый для вычисления площади треугольника.
  • Triangle::area()returnwidth * height / 2, это формула площади треугольника.
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
   public:
      // 构造函数,使用基类构造函数初始化 width 和 height
      Triangle(int a = 0, int b = 0) : Shape(a, b) { }

      // 重写 area 函数,计算三角形面积
      int area() override { 
         cout << "Triangle class area: " << endl;
         return (width * height / 2); 
      }
};

Полиморфное поведение в функции main:

  • Определяется указатель на базовый классshape, этот указатель может указывать на любойShapeобъект класса или объект его производных классов.
  • Сначала направьтеshapeуказатель наRectangleObjectrec, затем вызватьshape->area(). Посколькуarea()является виртуальной функцией, и в этот момент она динамически привяжется кRectangle::area(), выводя площадь прямоугольника.
  • Затем направьтеshapeуказатель наTriangleObjecttri, вызовитеshape->area()будет динамически связываться сTriangle::area(), выводящая площадь треугольника.
// 主函数
int main() {
   Shape *shape;           // 基类指针
   Rectangle rec(10, 7);   // 矩形对象
   Triangle tri(10, 5);    // 三角形对象

   // 将基类指针指向矩形对象,并调用 area 函数
   shape = &rec;
   cout << "Rectangle Area: " << shape->area() << endl;

   // 将基类指针指向三角形对象,并调用 area 函数
   shape = &tri;
   cout << "Triangle Area: " << shape->area() << endl;

   return 0;
}

Ключевые концепции

  • виртуальная функция: в базовом классеShapeвиртуальные функции определяются вarea(). Роль виртуальной функции заключается в том, чтобы позволить производным классам переопределить эту функцию и во время выполнения вызывать соответствующую функцию в зависимости от типа объекта, на который указывает указатель.

  • динамическое связывание: потому чтоarea()является виртуальной функцией,shape->area()при вызове, во время выполнения, в зависимости отshapeфактического типа объекта, на который он указывает (RectangleилиTriangle) чтобы вызвать соответствующуюarea()реализацию. Этот механизм определения, какую функцию вызвать во время выполнения, называется динамическим связыванием и является основой полиморфизма.

  • полиморфизм указателей базового класса: указатель базового классаshapeможет указывать на любой объект, производный отShapeобъекта. Когдаshapeпри указании на объекты разных производных классов, вызовshape->area()будет давать различное поведение, что отражает характеристики полиморфизма.

  • виртуальная функция

    Виртуальная функция— это использование ключевого слова в базовом классеvirtualдля объявления функции.

    Виртуальная функцияпозволяет подклассам переопределять её, так что во время выполнения переопределённая версия подкласса вызывается через указатель или ссылку на базовый класс, реализуя динамическое связывание.

    Мы хотим, чтобы в любой точке программы вызываемая функция могла выбираться в соответствии с типом вызываемого объекта. Эта операция называетсядинамическое связывание, илипозднее связывание.

    Возможности:

    • может иметь реализацию в базовом классе. Обычно виртуальные функции предоставляют реализацию по умолчанию в базовом классе, но производные классы могут по желанию переопределять их.
    • динамическое связывание: во время выполнения вызывать соответствующую версию функции на основе фактического типа объекта.
    • Необязательное переопределениеПроизводные классы могут при желании переопределять виртуальные функции, но это не обязательно.

    Пример

    #include <iostream>
    using namespace std;

    class Animal {
    public:
        virtual void sound() {  // Виртуальная функция
            cout << "Животное издаёт звук" << endl;
        }
    };

    class Dog : public Animal {
    public:
        void sound() override {  // Переопределение виртуальной функции
            cout << "Собака лает" << endl;
        }
    };

    int main() {
        Animal *animal = new Dog();
        animal->sound();  // Вывод: Dog barks
        delete animal;
    }

    В приведённом выше коде sound — это виртуальная функция класса Animal. При вызове sound() через указатель Animal* animal программа выбирает Dog::sound() в зависимости от фактического типа объекта (Dog).

    Чисто виртуальная функция

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

    Чисто виртуальная функция — это виртуальная функция без реализации; в базовом классе она объявляется с помощью= 0, чтобы объявить её.

    Чисто виртуальная функция означает, что базовый класс определяет интерфейс, но конкретная реализация возлагается на производный класс.

    Чисто виртуальные функции делают базовый класс абстрактным классом, который нельзя инстанцировать.

    Возможности:

    • должна быть объявлена в базовом классе как= 0, указывая на отсутствие реализации, а производные классы должны её переопределить.
    • Абстрактный класс: Класс, содержащий чистые виртуальные функции, не может быть создан напрямую; объект может быть создан только тогда, когда все чистые виртуальные функции реализованы производным классом.
    • Определение интерфейсаЧистые виртуальные функции обычно используются для определения интерфейсов, позволяя производным классам реализовывать конкретное поведение.

    Мы можем переписать виртуальную функцию area() в базовом классе следующим образом:

    #include <iostream> using namespace std; class Shape { public: virtual int area() = 0; // Чистая виртуальная функция, заставляет подклассы реализовать этот метод }; class Rectangle : public Shape { private: int width, height; public: Rectangle(int w, int h) : width(w), height(h) { } int area() override { // Реализация чистой виртуальной функции return width * height; } }; int main() { Shape *shape = new Rectangle(10, 5); cout << "Площадь прямоугольника: " << shape->area() << endl; // Вывод: Площадь прямоугольника: 50 delete shape; }

    = 0сообщает компилятору, что функция не имеет тела; вышеуказанная виртуальная функция являетсячистой виртуальной функцией。

    Сравнение виртуальных функций и чистых виртуальных функций

    ОсобенностиВиртуальная функцияЧистая виртуальная функция
    определениеИспользуется в базовом классеvirtualОбъявление с реализациейИспользуется в базовом классе= 0Объявление без реализации
    Переопределяется в производном классеПодклассы могут переопределить по своему выборуПодкласс должен реализовать
    Абстрактностьможет создать экземпляр классаделает класс абстрактным классом, экземпляр которого нельзя создать
    Назначениепредоставляет поведение по умолчанию, позволяя подклассам переопределять егоопределяет интерфейс, вынуждая производные классы реализовывать конкретные модели поведения
    Другие расширения