Статья C++ для понимания принципа реализации полиморфизма.

C++
Статья C++ для понимания принципа реализации полиморфизма.

Виртуальные функции и полиморфизм

01 Виртуальная функция

  • В определении класса, которому предшествуетvirtualФункции-члены с ключевым словом называются виртуальными функциями;
  • virtualКлючевые слова используются только в объявлениях функций в определениях классов, а не при написании тел функций.
class Base 
{
    virtual int Fun() ; // 虚函数
};

int Base::Fun() // virtual 字段不用在函数体时定义
{ }

02 Выражение первого полиморфизма

  • «Указатель на производный класс» может быть назначен «указателю на базовый класс»;
  • При вызове одноименной «виртуальной функции» в базовом классе и производном классе через указатель базового класса:
    1. Если указатель указывает на объект базового класса, то вызываемый виртуальная функция базового класса;
    2. Если указатель указывает на объект производного класса, то вызывается является виртуальной функцией производного класса.

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

// 基类
class CFather 
{
public:
    virtual void Fun() { } // 虚函数
};

// 派生类
class CSon : public CFather 
{ 
public :
    virtual void Fun() { }
};

int main() 
{
    CSon son;
    CFather *p = &son;
    p->Fun(); //调用哪个虚函数取决于 p 指向哪种类型的对象
    return 0;
}

в примере вышеpОбъект указателя указывает наCSonобъект класса, поэтомуp->Fun()называетсяCSonв классеFunфункция-член.

03 Второе выражение полиморфизма

  • Объекты производных классов могут быть назначены «ссылкам» базового класса.
  • При вызове «виртуальной функции» с тем же именем в базовом классе и производном классе через ссылку на базовый класс:
    1. Если ссылка ссылается на объект базового класса, вызываемый Используйте виртуальную функцию базового класса;
    2. Если ссылка относится к объекту производного класса, то Вызывается виртуальная функция производного класса.

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

// 基类
class CFather 
{
public:
    virtual void Fun() { } // 虚函数
};

// 派生类
class CSon : public CFather 
{ 
public :
    virtual void Fun() { }
};

int main() 
{
    CSon son;
    CFather &r = son;
    r.Fun(); //调用哪个虚函数取决于 r 引用哪种类型的对象
    return 0;
}
}

в примере вышеrСсылочный объектCSonобъект класса, поэтомуr.Fun()называетсяCSonв классеFunфункция-член.

04 Простой пример полиморфизма

class A 
{
public :
    virtual void Print() { cout << "A::Print"<<endl ; }
};

// 继承A类
class B: public A 
{
public :
    virtual void Print() { cout << "B::Print" <<endl; }
};

// 继承A类
class D: public A 
{
public:
    virtual void Print() { cout << "D::Print" << endl ; }
};

// 继承B类
class E: public B 
{
    virtual void Print() { cout << "E::Print" << endl ; }
};

Соотношение между классом A, классом B, классом E и классом D выглядит следующим образом:

int main() 
{
    A a; B b; E e; D d;
    
    A * pa = &a; 
    B * pb = &b;
    D * pd = &d; 
    E * pe = &e;
    
    pa->Print();  // a.Print()被调用,输出:A::Print
    
    pa = pb;
    pa -> Print(); // b.Print()被调用,输出:B::Print
    
    pa = pd;
    pa -> Print(); // d.Print()被调用,输出:D::Print
    
    pa = pe;
    pa -> Print(); // e.Print()被调用,输出:E::Print
    
    return 0;
}

05 Полиморфизм

Использование «полиморфизма» в объектно-ориентированном программировании может повысить производительность программы.Масштабируемость, то есть когда программу нужно изменить или добавить функции, ее нужноМеньше изменений кода и дополнений.


Пример игры LOL League of Legends

Давайте на примере проектирования героя игры LOL League of Legends проиллюстрируем, почему полиморфизм может меньше менять код при модификации или добавлении функций.

LOL League of Legends — соревновательная игра 5 на 5. В игре много героев, у каждого героя есть соответствующий ему «класс», а каждый герой — «объект».

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

Вот пять героев:

  • Исследователь
  • Здание CGaren
  • Слепой монах Клисин
  • Меч Обещания Святой CYi
  • Райз CRyze

Основная мысль:

  1. Напишите для каждого класса героевAttack,FightBackа такжеHurtedфункция-член.
  • AttackФункция представляет атакующее действие;
  • FightBackФункция представляет действие контратаки;
  • HurtedФункция указывает, что он снижает собственное здоровье и показывает действие травмы.
  1. установить базовый классCHero, каждый класс героя наследуется от этого базового класса

02 Неполиморфный метод реализации

// 基类
class CHero 
{
protected:  
    int m_nPower ; //代表攻击力
    int m_nLifeValue ; //代表生命值
};


// 无极剑圣类
class CYi : public CHero 
{
public:
    // 攻击盖伦的攻击函数
    void Attack(CGaren * pGaren) 
    {
        .... // 表现攻击动作的代码
        pGaren->Hurted(m_nPower);
        pGaren->FightBack(this);
    }

    // 攻击瑞兹的攻击函数
    void Attack(CRyze * pRyze) 
    {
        .... // 表现攻击动作的代码
        pRyze->Hurted(m_nPower);
        pRyze->FightBack( this);
    }
    
    // 减少自身生命值
    void Hurted(int nPower) 
    {
        ... // 表现受伤动作的代码
        m_nLifeValue -= nPower;
    }
    
    // 反击盖伦的反击函数
    void FightBack(CGaren * pGaren) 
    {
        ....// 表现反击动作的代码
        pGaren->Hurted(m_nPower/2);
    }
    
    // 反击瑞兹的反击函数
    void FightBack(CRyze * pRyze) 
    {
        ....// 表现反击动作的代码
        pRyze->Hurted(m_nPower/2);
    }
};

Есть n видов героев,CYiВ классе будет nAttackфункции-члены и nFightBackфункция-член. То же самое верно и для других классов.

При обновлении версии игры добавлен новый герой Фрост Эш.CAshe, программа сильно меняется. Все классы должны добавить две функции-члена:

void Attack(CAshe * pAshe);
void FightBack(CAshe * pAshe);

Это много работы! ! Очень бесчеловечно, так что этот метод проектирования очень плох!

03 Реализация полиморфизма

Чтобы реализовать это полиморфным способом, вы можете знать преимущества полиморфизма, Тогда способ изменить вышеуказанные каштаны на полиморфизм выглядит следующим образом:

// 基类
class CHero 
{
public:
    virtual void Attack(CHero *pHero){}
    virtual voidFightBack(CHero *pHero){}
    virtual void Hurted(int nPower){}

protected:  
    int m_nPower ; //代表攻击力
    int m_nLifeValue ; //代表生命值
};

// 派生类 CYi:
class CYi : public CHero {
public:
    // 攻击函数
    void Attack(CHero * pHero) 
    {
        .... // 表现攻击动作的代码
        pHero->Hurted(m_nPower); // 多态
        pHero->FightBack(this);  // 多态
    }
    
    // 减少自身生命值
    void Hurted(int nPower) 
    {
        ... // 表现受伤动作的代码
        m_nLifeValue -= nPower;
    }
    
    // 反击函数
    void FightBack(CHero * pHero) 
    {
        ....// 表现反击动作的代码
        pHero->Hurted(m_nPower/2); // 多态
    }
};

Если добавляется новый герой, Ледяной ЭшCAshe, просто напишите новый классCAshe, больше не нужно добавлять специально для новых героев в существующем классе:

void Attack( CAshe * pAshe) ;
void FightBack(CAshe * pAshe) ;

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

Как использовать полиморфизм:

void CYi::Attack(CHero * pHero) 
{
    pHero->Hurted(m_nPower); // 多态
    pHero->FightBack(this);  // 多态
}

CYi yi; 
CGaren garen; 
CLeesin leesin; 
CEzreal ezreal;

yi.Attack( &garen );  //(1)
yi.Attack( &leesin ); //(2)
yi.Attack( &ezreal ); //(3)

Согласно правилам полиморфизма указанные выше (1), (2), (3) входят вCYi::Attackпосле функции , соответственно вызов:

CGaren::Hurted
CLeesin::Hurted
CEzreal::Hurted

Еще один пример полиморфизма

Задайте вопрос, чтобы проверить, все ли понимают характеристики полиморфизма. Следующий код,pBase->fun1()Каков результат?

class Base 
{
public:
    void fun1() 
    { 
        fun2(); 
    }
    
    virtual void fun2()  // 虚函数
    { 
        cout << "Base::fun2()" << endl; 
    }
};

class Derived : public Base 
{
public:
    virtual void fun2()  // 虚函数
    { 
        cout << "Derived:fun2()" << endl; 
    }
};

int main() 
{
    Derived d;
    Base * pBase = & d;
    pBase->fun1();
    return 0;
}

Ты думаешьpBaseХотя объект-указатель указывает на объект производного класса, но производный класс неfun1функция-член, вызовите базовый классfun1функция-член,Base::fun1()вызовет базовый классfun2функция-член, поэтому выводBase::fun2()?

Предположим, я конвертирую приведенный выше код, но все по-прежнему думают, что результатBase::fun2()?

class Base 
{
public:
    void fun1() 
    { 
        this->fun2();  // this是基类指针,fun2是虚函数,所以是多态
    }
}

thisФункция указателя состоит в том, чтобы указывать на объект, над которым действует функция-член, поэтому в нестатической функции-члене это можно использовать непосредственно для представления указателя на объект, над которым действует функция.

pBaseОбъект-указатель указывает на объект производного класса, а производного класса нет.fun1функция-член, поэтому она будет вызывать базовый классfun1функция-член, вBase::fun1()тело функции-членаthis->fun2()При фактическом указании на объект производного классаfun2функция-член.

Таким образом, правильный вывод:

Derived:fun2()

Итак, мы должны обратить внимание на:

Вызов «виртуальной функции» в функции-члене, не являющейся конструктором, не являющейся деструктором, является полиморфизмом!!!

Есть ли полиморфизм в конструкторах и деструкторах?

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

Давайте рассмотрим следующий пример кода для иллюстрации:

// 基类
class CFather 
{
public:
    virtual void hello() // 虚函数
    {
        cout<<"hello from father"<<endl; 
    }
    
    virtual void bye() // 虚函数
    {
        cout<<"bye from father"<<endl; 
    }
};

// 派生类
class CSon : public CFather
{ 
public:
    CSon() // 构造函数
    { 
        hello(); 
    }
    
    ~CSon()  // 析构函数
    { 
        bye();
    }

    virtual void hello() // 虚函数
    { 
        cout<<"hello from son"<<endl;
    }
};

int main()
{
    CSon son;
    CFather *pfather;
    pfather = & son;
    pfather->hello(); //多态
    return 0;
}

Выходной результат:

hello from son  // 构造son对象时执行的构造函数
hello from son  // 多态
bye from father // son对象析构时,由于CSon类没有bye成员函数,所以调用了基类的bye成员函数

Принцип реализации полиморфизма

Ключ к «полиморфизму» — пройтиуказатель или ссылка на базовый класспозвонивиртуальная функция, время компиляции не может определить, вызывается ли функция базового класса или производного класса, это может определить только среда выполнения.

мы используемsizeofЧтобы вычислить размер класса с виртуальными функциями и класса без виртуальных функций, что получится в результате?

class A 
{
public:
    int i;
    virtual void Print() { } // 虚函数
};

class B
{
public:
    int n;
    void Print() { } 
};

int main() 
{
    cout << sizeof(A) << ","<< sizeof(B);
    return 0;
}

На 64-битной машине результат выполнения:

16,4

Из вышеприведенных результатов видно, что у класса с виртуальными функциями есть лишние 8 байт.На 64-битной машине размер типа указателя ровно 8 байт.Что делает лишние 8 байт указателя?

01 таблица виртуальных функций

Каждый класс с «виртуальной функцией» (или класс, производный от класса с виртуальной функцией) имеет «таблицу виртуальных функций», в которую помещается любой объект этого класса.указатель на таблицу виртуальных функций. Адрес «виртуальной функции» класса указан в «таблице виртуальных функций».

Дополнительные 8 байтов используются для размещения адреса «виртуальной таблицы функций».

// 基类
class Base 
{
public:
    int i;
    virtual void Print() { } // 虚函数
};

// 派生类
class Derived : public Base
{
public:
    int n;
    virtual void Print() { } // 虚函数
};

Приведенный выше класс Derived наследует базовый класс, и оба класса имеют «виртуальные функции», поэтому форму его «таблицы виртуальных функций» можно понять как следующий рисунок:

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

02 Докажите роль указателя таблицы виртуальных функций

выше мы используемsizeofОператор вычисляет размер класса с виртуальными функциями и находит, что есть лишние 8 байт по размеру (64-битная система), а лишние 8 байт — это «указатель на таблицу виртуальных функций». Адрес «виртуальной функции» класса указан в «таблице виртуальных функций».

Следующий пример кода используется для демонстрации роли "указателя таблицы виртуальных функций":

// 基类
class A 
{
public: 
    virtual void Func()  // 虚函数
    { 
        cout << "A::Func" << endl; 
    }
};

// 派生类
class B : public A 
{
public: 
    virtual void Func()  // 虚函数
    { 
        cout << "B::Func" << endl;
    }
};

int main() 
{
    A a;
    
    A * pa = new B();
    pa->Func(); // 多态
    
    // 64位程序指针为8字节
    int * p1 = (int *) & a;
    int * p2 = (int *) pa;
    
    * p2 = * p1;
    pa->Func();
    
    return 0;
}

Выходной результат:

B::Func
A::Func
  • в строках 25-26paуказатель указывает наBобъект класса, поэтомуpa->Func()называетсяBвиртуальная функция объекта классаFunc(), выходB::Func;
  • Цель строк 29-30 — поставитьAПервые 8 байтов «указателя таблицы виртуальных функций» класса хранятся вp1указатель и ручкаBПервые 8 байтов «указателя таблицы виртуальных функций» класса хранятся вp2указатель;
  • Цель строки 32 состоит в том, чтобы поместитьA«Указатель таблицы виртуальных функций» класса назначаетсяB«Указатель таблицы виртуальных функций» класса, поэтому он эквивалентен размещениюB«Указатель таблицы виртуальных функций» класса заменяется наA«Указатель таблицы виртуальных функций» класса;
  • Из-за эффекта строки 32 поместитеB«Указатель таблицы виртуальных функций» класса заменяется наA"указатель таблицы виртуальных функций" класса, поэтому строка 33 вызываетAвиртуальная функция классаFunc(), выходA::Func

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


виртуальный деструктор

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

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

См. следующий пример:

// 基类
class A 
{
public: 
    A()  // 构造函数
    {
        cout << "construct A" << endl;
    }
    
    ~A() // 析构函数
    {
        cout << "Destructor A" << endl;
    }
};

// 派生类
class B : public A 
{
public: 
    B()  // 构造函数
    {
        cout << "construct B" << endl;
    }
    
    ~B()// 析构函数
    {
        cout << "Destructor B" << endl;
    }
};

int main() 
{
    A *pa = new B();
    delete pa;
    
    return 0;
}

Выходной результат:

construct A
construct B
Destructor A

Как видно из приведенного выше вывода, после удаленияpaКогда объект указателя,BДеструктор класса не вызывается.

Решение: объявить деструктор базового класса виртуальным

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

Определите деструктор базового класса в приведенном выше коде как «виртуальный деструктор»:

// 基类
class A 
{
public: 
    A()  
    {
        cout << "construct A" << endl;
    }
    
    virtual ~A() // 虚析构函数
    {
        cout << "Destructor A" << endl;
    }
};

Выходной результат:

construct A
construct B
Destructor B
Destructor A

Итак, заведите привычку:

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

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

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

class A 
{

public:
    virtual void Print( ) = 0 ; //纯虚函数
private: 
    int a;
};

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

  • Абстрактные классы могут использоваться только как базовые классы для получения новых классов и не могут создавать объекты абстрактных классов.
  • Указатели и ссылки на абстрактные классы могут указывать на объекты классов, производных от абстрактных классов.
A a;         // 错,A 是抽象类,不能创建对象
A * pa ;     // ok,可以定义抽象类的指针和引用
pa = new A ; // 错误, A 是抽象类,不能创建对象

Рекомендуемое чтение:

Понимание и роль C++ этого указателя

Статья C++ для понимания общих особенностей наследования

Перегрузка оператора присваивания C++ '=' (поверхностное копирование, глубокое копирование)

C++ научит вас реализовывать массивы переменной длины.

在这里插入图片描述