11. 虚函数
多态:不关心子类对象的具体类型,调用子类对象自己的虚函数。
实现:虚函数。通过父类的指针或引用调用虚函数时才发生动态绑定。
虚表:虚函数构成的数组。
虚表指针:指向数组的指针
虚表指针的赋值时机:每个类在构造和析构时,会把虚表指针填成自己虚表的地址。子类构造时,先构造父类,于是先赋值为父类的虚表指针,父类构造完毕后,再赋值为子类的虚表指针。
虚表的创建在编译期就完成了,数据保存在exe中。
简单举例:
class B {
public:
B() {
}
virtual void f1() {}
virtual void f2() {}
virtual void f3() {}
virtual void f4() {}
};
class D :public B {
public:
D() {
}
virtual void f1() override {}
virtual void f2() override {}
};
010 Editor打开磁盘中的exe,选中的一行是虚表,有四个虚函数:

vs2019调试状态:

内存情况:

整个过程:
一.编译期:
1.编译器创建父类B的虚表
B::vftable[4] = {&B::f1, &B::f2, &B::f3, &B::f4};
2.编译器拷贝父类B的虚表给子类
D::vftable[4] = {&B::f1, &B::f2, &B::f3, &B::f4};
3.编译器替换子类D重写的部分,未重写则继续用父类的
D::vftable[4] = {&D::f1, &D::f2, &B::f3, &B::f4};
二.运行期:
书写B *p = new D;时,发生如下事情:
1.构造父类B
__vfptr = B::vftable;
2.构造子类D
__vfptr = D::vftable;
调用虚函数p->f1();时,通过虚表指针,以数组下标寻址的方式获取数组第一项,即虚函数f1的地址,然后调用:
(p->(__vfptr[0])) ();
多态在父类成员函数中的表现:
1.在父类的一般成员函数中调用虚函数,有多态效果.
2.在父类的构造函数中调用虚函数,此时虚表指针指向父类自己的虚表,子类尚未初始化,调用子类虚函数可能崩溃,vs2019直接以静态绑定方式调用。
3.在父类的析构函数中调用虚函数,此时虚表指针指向父类自己的虚表,子类早已经析构,调用子类虚函数可能崩溃,vs2019直接以静态绑定方式调用。
构造函数不允许标记为virtual,父类的虚构函数一般都标记为virtual。
class B {
public:
B() {
f();//构造时,虚表指针指向自己的虚表,调用B::f,观察反汇编发现以静态绑定方式调用B::f
}
void foo() {
f();
}
~B() {
f();//析构时,虚表指针指向自己的虚表,调用B::f,观察反汇编发现以静态绑定方式调用B::f
cout << "~B" << endl;
}
virtual void f() {
cout << "~Bf" << endl;
}
};
class D :public B {
public:
D() {
f();//构造时,虚表指针指向自己的虚表,调用D::f,观察反汇编发现以静态绑定方式调用D::f
}
~D() {
f();//析构时,虚表指针指向自己的虚表,调用D::f,观察反汇编发现以静态绑定方式调用D::f
cout << "~D" << endl;
}
virtual void f() override {
cout << "~Df" << endl;
}
};
int main() {
B *p = new D;
p->foo();//foo内部调用f时发生动态绑定,此时虚表指针指向子类D的虚表,调用D的虚函数f
cout << endl;
//由于基类B的析构函数是virtual,所以子类D的析构函数自动成为虚函数,即使~D没加virtual也如此,
//所以delete p时,先调用子类D的析构函数,再调用B的析构函数。
//如果基类B的析构函数未标记为virtual,则delete p只调用基类B的析构函数。
delete p;
return 0;
}

浙公网安备 33010602011771号