深入解析C++多态:虚函数与动态联编
一、核心概念静态联编与动态联编1. 静态联编编译期确定定义编译阶段就能确定调用的函数版本也叫早绑定。适用场景普通成员函数调用重载函数匹配默认使用的指针 / 引用调用非虚函数。特点速度快但缺乏灵活性无法适配 “一个接口多种实现” 的多态需求。2. 动态联编运行期确定定义程序运行时才确定调用的函数版本也叫晚绑定是 C 多态的核心实现方式。核心条件函数名相同、参数列表相同、返回值相同协变除外且基类函数声明为virtual。底层原理每个包含虚函数的类会生成虚函数表vftable存储类中所有虚函数的地址类的对象占用的内存首部会包含虚函数表指针vfptr指向所属类的虚函数表调用虚函数时通过对象的 vfptr 找到 vftable再从表中找到对应函数地址执行。二、虚函数的语法规则1. 声明与重写声明在基类成员函数前加virtual关键字仅需基类声明子类重写时virtual可省略但建议显式写。重写override子类重写基类虚函数时必须保证函数签名函数名、参数、const/volatile 限定完全一致C11 新增override关键字显式标注子类重写的虚函数编译器会检查重写合法性如签名不匹配则报错推荐使用。class Object { private: int value; public: Object(int x0):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 x0):Object(x10),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 x0):Base(x10),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关键字提升代码安全性虚析构函数避免内存泄漏多态的本质是 “接口复用实现差异化”是面向对象设计的核心特性。