前言:
共识
-
基类对象的指针或者引用调用虚函数时,都以多态的流程执行代码。可以说,虚函数就是为实现多态做准备的。 -
同一个类的不同对象,共用同一个虚函数表,且虚函数表最可能存放在常量区/代码段
重载
- 要求重载的函数在同一作用域,一般是全局作用域
- 函数名,参数相同
重定义
- 2个函数分别在基类和子类作用域
- 重定义也叫做隐藏,基类与子类的同名函数就构成隐藏
- 重定义关注的地方是:函数的声明和实现
- 同名成员变量也构造隐藏
重写
- 2个函数分别在基类和子类的作用域
- 要求三同:函数名/参数/返回值必须相同
- 2个函数必须是虚函数
- 重写就是一种接口继承(会继续基类的属性,这也就是为什么子类可以不写virtual),关注的是函数的实现。
- 当基类指针或者引用调用重写函数,可能构造多态,当子类对象调用重写函数,构成隐藏/重定义
重写的2个特殊情况
- 协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
- 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
重写的一个特殊点
- 重写如果在多态调用中就是
C++11 override 和 final
final
-
修饰函数:表明该函数不能被重写。 -
修饰类:表明该类不能被继承
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
一个例题
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
多态
概念
不同的子类对象,通过基类指针与引用,调用相同的函数,产生不同的效果。
条件
- . 必须通过基类的指针或者引用调用虚函数,即保证调用函数的this指针是基类地址
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
- 当子类对象赋值给基类对象时,编译器只会将成员变量赋值给基类对象,子类的虚函数不会拷贝给基类对象
应用层分析
通过基类对象的指针或者引用,调用虚函数,且该虚函数完成重写时,构成多态。
原理层分析
通过设置一个虚函数表,实现多态
虚函数表
- 虚函数指针是在对象构造函数初始化队列中初始化的。
- 本质是一个数组,存放的是虚函数的地址
虚函数表打印
class Base {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Derive : public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
//typedef void(*) () VFPTR;
//本来重命名函数指针类型时是这样,但是C++语言规定这种是不行的,需要将VFPTR放到内部
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr;++i)
{
printf("第%d个虚函数地址 :%p,->", i, vTable[i]);
//通过this调用函数是在编译阶段的事情,一旦到内存就没有这种限制
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
printf("Derive::func1地址%p\n", &(Derive::func1));
printf("Derive::func2地址%p\n", &(Derive::func2));
printf("Derive::func3地址%p\n", &(Derive::func3));
VFPTR* vTableb1 = (VFPTR*)(*((void**)&d));
//通过强制转换来获得d中前一个指针大小的字节内容。
//考虑到不同平台指针大小不同,
//我们指针指针解引用后会得到对应地址后的指向对象大小的内容
// 即int *p,p解引用后,得到从指向对象地址也就是变量p中的值
// 开始的sizeof(int)字节大小的内容
//
// 因此将d的地址强转为二级指针,当引用后,
// 就可以的到对应地址的sizeof(void*)大小的内容
// 就可以满足不同平台完成同样的需求
//最后再强制转换即可
PrintVTable(vTableb1);
}
可以发现,主动打印的函数地址和虚函数表中的地址是不同?
这是因为VS进行了封装,无论是主动打印的还是虚函数表中的地址都不是真正的函数地址,而是一个指令地址。证明如下
多继承关系的虚函数表
- 多继承中,子类会将自己独有的虚函数,放到继承声明中的第一个基类的虚函数表中
- 基类各自拥有着自己的虚函数表
菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇链接文章。
C++ 虚函数表解析
C++ 对象的内存布局
菱形继承
在发生代码冗余与二义性的地方,通过virtual继承,建立一个虚基表,通过相对偏移量实现基类的共享
菱形虚拟继承
- 菱形虚拟继承中,因为BC共享同一个基类A,但是A的虚函数表只有一个,那么A中的虚函数表该存放那个是没法确定的。
- 通过在D中重写func1来解决该问题。同时BC中的虚函数表存放D的func1
问答题
- inline函数可以是虚函数吗?答:可以,不过当进行多态调用时编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式,是无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。 - 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
|