当前位置: 首页 > news >正文

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

引言

多态(polymorphism)是面向对象编程的三大特性之一,字面意思即“多种形态”。C++ 中的多态分为编译时多态(静态多态)运行时多态(动态多态)。编译时多态主要指函数重载和函数模板,它们在编译阶段根据参数类型或数量确定调用哪个函数。运行时多态则是指在程序运行时,通过基类的指针或引用调用同一个函数名,根据实际指向的对象类型执行不同的行为。本文聚焦于运行时多态,详细阐述其构成条件、虚函数重写、纯虚函数与抽象类、多态的原理(虚函数表与动态绑定),以及常见考点如析构函数重写、override/final 关键字等。


目录

引言

一、多态的概念

二、多态的定义及实现

2.1 多态的构成条件

2.2 虚函数

2.3 虚函数的重写(覆盖)

2.4 多态场景选择题分析

2.5 虚函数重写的特殊情形

2.5.1 协变(Covariance)

2.5.2 析构函数的重写

2.6 override 和 final 关键字(C++11)

2.7 重载、重写、隐藏的对比

三、纯虚函数和抽象类

四、多态的原理

4.1 虚函数表指针(_vfptr)

4.2 多态的实现机制

4.3 虚函数表的内容

4.4 虚函数和虚表的存储位置

五、总结


一、多态的概念

运行时多态的具体表现为:执行某个行为(函数)时,传入不同的对象会完成不同的操作。例如:

  • 买票行为:普通人买票全价,学生买票打折,军人买票优先。

  • 动物叫声:猫对象传入发出“喵”,狗对象传入发出“汪汪”。

这种“同一接口,不同实现”的能力,正是多态的核心价值。


二、多态的定义及实现

2.1 多态的构成条件

要实现运行时多态,必须同时满足两个条件:

  1. 调用方式:必须通过基类的指针引用来调用虚函数。只有基类的指针或引用才能在运行时既指向基类对象又指向派生类对象。

  2. 函数属性:被调用的函数必须是虚函数,并且派生类必须对该虚函数进行重写(覆盖)

2.2 虚函数

在类成员函数声明前加上virtual关键字,该函数即成为虚函数。非成员函数不能加virtual

cpp

class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } };

2.3 虚函数的重写(覆盖)

派生类中有一个与基类虚函数完全相同的函数(返回值类型、函数名、参数列表均相同),则称派生类的虚函数重写了基类的虚函数。

注意:派生类重写时,可以省略virtual关键字。因为基类的虚函数被继承后,在派生类中仍然保持虚函数属性,但为了代码规范,建议显式写出virtual。考试选择题中常利用省略virtual来考察是否构成重写。

cpp

class 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 多态场景选择题分析

题目(来自课件):

cpp

class 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*,所以默认参数使用基类funcval = 1。因此输出B->1

2.5 虚函数重写的特殊情形

2.5.1 协变(Covariance)

派生类重写基类虚函数时,返回值类型可以不同,但必须满足:基类虚函数返回基类对象的指针/引用,派生类虚函数返回派生类对象的指针/引用。这种特性称为协变,实际应用较少。

cpp

class 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派生类对象时,只会调用基类的析构函数,不会调用派生类的析构函数,导致派生类中动态分配的资源无法释放,造成内存泄漏。

cpp

class 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 关键字(C++11)

  • override:显式声明派生类函数重写了基类的虚函数。如果实际未构成重写(如函数名拼写错误、参数列表不同),编译器会报错,避免运行时意外。

  • final:修饰虚函数,禁止派生类重写该函数;或修饰类,禁止该类被继承。

cpp

class 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,该函数即为纯虚函数。纯虚函数通常不需要定义(但语法上允许提供实现)。包含纯虚函数的类称为抽象类,抽象类不能实例化对象。派生类必须重写所有纯虚函数,否则派生类仍然是抽象类。纯虚函数强制派生类实现特定接口。

cpp

class 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

一个含有虚函数的类,其实例化对象中会多出一个指针,称为虚函数表指针_vfptr,v 代表 virtual,f 代表 function)。该指针指向一个虚函数表(简称虚表),虚表中存放该类所有虚函数的地址。

cpp

class 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 多态的实现机制

当通过基类指针或引用调用虚函数时,编译器不会在编译时直接确定函数地址,而是:

  1. 运行时取出指针/引用所指向对象的_vfptr

  2. 从虚表中获取对应的虚函数地址。

  3. 调用该函数。

这就是动态绑定(运行时绑定)。如果不满足多态条件(如通过对象直接调用虚函数,或调用的不是虚函数),则在编译时确定函数地址,称为静态绑定

cpp

void Func(Person* ptr) { ptr->BuyTicket(); // 动态绑定:运行时到ptr指向对象的虚表中查找BuyTicket地址 }

4.3 虚函数表的内容

以如下代码为例:

cpp

class 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::func1&Base::func2,以0结尾(VS 编译器)。

  • 派生类 Derive 的虚表

    • 首先存放继承自 Base 的虚函数地址,但被重写的func1被替换为&Derive::func1

    • 然后存放&Base::func2(未重写)。

    • 最后存放派生类自己的虚函数&Derive::func3

    • 普通成员函数func4func5不在虚表中。

4.4 虚函数和虚表的存储位置

  • 虚函数:和普通函数一样,编译后成为指令,存放在代码段(或常量区)。虚表中存储的是这些函数的地址。

  • 虚表:C++ 标准未规定具体位置。在 VS 编译器中,虚表通常存放在常量区(代码段)。可通过对比栈、堆、静态区、常量区的地址验证。

cpp

int 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)显示虚表地址与常量区地址相近,证明虚表存放在常量区。


五、总结

本文从运行时多态的基本概念出发,系统阐述了其实现所需的两个核心条件(基类指针/引用 + 虚函数重写),并深入分析了虚函数重写的各种细节,包括协变、析构函数重写、默认参数陷阱等。C++11 引入的overridefinal关键字为虚函数重写提供了编译期检查,提高了代码安全性。纯虚函数和抽象类则提供了接口强制实现的机制。

多态的原理基于虚函数表指针和虚表:每个含虚函数的对象都有一个_vfptr指向所属类的虚表,虚表中存放该类所有虚函数的地址。通过基类指针/引用调用虚函数时,运行时动态地从虚表中获取函数地址,实现动态绑定。这种机制使得程序可以在运行时根据实际对象类型决定行为,极大地提升了代码的可扩展性和复用性。

理解多态的内部原理,对于编写正确的继承体系代码、避免内存泄漏(虚析构)、理解动态绑定开销等均有重要意义。在实际工程中,应合理运用多态来设计可扩展的接口,同时注意避免过度复杂的继承层次。

http://www.jsqmd.com/news/760157/

相关文章:

  • 从固件到Shell:逆向分析Netgear R9000 uhttpd漏洞(CVE-2019-20760)的挖掘与修复
  • Heightmapper完整指南:5分钟免费生成专业3D地形高度图
  • 视觉文本分词技术:原理、挑战与应用实践
  • HC-276合金厂商哪家好?东莞附近HC-276合金厂商推荐 - 品牌2026
  • 4J32超因瓦合金怎么选?2026年4J32超因瓦合金厂商推荐 - 品牌2026
  • AI辅助开发进阶:让快马智能生成带炫酷交互的r星赛事官网
  • ESP32 与 Air780E 4G 模块配合做 MQTT 数据传输
  • 从“借书”到“退票”:聊聊UML用例图里那些容易被误解的「包含」与「扩展」关系(附避坑指南)
  • 深入解析driver.page_source:获取动态渲染后的完整页面源码,构建新一代Python爬虫实战
  • oomd:终极用户空间内存杀手指南 - 告别30分钟主机死锁
  • Godot基础之碰撞检测
  • 实战指南:利用快马AI为你的微商城生成会员积分系统模块代码
  • OpenIM Server企业级生产环境部署实战:从架构设计到高可用配置的完整指南
  • 17-4Ph不锈钢厂商推荐哪家?1.4542沉淀硬化不锈钢厂商联系方式 - 品牌2026
  • 用全志F1C200S开发板DIY一个复古游戏机:从刷机到运行模拟器的保姆级教程
  • 5步轻松配置罗技鼠标宏:PUBG压枪技巧终极指南
  • 串口和LCD使用同一队列传递status,多消费者竞争导致 LCD 延迟丢包
  • 在医学图像分割任务中,给UNet加上SK和CBAM模块到底有没有用?我用Refuge数据集实测告诉你
  • 2026最权威的六大AI写作助手实际效果
  • 别再手动调舵机了!用机智云+ESP8266做个手机遥控器,附完整STM32标准库代码
  • 别再手动调LOD了!UE5 Nanite实战:如何一键导入ZBrush高模并优化开放世界地形
  • Android Demos高级UI组件:CarouselFragment与EditTextChips深度解析
  • ESP32与Air780E的MQTT通信如何实现数据的实时传输?
  • 5分钟实现Figma中文界面:设计师必备的界面翻译完整指南
  • 3分钟掌握B站字幕下载:BiliBiliCCSubtitle免费工具全解析
  • MATLAB实战:手把手教你用SLM和PTS算法搞定OFDM信号的高PAPR难题
  • DLSS Swapper:游戏性能智能调优与动态DLL管理解决方案
  • 区块链原理-大白话极简版
  • 别再手动核销了!用uniapp+uQRCode插件5分钟搞定微信扫码核销功能
  • 68万小时音频喂出来的Whisper,真的比无监督预训练强吗?一次深度技术选型分析