【步兵 c++】 多态&虚函数

【步兵 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~
之前
真爱无价,欢迎打赏~
赞赏码

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页