🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 一.虚函数 ### 1.作用 允许在派生类中重新定义与基类同名的函数,并且通过 **基类指针或引用** 来访问派生类中与基类同名的函数。 ### 2.虚函数的定义声明 在基类 `A` 声明时,在函数名 `f` 前添加 `virtual` 关键字。 此时若有派生类 `B` 继承了 `A` 类,则与基类同名的函数 `f` 会自动加上 `virtual` 关键字。 #### 例1 使用虚函数与不使用虚函数的案例对比 不使用虚函数: ```c++ #include <iostream> using namespace std; struct A { //这不是虚函数哦 void hi() { cout << "Hi, A" <<endl; } }; struct B :public A { //这不是虚函数哦 void hi() { cout << "Hi, B" <<endl; } }; int main() { B b; A &ra = b; b.hi(); ra.hi(); return 0; } ``` >[test] >Hi, B >Hi, A 使用虚函数: ```c++ #include <iostream> using namespace std; struct A { //虚函数 virtual void hi() { cout << "Hi, A" <<endl; } }; struct B :public A { //虚函数 void hi() { cout << "Hi, B" <<endl; } }; int main() { B b; A &ra = b; b.hi(); ra.hi(); return 0; } ``` >[test] >Hi, B >Hi, B #### 例2 虚析构函数 当我们使用基类指针去接收一个动态创建的派生类对象时,直接 `delete` 基类指针,则只会执行基类的析构函数,而不会删除派生类新增部分。这就会产生内存垃圾: ```c++ #include <iostream> using namespace std; struct A { ~A() { cout << "析构 A" <<endl; } }; struct B :public A { ~B() { cout << "析构 B" <<endl; } }; int main() { A *pa = new B; delete pa; return 0; } ``` >[test] >析构 A 为了避免这个问题,我们一般把析构函数声明虚函数,即使析构函数是空的: ```c++ #include <iostream> using namespace std; struct A { //虚析构函数 virtual ~A() { cout << "析构 A" <<endl; } }; struct B :public A { //虚析构函数 ~B() { cout << "析构 B" <<endl; } }; int main() { A *pa = new B; delete pa; return 0; } ``` >[test] >析构 B >析构 A >[warning]不可以将构造函数设置为虚函数,因为构造函数执行完毕后,对象创建才算完成,没有对象,就不能完成匹配。 ### 3.通过虚函数实现动态联编的条件 + 虚函数 + 通过 **基类指针或引用** 访问派生类对象 如果直接通过派生类、派生类的指针、派生类的引用、作用域运算符(包括基类和派生类)来访问与基类同名的函数,则只进行 **静态联编** 。 ### 4.虚函数的实现原理 编译器会为每个含有虚函数的类提供一个虚函数表(vtable),它实际上就是一个函数指针数组,用于存放该类中所有虚函数的入口地址;而每当用多态类创建一个对象时,编译器就会自动生成一个虚函数表指针(vptr),由构造函数正确对其初始化,使其指向该对象所属类的虚函数表,最后将它放置在对象结构的开头。 虚函数表中的函数地址可能并不是真正的虚函数地址,而可能是中间过渡函数 `thunk` 的地址,而这个中间过渡函数用于跳转到虚函数的真实地址,并对传入的对象指针 `this` 进行调整。 #### 例3 假设有以下类的声明: ```c++ struct A1 { int x_; A1(int x = 1) :x_(x) {} }; struct A2 { int x_; A2(int x = 2) :x_(x) {} virtual void test() { cout << "Call base A2 vf"; } }; struct A3 { int x_; A3(int x = 3) :x_(x) {} virtual void test() { cout << "Call base A3 vf"; } }; struct B : public A1, public A2, public A3 { int y_; void test() { cout << "Call derived vf"; } }; ``` 使用 `Visual Studio` 打开上述代码,并在命令行选项中添加 `/d1 reportSingleClassLayoutB `,这样,在编译时,通过输出窗口可以看到 类 `B` 的内存布局和虚函数表: ``` class B size(24): +--- 0 | +--- (base class A2) 0 | | {vfptr} 4 | | x_ | +--- 8 | +--- (base class A3) 8 | | {vfptr} 12 | | x_ | +--- 16 | +--- (base class A1) 16 | | x_ | +--- 20 | y_ +--- B::$vftable@A2@: | &B_meta | 0 0 | &B::test B::$vftable@A3@: | -8 0 | &thunk: this-=8; goto B::test B::test this adjustor: 0 ```