Виртуальные функции и полиморфизм
01 Виртуальная функция
- В определении класса, которому предшествует
virtual
Функции-члены с ключевым словом называются виртуальными функциями; -
virtual
Ключевые слова используются только в объявлениях функций в определениях классов, а не при написании тел функций.
class Base
{
virtual int Fun() ; // 虚函数
};
int Base::Fun() // virtual 字段不用在函数体时定义
{ }
02 Выражение первого полиморфизма
- «Указатель на производный класс» может быть назначен «указателю на базовый класс»;
- При вызове одноименной «виртуальной функции» в базовом классе и производном классе через указатель базового класса:
- Если указатель указывает на объект базового класса, то вызываемый виртуальная функция базового класса;
- Если указатель указывает на объект производного класса, то вызывается является виртуальной функцией производного класса.
Этот механизм называется «полиморфизмом», если говорить прямо.Какую виртуальную функцию вызывать, зависит от типа объекта, на который указывает объект-указатель..
// 基类
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 Второе выражение полиморфизма
- Объекты производных классов могут быть назначены «ссылкам» базового класса.
- При вызове «виртуальной функции» с тем же именем в базовом классе и производном классе через ссылку на базовый класс:
- Если ссылка ссылается на объект базового класса, вызываемый Используйте виртуальную функцию базового класса;
- Если ссылка относится к объекту производного класса, то Вызывается виртуальная функция производного класса.
Этот механизм еще называют «полиморфизмом», если говорить прямо.Какую виртуальную функцию вызывать, зависит от типа объекта, на который ссылаются..
// 基类
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
Основная мысль:
- Напишите для каждого класса героев
Attack
,FightBack
а такжеHurted
функция-член.
-
Attack
Функция представляет атакующее действие; -
FightBack
Функция представляет действие контратаки; -
Hurted
Функция указывает, что он снижает собственное здоровье и показывает действие травмы.
- установить базовый класс
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-26
pa
указатель указывает на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++ '=' (поверхностное копирование, глубокое копирование)