C++ 多态和虚函数
一. 先搞清override overload overwrite的区别
1. overload(重载)(不是多态)
在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
2. override(覆盖)(运行时多态、虚函数)
是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
3. overwrite(重写)(编译时多态)
是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
二. 静态联编和动态联编(运行时多态和编译时多态)
- 在C++中,多态性的实现和联编这一概念有关。一个源程序经过编译,链接,成为可执行文件的过程是把可执行代码连接在一起的过程。
- 在编译过程中进行联编被称为静态联编(static binding),编译器生成的能够在程序运行时选择正确的虚方法的代码,被称为动态联编(dynamic binding)。
- 静态联编也称为编译时多态性,主要通过函数重写实现。动态联编也称为运行时多态性。主要通过继承和虚函数来实现。
- 编译器对非虚方法使用静态联编,一个父类指针指向一个子类时,静态联编调用的是父类函数。
编译器对虚方法使用动态联编,运行时程序才确定对象的类型,此时一个父类指针指向一个子类,调用的是子类的函数。
三. 动态联编的工作原理(虚函数表)
- 编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,用于保存一个指向函数地址数组的指针。这个数组称为“虚函数表”。
- 没有虚函数的C++类,是不会有虚函数表的。
- 虚函数表(virtual function table,vtbl):存储了为类对象进行声明的虚函数的地址。
1. 虚表工作原理
虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。
调用虚函数的时候,程序将查看存储在对象中的虚表地址(也就是说按照实例所属的类来看的,而不是按照指针类型。同一类共用一张虚表,只是每一个对象都一个指向该虚表的指针)【前几天面试遇到一个面试官,对方坚持说是一个实例里面一张虚表,就感觉很奇怪,后来问了一些大佬,回复是看编译器实现,两种都可能。不过多数还是一个指针,共用一张虚表的。】,然后转向相应的函数地址表(放在类中,一个类只有一张)。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址;如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。
而这张虚表,是会根据你是否有override、是否有overwrite来决定每个函数指针指向的位置的。
2. 举个栗子(看完再回去1理解原理可能更好理解)
- 当使用了虚函数override时,可以明显看到,两个虚表指针 _vfptr 不一样,同时指向两个函数的位置也不一样(红圈)。
- 在找函数时候都是找虚表中的 [2],猜测[0]和[1]是默认的构造和析构函数。
#include<iostream>
using namespace std;
class father_test {
public:
virtual void test() { cout << "test1" << endl; }
};
class son_test :public father_test {
public:
virtual void test() { cout << "test2" << endl; }
};
int main() {
son_test son;
father_test father;
father_test *p1,*p2;
p1 = &son;
p2 = &father;
p1->test();
p2->test();
for (int i = 0; i < 10; i++);
cout << endl;
getchar();
return 0;
}
- 当使用了vitrual声明,但没有进行override的时候,可以看到还是有两个虚表 _vfptr 的指针,只是这时函数指向的地址一样(红圈)。
#include<iostream>
using namespace std;
class father_test {
public:
virtual void test() { cout << "test1" << endl; }
};
class son_test :public father_test {
public:
//virtual void test() { cout << "test2" << endl; }
};
int main() {
son_test son;
father_test father;
father_test *p1,*p2;
p1 = &son;
p2 = &father;
p1->test();
p2->test();
for (int i = 0; i < 10; i++);
cout << endl;
getchar();
return 0;
}
- 为了验证上面对[2]的猜测,在test函数前面,多加了一个test2函数。果然可以看到这时调用test变成[3]
#include<iostream>
using namespace std;
class father_test {
public:
virtual void test2() {}
virtual void test() { cout << "test1" << endl; }
father_test(){}
~father_test(){}
};
class son_test :public father_test {
public:
virtual void test2(){}
virtual void test() { cout << "test2" << endl; }
son_test(){}
~son_test(){}
};
int main() {
son_test son;
father_test father;
father_test *p1,*p2;
p1 = &son;
p2 = &father;
p1->test();
p2->test();
for (int i = 0; i < 10; i++);
cout << endl;
getchar();
return 0;
}
- 再来看虚表什么时候真正初始化的
运行到①的时候,此时有指针p1和p2,但显示“无法读取内存”
运行到②,p1指针指向一个实例对象,此时可以看到_vfptr了,p2还是没有
运行到③,两个指针的虚表都有了内容。
- 那么问题又来了,是什么时候有的虚表。指针初始化的时候才有吗?显然不是,根据前面的理论可以知道,其实在对象初始化的时候就有。
所以回到 son_test son; 这句语句执行完,而father_test father; 还没执行之前
可看到,对象son里面已经有虚表了,但father的虚表还是一堆问号
四. 多重继承时的虚函数表
- 多重继承情况下,和上面类似。只是有几个父类,就有几个vtbl和_vfptr