您的位置:首页 > 编程语言 > C语言/C++

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

2016-05-04 00:18 316 查看

【步兵 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~

之前

真爱无价,欢迎打赏~

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息