【步兵 c++】 多态&虚函数 by EOS.
多态和虚函数
其实,多态的主要表现其实就是通过父类来调子类。而这种表现的主角就是虚函数。
至于多态和虚函数的具体概念,请自行搜索,我这里就不多说了。
提到虚函数就不能不提虚函数表,虚函数表是一个什么东西呢,可以认为它就是一个存储函数指针的列表。
但是实际上,他是一个跳转到自身函数的中转站,不考虑多继承的话,每个类只有一个虚函数表,而却可以有多个实例,而每个实例又有一个虚函数表指针指向这个虚函数表,通过中专找到自身函数的位置。
下面结合实例来讲解
实例讲解
static inline void print(const char* str)
{
OutputDebugString(str);
OutputDebugString("\n");
}
class Hero
{
public:
Hero() { print("Hero_Create"); };
~Hero() { print("Hero_Destroy"); };
void attack() { print("Hero_Attack"); }
};
class Knight : public Hero
{
public:
Knight() { print("Knight_Create"); };
~Knight() { print("Knight_Destroy"); };
void attack() { print("Knight_Attack"); }
};
以上是准备工作,我们定义了一个英雄基类,然后派生了一个骑士的子类。
Hero* k = new Knight;
k->attack();
delete k;
//输出结果
//Hero_Create
//Knight_Create
//Hero_Attack
//Hero_Destroy
显然,Knight的析构没有被调用,如果你在Knight的构造中,申请了大量的内存空间,预计在Knight的
的析构中去释放,结果却没有被执行,那么将直接造成内存泄漏。what the fuck~
而且attack方法也不是我们想要的结果。
当在Hero的析构函数前加入virtual关键字时,输出发生了改变
//virtual ~Hero() { print("Hero_Destroy"); };
Hero* k = new Knight;
k->attack();
delete k;
//输出结果
//Hero_Create
//Knight_Create
//Hero_Attack
//Knight_Destroy
//Hero_Destroy
虽然效果达到了,我们调用到了Knight的析构函数,但我不得不说,这个写法很不明智。
因为如果一个基类只有析构函数是虚函数,那么这个基类是没有完全意义的,上面可以看到,
攻击函数还是调用的Hero的,那么我用基类调不到子类的方法,无疑我们没有实现多态性。
所以,会有这样的说法:当析构函数是虚函数时,至少存在一个成员函数是虚函数。
说法不重要,重要的是我们要知道,基类要想调用到子类函数,那么这个函数必须是虚函数。
//virtual void attack() { print("Hero_Attack"); }
Hero* k = new Knight;
k->attack();
delete k;
//输出结果
//Hero_Create
//Knight_Create
//Knight_Attack
//Knight_Destroy
//Hero_Destroy
这才是我们想要的结果,也就是多态的实现,父类来调子类。
虚函数表(vtbl)
Hero 虚函数表 |
---|
&Hero::析构 |
&Hero::attack |
…
Knight 虚函数表 |
---|
&Knight::析构 |
&Knight::attack |
虽然 Hero* k = new Knight;
把Knight转为了Hero,但是这个对象的原有属性还在(首地址和内容没变),
实际表现就是,强制转换回来Knight* k2 = static_cast<Knight*>(k);
依旧可以当作Knight使用。
所以它的虚函数表指针(vptr)依然指向的是Knight的虚函数表,然后根据自己首地址和虚函数表的一个跳转,
这样就能找到自己的attack方法了。
这就是为什么
Hero* k = new Knight;
k->attack();
能输出Knight_Attack的原因了。
关于析构的疑惑
首先需要了解
Knight* k = new Knight;
delete k2;
//输出内容
//Hero_Create
//Knight_Create
//Knight_Destroy
//Hero_Destroy
也就是说,派生类(也就是子类)的析构函数默认就会调用父类的析构函数,
但是因为我们通过基类(也就是父类)来调用,使其失去了原有的机能。
因为他找不到自己的析构函数,然后我们通过又虚析构函数,
让他找到了自己的析构,恢复了原有的机能,这一点希望大家不要迷糊。
–
总结和延伸
通过上面的讲解,从多态的实现,希望大家记住:
当一个类作为基类来使用时,他的析构函数必须是虚函数,而且至少有一个成员函数是虚函数。
提到 析构函数是虚函数,那么自然会有构造函数可以是虚函数吗 的疑问。
正推:
如果构造函数是虚函数,那么他就会将存在与虚函数表中,那么我们只有通过实例中转才能找到,
但是,创建一个对象时,构造函数必须知道其确切类型,否则是无法分配内存的。
反推:
就算创建了对象找到了虚函数表,也不可能有指向构造函数的指针。因为构造函数不是普通的函数,
他是直接跟内存打交道的。
See Again~
之前
真爱无价,欢迎打赏~