C++ 多态机制完全解析:从虚函数重写到动态绑定原理

张开发
2026/5/5 23:38:47 15 分钟阅读

分享文章

C++ 多态机制完全解析:从虚函数重写到动态绑定原理
引言多态polymorphism是面向对象编程的三大特性之一字面意思即“多种形态”。C 中的多态分为编译时多态静态多态和运行时多态动态多态。编译时多态主要指函数重载和函数模板它们在编译阶段根据参数类型或数量确定调用哪个函数。运行时多态则是指在程序运行时通过基类的指针或引用调用同一个函数名根据实际指向的对象类型执行不同的行为。本文聚焦于运行时多态详细阐述其构成条件、虚函数重写、纯虚函数与抽象类、多态的原理虚函数表与动态绑定以及常见考点如析构函数重写、override/final 关键字等。目录引言一、多态的概念二、多态的定义及实现2.1 多态的构成条件2.2 虚函数2.3 虚函数的重写覆盖2.4 多态场景选择题分析2.5 虚函数重写的特殊情形2.5.1 协变Covariance2.5.2 析构函数的重写2.6 override 和 final 关键字C112.7 重载、重写、隐藏的对比三、纯虚函数和抽象类四、多态的原理4.1 虚函数表指针_vfptr4.2 多态的实现机制4.3 虚函数表的内容4.4 虚函数和虚表的存储位置五、总结一、多态的概念运行时多态的具体表现为执行某个行为函数时传入不同的对象会完成不同的操作。例如买票行为普通人买票全价学生买票打折军人买票优先。动物叫声猫对象传入发出“喵”狗对象传入发出“汪汪”。这种“同一接口不同实现”的能力正是多态的核心价值。二、多态的定义及实现2.1 多态的构成条件要实现运行时多态必须同时满足两个条件调用方式必须通过基类的指针或引用来调用虚函数。只有基类的指针或引用才能在运行时既指向基类对象又指向派生类对象。函数属性被调用的函数必须是虚函数并且派生类必须对该虚函数进行重写覆盖。2.2 虚函数在类成员函数声明前加上virtual关键字该函数即成为虚函数。非成员函数不能加virtual。cppclass Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } };2.3 虚函数的重写覆盖派生类中有一个与基类虚函数完全相同的函数返回值类型、函数名、参数列表均相同则称派生类的虚函数重写了基类的虚函数。注意派生类重写时可以省略virtual关键字。因为基类的虚函数被继承后在派生类中仍然保持虚函数属性但为了代码规范建议显式写出virtual。考试选择题中常利用省略virtual来考察是否构成重写。cppclass Person { public: virtual void BuyTicket() { cout 买票-全价 endl; } }; class Student : public Person { public: virtual void BuyTicket() { cout 买票-打折 endl; } // 重写 }; void Func(Person* ptr) { ptr-BuyTicket(); // 多态调用由ptr指向的对象决定调用哪个版本 } int main() { Person ps; Student st; Func(ps); // 输出买票-全价 Func(st); // 输出买票-打折 return 0; }2.4 多态场景选择题分析题目来自课件cppclass A { public: virtual void func(int val 1) { std::cout A- val std::endl; } virtual void test() { func(); } }; class B : public A { public: void func(int val 0) { std::cout B- val std::endl; } }; int main() { B* p new B; p-test(); return 0; }输出结果B-1解析p-test()调用从A继承来的test()函数B未重写test。test()内部调用func()由于func是虚函数且通过this指针相当于基类指针调用满足多态条件因此调用B中重写的func。关键陷阱虚函数的重写只覆盖函数体不覆盖默认参数。默认参数在编译阶段根据调用者的静态类型确定。此处test()是在A中定义的编译时this的类型是A*所以默认参数使用基类func的val 1。因此输出B-1。2.5 虚函数重写的特殊情形2.5.1 协变Covariance派生类重写基类虚函数时返回值类型可以不同但必须满足基类虚函数返回基类对象的指针/引用派生类虚函数返回派生类对象的指针/引用。这种特性称为协变实际应用较少。cppclass A {}; class B : public A {}; class Person { public: virtual A* BuyTicket() { cout 买票-全价 endl; return nullptr; } }; class Student : public Person { public: virtual B* BuyTicket() { cout 买票-打折 endl; return nullptr; } };2.5.2 析构函数的重写基类的析构函数建议定义为虚函数。虽然基类和派生类的析构函数名称不同~Personvs~Student但编译器会将所有析构函数名统一处理为destructor。因此只要基类析构函数是虚函数派生类的析构函数无论是否加virtual都与基类析构函数构成重写。重要性若基类析构函数不是虚函数则通过基类指针delete派生类对象时只会调用基类的析构函数不会调用派生类的析构函数导致派生类中动态分配的资源无法释放造成内存泄漏。cppclass A { public: virtual ~A() { cout ~A() endl; } // 虚析构 }; class B : public A { public: ~B() { cout ~B() endl; delete[] _p; } private: int* _p new int[10]; }; int main() { A* p2 new B; delete p2; // 先调用~B()再调用~A() return 0; }2.6 override 和 final 关键字C11override显式声明派生类函数重写了基类的虚函数。如果实际未构成重写如函数名拼写错误、参数列表不同编译器会报错避免运行时意外。final修饰虚函数禁止派生类重写该函数或修饰类禁止该类被继承。cppclass Car { public: virtual void Drive() {} }; class Benz : public Car { public: virtual void Drive() override { cout Benz-舒适 endl; } // 正确重写 }; class Car2 { public: virtual void Drive() final {} // 禁止重写 }; class Benz2 : public Car2 { public: virtual void Drive() {} // 编译错误无法重写final函数 };2.7 重载、重写、隐藏的对比比较项重载Overload重写/覆盖Override隐藏Hide作用范围同一类中基类和派生类之间基类和派生类之间函数名相同相同相同参数列表不同类型、个数、顺序完全相同协变除外可以相同也可以不同返回值无要求相同或协变无要求virtual不需要基类必须加virtual派生类可加可不加不需要访问方式编译时决定运行时多态基类指针/引用调用派生类对象直接调用时隐藏基类同名成员三、纯虚函数和抽象类在虚函数声明后加上 0该函数即为纯虚函数。纯虚函数通常不需要定义但语法上允许提供实现。包含纯虚函数的类称为抽象类抽象类不能实例化对象。派生类必须重写所有纯虚函数否则派生类仍然是抽象类。纯虚函数强制派生类实现特定接口。cppclass Car { public: virtual void Drive() 0; // 纯虚函数 }; class Benz : public Car { public: virtual void Drive() override { cout Benz-舒适 endl; } }; class BMW : public Car { public: virtual void Drive() override { cout BMW-操控 endl; } }; int main() { // Car car; // 错误抽象类不能实例化 Car* pBenz new Benz; pBenz-Drive(); Car* pBMW new BMW; pBMW-Drive(); return 0; }四、多态的原理4.1 虚函数表指针_vfptr一个含有虚函数的类其实例化对象中会多出一个指针称为虚函数表指针_vfptrv 代表 virtualf 代表 function。该指针指向一个虚函数表简称虚表虚表中存放该类所有虚函数的地址。cppclass Base { public: virtual void Func1() { cout Func1() endl; } protected: int _b 1; char _ch x; }; int main() { Base b; cout sizeof(b) endl; // 在32位平台下通常为12字节_b(4) _ch(1) 对齐(3) _vfptr(4) return 0; }同一个类的不同对象共享同一张虚表。派生类对象中包含基类部分基类部分的虚表指针与基类对象的虚表指针不是同一个但指向的虚表内容不同。4.2 多态的实现机制当通过基类指针或引用调用虚函数时编译器不会在编译时直接确定函数地址而是运行时取出指针/引用所指向对象的_vfptr。从虚表中获取对应的虚函数地址。调用该函数。这就是动态绑定运行时绑定。如果不满足多态条件如通过对象直接调用虚函数或调用的不是虚函数则在编译时确定函数地址称为静态绑定。cppvoid Func(Person* ptr) { ptr-BuyTicket(); // 动态绑定运行时到ptr指向对象的虚表中查找BuyTicket地址 }4.3 虚函数表的内容以如下代码为例cppclass Base { public: virtual void func1() { cout Base::func1 endl; } virtual void func2() { cout Base::func2 endl; } void func5() { cout Base::func5 endl; } protected: int a 1; }; class Derive : public Base { public: virtual void func1() override { cout Derive::func1 endl; } // 重写 virtual void func3() { cout Derive::func3 endl; } void func4() { cout Derive::func4 endl; } protected: int b 2; };虚表结构基类 Base 的虚表存放Base::func1Base::func2以0结尾VS 编译器。派生类 Derive 的虚表首先存放继承自 Base 的虚函数地址但被重写的func1被替换为Derive::func1。然后存放Base::func2未重写。最后存放派生类自己的虚函数Derive::func3。普通成员函数func4、func5不在虚表中。4.4 虚函数和虚表的存储位置虚函数和普通函数一样编译后成为指令存放在代码段或常量区。虚表中存储的是这些函数的地址。虚表C 标准未规定具体位置。在 VS 编译器中虚表通常存放在常量区代码段。可通过对比栈、堆、静态区、常量区的地址验证。cppint main() { int i 0; // 栈 static int j 1; // 静态区 int* p1 new int; // 堆 const char* p2 xxxx; // 常量区 Base b; printf(栈:%p\n, i); printf(静态区:%p\n, j); printf(堆:%p\n, p1); printf(常量区:%p\n, p2); printf(Base虚表地址:%p\n, *(int**)b); // 虚表地址 printf(虚函数地址:%p\n, Base::func1); printf(普通函数地址:%p\n, Base::func5); return 0; }运行结果示例VS显示虚表地址与常量区地址相近证明虚表存放在常量区。五、总结本文从运行时多态的基本概念出发系统阐述了其实现所需的两个核心条件基类指针/引用 虚函数重写并深入分析了虚函数重写的各种细节包括协变、析构函数重写、默认参数陷阱等。C11 引入的override和final关键字为虚函数重写提供了编译期检查提高了代码安全性。纯虚函数和抽象类则提供了接口强制实现的机制。多态的原理基于虚函数表指针和虚表每个含虚函数的对象都有一个_vfptr指向所属类的虚表虚表中存放该类所有虚函数的地址。通过基类指针/引用调用虚函数时运行时动态地从虚表中获取函数地址实现动态绑定。这种机制使得程序可以在运行时根据实际对象类型决定行为极大地提升了代码的可扩展性和复用性。理解多态的内部原理对于编写正确的继承体系代码、避免内存泄漏虚析构、理解动态绑定开销等均有重要意义。在实际工程中应合理运用多态来设计可扩展的接口同时注意避免过度复杂的继承层次。

更多文章