以前学习多态部分的时候总觉得囫囵吞枣,感觉懂了但是又觉得没太理解,今天研究了一下,索性就记录一下吧。
多态需要发生的条件:
- 父类的指针或者引用指向子类对象
- 父类的函数必须要是虚函数
- 子类必须要对父类的虚函数进行重写。
需要注意的是第3点:这里子类中对应的函数和父类中的函数必须长得一模一样,也意味着除了函数内部的实现机制不一样,函数的外壳(名称,返回值,参数类型大小)必须要一模一样。
为什么要有如此严格的限制呢,多态机制就是依靠对象中的虚函数表来实现的。我们一点点来分析。
如果一个对象中的成员函数不是虚函数时,为了节省空间,我们知道函数是被保存在公共代码区的。对象a的大小为4个字节,代码如下所示:
class Base
{
public:
void Func1()
{
cout << "Base::Func1()" << endl;
}
void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
Base a;
cout<<sizeof(a)<<endl;//4
当我们尝试使得Func1和Func2变为虚函数时,再来看看a的大小,此时对象a的大小已经变成了8个字节。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
Base a;
cout<<sizeof(a)<<endl;
父类的虚函数表:
编译器会为具有虚函数的类创建一个虚函数表,该虚函数表被该类的所有对象共享,虚函数表中存在的是每一个虚函数的地址,如下图所示。
虚表指针:
所有的对象共用1张虚函数表,为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。因此编译器在类中添加了一个指针_vfptr,用来指向虚函数表,它是一个二级指针。
由于对象a和对象b都属于Base类,因此_vfptr的值是相同的。
打印虚函数表的地址:
int main()
{
Base a;
Base b;
cout << "a:_vsfptr虚表地址" << (int*)(*(int*)(&a)) << endl;;
cout << "b:_vsfptr虚表地址" << (int*)(*(int*)(&b)) << endl;
}
子类的虚函数表:
下面我们看如果子类对父类进行继承,同时对父类的虚函数重写之后会发生什么现象。
子类定义如下:对父类的Func1()进行了重写,而且自身还有1个虚函数Func4()
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base a;
Derive b;
cout << "父类虚表地址" << (int*)(*(int*)(&a)) << endl;;
cout << "子类虚表地址" << (int*)(*(int*)(&b)) << endl;
}
可以看出来父类和子类虚函数表的地址是不一样的,因此子类和父类有两张不同的虚函数表,对应的内存模型如下:
注意,使用visual studio查看b的时候,由于b是继承自Base类的,因此通过编译器查看的时候:我们只能看到b下面的Base类我们只能看到Fun1()和Fun2(),而无法看到b中的Fun4()。
通过以下代码可以查看到Fun4()是真实存在的,注意这里强转为int类型的指针是因为函数的地址也是4个字节大小,方便操作,具体的注释如下所示:
int main()
{
函数指针重新定义为Fun类型
typedef void(*Fun)(void);
Fun pfun = NULL;
Base a;
Derive b;
虚函数表第一行的地址
int* vt_one = (int*)(*(int*)(&b));
虚函数表第二行的地址
int* vt_two = (int*)(*(int*)(&b)) + 1;
虚函数表的第三行的地址
int* vt_three = (int*)(*(int*)(&b)) + 2;
对首地址解引用得到的第一个虚函数的地址,强转为函数指针类型
pfun = (Fun) * (vt_one);
调用函数
pfun();
对第二行的地址解引用得到的第二个虚函数的地址
pfun = (Fun) * (vt_two);
pfun();
pfun = (Fun) * (vt_three);
pfun();
}
结果如下所示:可以看到Derive:Func4()函数的存在
多态的原理解释:
当父类的指针指向不同的子类对象时,调用父类对应的函数,会触发不同的功能,看起来很玄学,但其实正是虚函数表作祟的原因。
由于父类的指针指向子类的对象,因此实际上访问的仍然是子类的对象。但是由于该指针是父类类型的,所以你只能看到子类中父类的那一部分,子类中有父类中没有的那部分当然就看不到了,就像上面提到的Func4函数一样,站在父类的角度上,是看不到该函数的。
因为子类函数因为和父类函数长得一模一样,因此子类虚函数的地址覆盖掉原本父类中相对应函数的地址,所以通过父类指针调用时,才可以调用子类的函数。
int main()
{
typedef void(*Fun)(void);
Fun pfun = NULL;
Base a;
Derive b;
Base *c = &b;
a.Func1();
c->Func1();
c->Func4()//会报错
b.Func4();//正常
}
对应的内存模型如下:注意这里通过c是访问不了Func4()了,大家可以和上面的图对比。
那么如果不是指针或者引用,仅仅通过拷贝赋值的话,是无法达到这个效果的,因为a所指向的地址和b所指向的地址是不一样的。故c的虚函数表还是最初的父类的虚函数表,和子类的虚表无关。
Base a;
Derive b;
// 拷贝赋值,子类可以转化为父类(子类的东西多于父类哦),反之不可以的。
Base c = b;
a.Func1();
c.Func1();
常见的面试题目总结:
1.inline, static, constructor三种函数都不能带有virtual关键字
inline函数没有地址,无法将地址放入虚函数表中,它是直接插入到运行的地方中。它在编译时被确定,而虚函数只有运行的时候才可以确定。
static函数没有this指针,virtual函数一定要通过对象来调用。
构造函数也不能是虚函数,因为对象中的虚表指针是在构造函数初始化列表中才初始化的。
2. 析构函数可以是虚函数吗?
最好定义为虚函数,而且是必须的。否则有可能造成内存泄漏,如果子类对象中包含指针成员,由于析构函数没有进行重写,因此只会调用父类的析构函数,而不会调用子类的析构函数。
3.什么是抽象类,抽象类的作用是什么?
在虚函数后面写上=0,则这个函数是纯虚函数。包含纯虚函数的类称为抽象类,抽象类无法实例化对象,派生类继承之后必须要重写虚函数,才可以实例化出对象。
虚函数的继承体现的是一种接口继承,继承的是基类虚函数的接口,目的是为了重写,达成多态。