C++ 多态详解:从虚函数到虚表
2026/9/4 7:16:39 网站建设 项目流程

多态让同一个接口根据对象的真实类型表现出不同的行为。理解它,需要同时掌握继承、虚函数重写、抽象类以及虚函数表背后的动态绑定机制。

一、什么是多态

多态可以分为编译时多态和运行时多态。函数重载、函数模板属于编译时多态,编译器在编译阶段根据参数类型决定调用哪个函数;运行时多态则发生在继承体系中,通过基类指针或引用调用虚函数,程序运行时根据对象的真实类型选择实现。

例如“买票”是同一个行为,但普通人购买全价票,学生购买优惠票,军人可以优先购票。调用方只需要面对统一的 Person 接口,具体行为由对象类型决定。

二、运行时多态的两个条件

运行时多态必须同时满足:

  1. 调用虚函数的表达式是基类指针或基类引用。
  2. 派生类对基类虚函数完成了重写。
#include<iostream>usingnamespacestd;classPerson{public:virtualvoidBuyTicket()const{cout<<"买票-全价"<<endl;}};classStudent:publicPerson{public:voidBuyTicket()constoverride{cout<<"买票-打折"<<endl;}};voidBuy(Personconst&person){person.BuyTicket();}intmain(){Person person;Student student;Buy(person);// 买票-全价Buy(student);// 买票-打折}

Buy 的参数类型始终是Person const&,但传入Student后仍会调用 Student::BuyTicket。这正是动态多态的表现。

三、虚函数与重写

成员函数前加virtual,该函数就成为虚函数非成员函数不能使用 virtual。重写要求派生类函数与基类虚函数的函数名、参数列表和返回类型满足匹配规则。

派生类即使省略 virtual,也可能因为继承关系继续保持虚函数属性,但推荐显式写 override:

classPerson{public:virtualvoidBuyTicket(){cout<<"买票全价"<<endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(inti)override{cout<<"买票半价"<<endl;}};intmain(){return0;}

override 会让拼写错误、参数不匹配等问题在编译期暴露。如果不允许继续重写,可以使用 final:

classPerson{public:virtualvoidBuyTicket()final{cout<<"买票全价"<<endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(){cout<<"买票半价"<<endl;}};intmain(){return0;}

默认参数不会动态绑定

虚函数体可以动态绑定,但默认参数在编译期确定:

#include<iostream>usingnamespacestd;classA{public:virtualvoidFunc(intvalue=1){cout<<"A->"<<value<<endl;}voidTest(){Func();}};classB:publicA{public:voidFunc(intvalue=0)override{cout<<"B->"<<value<<endl;}};intmain(){B object;object.Test();// B->1}

Test 中的默认参数来自 A::Func 的声明,随后函数体才通过虚表跳转到 B::Func。因此不应在虚函数层级中为同一参数设置不同默认值。

协变返回类型

如果基类虚函数返回 A* 或 A&,派生类重写时可以返回 B* 或 B&,前提是 B 派生自 A。这称为协变返回类型,实际接口中应谨慎使用并保持语义清晰。

四、析构函数的重写

基类的析构函数为虚函数时,派生类析构函数只要定义,不管写不写 virtual 关键字,都会和基类析构函数构成重写。
表面上基类和派生类析构函数名字不一样,不满足普通虚函数重写的命名要求,但是编译器会对析构函数名字做特殊处理,编译之后全部统一命名为 destructor。
只要基类析构带上 virtual,派生类析构自动完成重写。

classA{public:virtual~A(){cout<<"~A()"<<endl;}};classB:publicA{public:~B(){cout<<"~B()->delete:"<<_p<<endl;delete_p;}protected:int*_p=newint[10];};intmain(){A*p1=newA;B*p2=newB;deletep1;deletep2;return0;}


从代码中我们可以看到,如果~A ()前不加 virtual,那么delete p2只调用的 A 的析构函数,没有调用 B 的析构函数,就会导致内存泄漏问题,因为~B () 中在释放资源。

五、纯虚函数与抽象类

虚函数声明为 = 0 后成为纯虚函数。包含纯虚函数的类是抽象类,不能直接实例化;派生类若没有实现全部纯虚函数,也仍是抽象类。

#include<iostream>usingnamespacestd;classCar{public:virtualvoidDrive()=0;virtual~Car()=default;};classBenz:publicCar{public:voidDrive()override{cout<<"Benz-舒适"<<endl;}};classBMW:publicCar{public:voidDrive()override{cout<<"BMW-操控"<<endl;}};intmain(){Car car;Car*pBenz=newBenz;pBenz->Drive();Car*pBMW=newBMW;pBMW->Drive();return0;}

抽象类的价值是定义接口契约:它规定派生类必须提供什么行为,而不要求基类提供没有意义的默认实现。

六、重载、重写与隐藏

概念发生位置关键特征
重载同一作用域名字相同、参数列表不同,编译期决定
重写基类与派生类派生类覆盖基类虚函数,运行时决定
隐藏基类与派生类派生类同名函数隐藏基类同名函数,不要求签名相同

基类与派生类处于不同作用域,同名函数不一定构成重写。如果派生类函数隐藏了基类的其他重载,可以使用 using Base::Func; 把基类版本引入当前作用域。

七、动态绑定与静态绑定

对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

classBase{public:virtualvoidFunc(){}protected:intvalue=1;charmark='x';};
// ptr是指针+BuyTicket是虚函数满⾜多态条件。// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址ptr->BuyTicket();00EF2001 mov eax,dword ptr[ptr]00EF2004 mov edx,dword ptr[eax]00EF2006 mov esi,esp00EF2008 mov ecx,dword ptr[ptr]00EF200B mov eax,dword ptr[edx]00EF200D call eax// BuyTicket不是虚函数,不满⾜多态条件。// 这⾥就是静态绑定,编译器直接确定调⽤函数地址ptr->BuyTicket();00EA2C91 mov ecx,dword ptr[ptr]00EA2C94 callStudent::Student(0EA153Ch)

八、总结

  • 重载和模板属于编译时多态,虚函数体系实现运行时多态。
  • 运行时多态的核心条件是基类指针或引用、虚函数和正确重写。
  • override 检查重写意图,final 禁止继续重写。
  • 默认参数静态绑定,虚函数体动态绑定。
  • 多态基类应提供虚析构函数。
  • 纯虚函数定义接口契约,抽象类不能直接实例化。
  • 虚表和 vptr 是常见实现方式,但具体布局属于平台实现细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询