C++ 多态详解:从虚函数到虚表
多态让同一个接口根据对象的真实类型表现出不同的行为。理解它需要同时掌握继承、虚函数重写、抽象类以及虚函数表背后的动态绑定机制。一、什么是多态多态可以分为编译时多态和运行时多态。函数重载、函数模板属于编译时多态编译器在编译阶段根据参数类型决定调用哪个函数运行时多态则发生在继承体系中通过基类指针或引用调用虚函数程序运行时根据对象的真实类型选择实现。例如“买票”是同一个行为但普通人购买全价票学生购买优惠票军人可以优先购票。调用方只需要面对统一的 Person 接口具体行为由对象类型决定。二、运行时多态的两个条件运行时多态必须同时满足调用虚函数的表达式是基类指针或基类引用。派生类对基类虚函数完成了重写。#includeiostreamusingnamespacestd;classPerson{public:virtualvoidBuyTicket()const{cout买票-全价endl;}};classStudent:publicPerson{public:voidBuyTicket()constoverride{cout买票-打折endl;}};voidBuy(Personconstperson){person.BuyTicket();}intmain(){Person person;Student student;Buy(person);// 买票-全价Buy(student);// 买票-打折}Buy 的参数类型始终是Person const但传入Student后仍会调用 Student::BuyTicket。这正是动态多态的表现。三、虚函数与重写在成员函数前加virtual该函数就成为虚函数。非成员函数不能使用 virtual。重写要求派生类函数与基类虚函数的函数名、参数列表和返回类型满足匹配规则。派生类即使省略 virtual也可能因为继承关系继续保持虚函数属性但推荐显式写 overrideclassPerson{public:virtualvoidBuyTicket(){cout买票全价endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(inti)override{cout买票半价endl;}};intmain(){return0;}override 会让拼写错误、参数不匹配等问题在编译期暴露。如果不允许继续重写可以使用 finalclassPerson{public:virtualvoidBuyTicket()final{cout买票全价endl;}};classStudent:publicPerson{public:virtualvoidBuyTicket(){cout买票半价endl;}};intmain(){return0;}默认参数不会动态绑定虚函数体可以动态绑定但默认参数在编译期确定#includeiostreamusingnamespacestd;classA{public:virtualvoidFunc(intvalue1){coutA-valueendl;}voidTest(){Func();}};classB:publicA{public:voidFunc(intvalue0)override{coutB-valueendl;}};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:_pendl;delete_p;}protected:int*_pnewint[10];};intmain(){A*p1newA;B*p2newB;deletep1;deletep2;return0;}从代码中我们可以看到如果A ()前不加 virtual那么delete p2只调用的 A 的析构函数没有调用 B 的析构函数就会导致内存泄漏问题因为B () 中在释放资源。五、纯虚函数与抽象类虚函数声明为 0 后成为纯虚函数。包含纯虚函数的类是抽象类不能直接实例化派生类若没有实现全部纯虚函数也仍是抽象类。#includeiostreamusingnamespacestd;classCar{public:virtualvoidDrive()0;virtual~Car()default;};classBenz:publicCar{public:voidDrive()override{coutBenz-舒适endl;}};classBMW:publicCar{public:voidDrive()override{coutBMW-操控endl;}};intmain(){Car car;Car*pBenznewBenz;pBenz-Drive();Car*pBMWnewBMW;pBMW-Drive();return0;}抽象类的价值是定义接口契约它规定派生类必须提供什么行为而不要求基类提供没有意义的默认实现。六、重载、重写与隐藏概念发生位置关键特征重载同一作用域名字相同、参数列表不同编译期决定重写基类与派生类派生类覆盖基类虚函数运行时决定隐藏基类与派生类派生类同名函数隐藏基类同名函数不要求签名相同基类与派生类处于不同作用域同名函数不一定构成重写。如果派生类函数隐藏了基类的其他重载可以使用 using Base::Func; 把基类版本引入当前作用域。七、动态绑定与静态绑定对不满足多态条件(指针或者引用调用虚函数)的函数调用是在编译时绑定也就是编译时确定调用函数的地址叫做静态绑定。满足多态条件的函数调用是在运行时绑定也就是在运行时到指向对象的虚函数表中找到调用函数的地址也就做动态绑定。classBase{public:virtualvoidFunc(){}protected:intvalue1;charmarkx;};// 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 是常见实现方式但具体布局属于平台实现细节。