函数调用绑定(Function Call Binding)指的是将一个函数调用与相应的函数定义(实现)关联起来的过程。
1. 函数绑定
对于 C++ 程序而言,将函数查找、关联的过程放在编译期完成,在运行时,避免这部分工作,将会提升程序运行时的性能。所以,C++ 中大部分函数的绑定工作都是在编译期完成。
在编译期进行函数绑定,也叫做函数的静态绑定、早绑定、编译期绑定。
#include <iostream> using namespace std; // 普通函数 void do_logic() {} // 重载函数 void func(int) {} void func(double) {} void func(int, int) {} // 成员函数 struct MyClass { void do_sth() {} }; struct OtherClass { void do_sth() {} }; void test() { // 全局就一个 do_logic 函数,编译期确定 do_logic(); // 通过参数确定函数调用,编译期确定 func(100); // 根据对象类型、指针类型确定函数调用,编译期确定 MyClass mc; mc.do_sth(); MyClass* pmc = new MyClass; pmc->do_sth(); delete pmc; } int main() { test(); return 0; }
对于大部分场景下,A 类型指针或者引用指向 A 类型对象,调用的也是 A 类型对应的函数实现。但是,在多态场景下,会出现 A 类型指针或者引用指向 B 类型的场景,此时就出现了与我们预期不相符的情况。
#include <iostream> using namespace std; struct Animal { void Speak() { cout << "Animal::Speak" << endl; } }; struct Dog : public Animal { void Speak() { cout << "Dog::Speak" << endl; } }; struct Cat : public Animal { void Speak() { cout << "Cat::Speak" << endl; } }; void test() { Animal* animal = nullptr; animal = new Dog; animal->Speak(); animal = new Cat; animal->Speak(); } int main() { test(); return 0; }
程序执行结果:
Animal::Speak Animal::Speak
由于 C++ 编译器默认进行静态绑定,无法根据对象指针实际指向的对象类型来进行函数调用,这就不符合我们的预期。所以,我们需要将函数的绑定由编译阶段延迟到运行阶段,从而实现根据实际对象类型来选择函数调用。这就是动态绑定,也称作晚绑定、运行时绑定。
C++ 中,通过将成员函数声明为 virtual 虚函数来实现动态绑定。即:当类中包含任何虚函数时:
- 编译阶段:当编译器看到类中的虚函数,就不再简单根据对象类型来进行函数绑定
- 运行阶段:会根据实际的对象类型来进行函数的确定,然后调用执行
#include <iostream> using namespace std; struct Animal { virtual void Speak() { cout << "Animal::Speak" << endl; } }; struct Dog : public Animal { virtual void Speak() { cout << "Dog::Speak" << endl; } }; struct Cat : public Animal { virtual void Speak() { cout << "Cat::Speak" << endl; } }; void test() { Animal* animal = nullptr; animal = new Dog; animal->Speak(); animal = new Cat; animal->Speak(); } int main() { test(); return 0; }
程序的执行结果:
Dog::Speak Cat::Speak
3. 虚函数表
函数的动态绑定是基于虚函数表实现的。当类的内部包含虚函数时,编译器会在对象的第一个数据成员位置安插一个 vfptr 指针(构造函数中初始化),指向一个包含所有虚函数地址的数组。
当子类没有重写父类的虚函数时,在子类的虚函数表中保存父类虚函数的地址。但是,当子类重写父类的虚函数时,就会在虚函数表中发生覆盖行为。当调用对应函数时,会通过虚函数指针找到虚函数表,并找到对应函数地址调用执行。
接下来,通过一段代码来理解上面的内容:
cl /d1 reportSingleClassLayout类名 xxx.cpp
#if 1 #include <iostream> #include <functional> using namespace std; struct Animal { virtual void Speak() { cout << "Animal::Speak" << endl; } }; struct Dog : public Animal {}; struct Cat : public Animal { virtual void Speak() { cout << "Cat::Speak" << endl; } virtual void Ohter() { cout << "Cat::Ohter" << endl; } }; void test() { Animal* animal = nullptr; // 根据对象的实际类型选择合适的函数实现 animal = new Animal; cout << sizeof(*animal) << endl; // 8 animal->Speak(); animal = new Dog; animal->Speak(); animal = new Cat; animal->Speak(); } int main() { test(); return 0; } #endif
// Animal 类 class Animal size(4): +--- 0 | {vfptr} +--- Animal::$vftable@: | &Animal_meta | 0 0 | &Animal::Speak // Dog 类 class Dog size(4): +--- 0 | +--- (base class Animal) 0 | | {vfptr} | +--- +--- Dog::$vftable@: | &Dog_meta | 0 0 | &Animal::Speak // Cat 类 class Cat size(4): +--- 0 | +--- (base class Animal) 0 | | {vfptr} | +--- +--- Cat::$vftable@: | &Cat_meta | 0 0 | &Cat::Speak 1 | &Cat::Ohter
注意:下面几个关于虚函数的点也需要大家了解:
- 虚函数表生成与共享:
- 编译器为每个包含虚函数的类生成一张虚函数表(vtable)。该类型的所有对象共享同一个虚函数表。这个表包含了该类及其基类中所有虚函数的地址。
- 虚函数表的初始化与销毁:
- 虚函数表的初始化和销毁由编译器负责。在对象的构造过程中,编译器会确保对象的虚函数指针指向正确的虚函数表。
- 多重继承和虚函数表
- 在多重继承的情况下,可能会有多个虚函数表。每个基类都有自己的虚函数表,并且子类对象可能包含多个虚函数指针,以确保每个基类的虚函数都能正确调用。
#if 1 #include <iostream> #include <functional> using namespace std; struct Animal { virtual void Speak() { cout << "Animal::Speak" << endl; } }; struct Dog : public Animal {}; struct Cat : public Animal { virtual void Speak() { cout << "Cat::Speak" << endl; } virtual void Ohter() { cout << "Cat::Ohter" << endl; } }; void test() { Animal* animal = nullptr; // 1. 每个类都会对应一个虚函数表,所有该类的对象共享虚函数表 // 获得虚函数表地址 animal = new Animal; void** vfptr = *(void***)animal; cout << "Animal 虚函数表地址:\t" << vfptr << endl; animal = new Dog; vfptr = *(void***)animal; cout << "Dog 虚函数表地址:\t" << vfptr << endl; animal = new Cat; vfptr = *(void***)animal; cout << "Cat 虚函数表地址:\t" << vfptr << endl; Cat* other = new Cat; vfptr = *(void***)other; cout << "Cat 虚函数表地址:\t" << vfptr << endl; // 2. 调用虚函数表中的函数 // 2.1 获得虚函数表地址 vfptr = *(void***)animal; // 2.2 获得虚函数地址 void(*p_func)() = reinterpret_cast<void(*)()>(vfptr[1]); // 2.3 执行虚函数 p_func(); } struct Base1 { virtual void Vfunc1() { cout << "Base1::Vfunc1" << endl; } }; struct Base2 { virtual void Vfunc2() { cout << "Base2::Vfunc2" << endl; } }; struct Derived : public Base1, public Base2 {}; int main() { test(); return 0; } #endif
类 Derived 的对象结构:
class Derived size(8): +--- 0 | +--- (base class Base1) 0 | | {vfptr} | +--- 4 | +--- (base class Base2) 4 | | {vfptr} | +--- +--- Derived::$vftable@Base1@: | &Derived_meta | 0 0 | &Base1::Vfunc1 Derived::$vftable@Base2@: | -4 0 | &Base2::Vfunc2