深入解析C++多态:虚函数与动态联编
一、核心概念:静态联编与动态联编
1. 静态联编(编译期确定)
- 定义:编译阶段就能确定调用的函数版本,也叫早绑定。
- 适用场景:
- 普通成员函数调用;
- 重载函数匹配;
- 默认使用的指针 / 引用调用(非虚函数)。
- 特点:速度快,但缺乏灵活性,无法适配 “一个接口,多种实现” 的多态需求。
2. 动态联编(运行期确定)
- 定义:程序运行时才确定调用的函数版本,也叫晚绑定,是 C++ 多态的核心实现方式。
- 核心条件:函数名相同、参数列表相同、返回值相同(协变除外),且基类函数声明为
virtual。 - 底层原理:
- 每个包含虚函数的类会生成虚函数表(vftable):存储类中所有虚函数的地址;
- 类的对象占用的内存首部会包含虚函数表指针(vfptr):指向所属类的虚函数表;
- 调用虚函数时,通过对象的 vfptr 找到 vftable,再从表中找到对应函数地址执行。
二、虚函数的语法规则
1. 声明与重写
- 声明:在基类成员函数前加
virtual关键字(仅需基类声明,子类重写时virtual可省略,但建议显式写)。 - 重写(override):
- 子类重写基类虚函数时,必须保证函数签名(函数名、参数、const/volatile 限定)完全一致;
- C++11 新增
override关键字:显式标注子类重写的虚函数,编译器会检查重写合法性(如签名不匹配则报错),推荐使用。
class Object { private: int value; public: Object(int x=0):value(x){ } virtual void func(int a) { cout << "Object::func:a:" << a << endl; } virtual void hello()const { cout << "Object::hello" << endl; } virtual void show() { cout << "Object::show" << endl; } }; //为什么每次构造,都必须初始化上一个类? /* 因为子类是继承父类的,所以必须在父类的基础上去新增自己的部分 */ #if 0 class Base :public Object { private:int num; public: Base(int x=0):Object(x+10),num(x){ } //函数名相同 参数类型相同 返回类型相同 才可以覆盖 //override重写关键词: //用于显式声明子类虚函数重写基类虚函数,让编译器进行严格检查,避免因函数签名不匹配导致的隐藏、重载错误,提高代码可读性与安全性。 virtual void func(int a)override { cout << "Base::func:a:" <<a<< endl; } virtual void hello()const { cout << "Base::hello" << endl; } virtual void zero() { cout << "Base::zero" << endl; } }; class Test :public Base { private: int sum; public:Test(int x=0):Base(x+10),sum(x){ } virtual void func(int x) { cout << "Test::func:x:" << x << endl; } virtual void show() { cout << "Test::show" << endl; } virtual void zero() { cout << "Test::zero" << endl; } }; void print(Object* pobj) { assert(pobj != nullptr); pobj->func(1); pobj->hello(); pobj->show(); ((Test*)pobj)->zero();//强转,很危险,pobj指向基类,基类没有第四个zero对象,如果指向pobj指向Object对象会造成越界访问,就会报错 } void print(Object& pobj) { pobj.func(1); pobj.hello(); pobj.show(); } //对象调用不查虚表 /* void print1(Test& pobj) { pobj.func(1); pobj.hello(); pobj.show(); } */ int main() { Object objx(10); Base base(20); Test test(30); print(&base); //print1(test); //静态编译 test.func(2); }
2. 不能声明为虚函数的函数
| 函数类型 | 原因 |
|---|---|
| 构造函数 | 构造函数执行时,对象的虚函数表指针尚未初始化完成,无法实现动态联编;且构造函数是初始化对象,而非对象调用。 |
| 全局函数 / 静态成员函数 | 静态成员函数属于类而非对象,无 this 指针,无法访问虚函数表;全局函数不属于类体系。 |
3. 析构函数建议声明为虚函数
- 若基类指针 / 引用指向子类对象,当释放对象时:
- 基类析构函数非虚:仅调用基类析构函数,子类析构函数不执行,导致内存泄漏;
- 基类析构函数为虚:动态联编调用子类析构函数,再自动调用基类析构函数,完成完整释放。
三、多态的实现与使用
1. 多态的核心场景
通过基类指针 / 引用指向子类对象,调用虚函数时自动匹配子类的重写版本:
class Object { public: virtual void show() { cout << "Object::show" << endl; } }; class Test : public Object { public: virtual void show() override { cout << "Test::show" << endl; } }; void print(Object& obj) { // 基类引用 obj.show(); // 动态联编:传入Test对象则调用Test::show } int main() { Test test; print(test); // 输出:Test::show return 0; }2. 风险点:强制类型转换
若将基类指针强制转为子类指针调用子类独有虚函数,但若基类指针实际指向基类对象,会导致未定义行为(内存越界 / 崩溃):
void print(Object* pobj) { ((Test*)pobj)->zero(); // 危险:若pobj指向Object对象,无zero函数,直接崩溃 }3. 虚函数表的可视化(底层验证)
通过手动解析对象内存中的 vfptr 和 vftable,可打印虚函数地址:
typedef void(*func1)(); // 函数指针类型 void Printf_Table(void* obj, int n) { uint64_t** vfptr = (uint64_t**)obj; // 虚表指针(对象首地址) uint64_t* vftable = *vfptr; // 虚函数表首地址 cout << "虚表地址:" << vftable << endl; for (int i = 0; i < n; i++) { func1 f = (func1)vftable[i]; cout << "第" << i << "个虚函数地址:" << (void*)f << endl; } } // 调用示例: Dog dog("dollar", "XiaoDan"); Printf_Table(&dog, 4); // 打印Dog类4个虚函数的地址四、多态的设计意义
- 接口统一:将不同子类的共性行为抽象为基类虚函数(接口),子类重写实现差异化逻辑;
- 扩展性强:新增子类时,无需修改原有调用逻辑(如
print(Object*)),仅需重写虚函数即可适配; - 解耦:调用方仅依赖基类接口,不依赖具体子类,符合 “开闭原则”(对扩展开放,对修改关闭)。
五、示例:动物多态体系
class Animal { // 抽象基类 private: string name; string owner; public: Animal(const string& na, const string& own) : name(na), owner(own) {} virtual ~Animal() = default; // 虚析构函数 virtual void eat() = 0; // 纯虚函数(接口) virtual void walk() = 0; virtual void talk() = 0; }; class Dog : public Animal { public: Dog(const string& na, const string& own) : Animal(na, own) {} void eat() override { cout << "Dog::eat:meat" << endl; } void walk() override { cout << "Dog::walk:quick" << endl; } void talk() override { cout << "Dog::talk:wang wang" << endl; } }; class Cat : public Animal { public: Cat(const string& na, const string& own) : Animal(na, own) {} void eat() override { cout << "Cat::eat:fish" << endl; } void walk() override { cout << "Cat::walk:silent" << endl; } void talk() override { cout << "Cat::talk:miao miao" << endl; } }; // 统一调用接口 void animalBehavior(Animal& animal) { animal.eat(); animal.walk(); animal.talk(); } int main() { Dog dog("Dollar", "XiaoDan"); Cat cat("Money", "XiaoDan"); animalBehavior(dog); // 输出Dog的行为 animalBehavior(cat); // 输出Cat的行为 return 0; }六、关键总结
- 虚函数是动态联编的核心,依赖 vfptr + vftable 实现;
- 多态必须通过 “基类指针 / 引用 + 虚函数重写” 实现;
override关键字提升代码安全性,虚析构函数避免内存泄漏;- 多态的本质是 “接口复用,实现差异化”,是面向对象设计的核心特性。
