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

解密C++多态:一篇文章掌握精髓

目录

1. 什么是多态?

1.1 C++中的多态分为两类

1.2 生活中的多态例子

2. 多态的实现条件

2.1 虚函数与重写

2.1.1 虚函数(Virtual Function)

2.1.2 虚函数的重写(Override)

2.2 多态调用示例

3. 多态的实现原理

3.1 虚函数表指针(vptr)

4.2虚函数表

4. 动态绑定与静态绑定

5. 特殊情况与关键字

5.1 协变(Covariant)

5.2 析构函数的重写

5.3 override 和 final 关键字

6. 重载、重写、隐藏的对比

7. 纯虚函数与抽象类

8. 总结


1. 什么是多态?

多态(Polymorphism) 是面向对象编程的三大特性之一(另外两个是封装继承)。通俗来讲,多态就是“一个接口,多种实现”。即同一个行为或函数,在不同对象上表现出不同的形态。

1.1 C++中的多态分为两类

  • 编译时多态(静态多态:包括函数重载和模板。在编译阶段就确定了调用哪个函数。

  • 运行时多态(动态多态:通过虚函数机制实现,在程序运行时根据对象的实际类型来决定调用哪个函数。

本文将重点讲解运行时多态

1.2 生活中的多态例子

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

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

2. 多态的实现条件

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

  1. 必须使用基类的指针或引用来调用虚函数

  2. 被调用的函数必须是虚函数,并且在派生类中完成了重写(覆盖)

说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

2.1 虚函数与重写

2.1.1 虚函数(Virtual Function)

在成员函数前加上 virtual 关键字,该函数就成为虚函数。注意非成员函数不能加virtual修饰。

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

2.1.2 虚函数的重写(Override)

派生类中定义一个与基类虚函数完全相同(函数名、参数列表、返回值相同)的虚函数,称为重写或覆盖。

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

2.2 多态调用示例

多态是面向对象编程的重要特性之一,它允许我们通过基类指针或引用来调用派生类的函数。下面是一个完整的多态调用示例:

class Shape {
public:virtual double area() const = 0; // 纯虚函数
};class Circle : public Shape {double radius;
public:Circle(double r) : radius(r) {}double area() const override {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}
};void printArea(const Shape& shape) {cout << "面积: " << shape.area() << endl;
}int main() {Circle c(5.0);Rectangle r(4.0, 6.0);printArea(c);  // 输出圆的面积printArea(r);  // 输出矩形的面积return 0;
}

这个示例展示了:

  • 纯虚函数和抽象类

  • 通过引用实现多态

  • 带参数的多态方法调用

3. 多态的实现原理

3.1 虚函数表指针(vptr)

每个含有虚函数的类(或有虚函数的基类)都会有一个虚函数表指针,指向该类的虚函数表。虚表中存放的是该类所有虚函数的地址。

class Base {
public:virtual void func1() {}virtual void func2() {}int a = 1;
};int main() {Base b;cout << sizeof(b) << endl;  // 输出可能是12(32位系统)return 0;
}

上面代码运行结果12bytes,除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.2虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。

  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。

  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

  • 派生类的虚函数表中包含:(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。

  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)

  • 虚函数存在哪的?虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。

  • 虚函数表存在哪的?这个问题严格说并没有标准答案,C++标准并没有规定,我们写下面的代码可以对比验证一下。vs下是存在代码段(常量区)

这里Derive中没有看到func3函数,这个vs监视窗口看不到,可以通过内存窗口查看

class Derive : public Base {
public:virtual void func1() override { cout << "Derive::func1"; }virtual void func3() { cout << "Derive::func3"; }
};

4. 动态绑定与静态绑定

  • 静态绑定:在编译时确定函数地址,如普通函数调用、重载函数。

  • 动态绑定:在运行时通过虚函数表查找函数地址,实现多态。

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

5. 特殊情况与关键字

5.1 协变(Covariant)

允许派生类虚函数的返回值类型为基类虚函数返回类型的派生类指针或引用。

class A {};
class B : public A {};class Person {
public:virtual A* BuyTicket() { return nullptr; }
};class Student : public Person {
public:virtual B* BuyTicket() override { return nullptr; }
};

5.2 析构函数的重写

建议将基类的析构函数声明为虚函数,否则通过基类指针删除派生类对象时,只会调用基类析构函数,导致内存泄漏。

class A {
public:virtual ~A() { cout << "~A()"; }
};class B : public A {
public:~B() { cout << "~B()"; delete[] _p; }
private:int* _p = new int[10];
};// 使用:
A* p = new B;
delete p;  // 正确调用 ~B() 和 ~A()

5.3 override 和 final 关键字

从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错、参数写错等,导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

  • override 显式声明重写,编译器会检查是否成功重写。

  • final 禁止后续派生类重写该虚函数。

class Car {
public:virtual void Drive() final {}  // 禁止重写
};class Benz : public Car {
public:virtual void Drive() override {} // 错误:无法重写final函数
};

6. 重载、重写、隐藏的对比

类型作用域函数要求关键字
重载同一类函数名相同,参数不同
重写基类与派生类函数名、参数、返回值相同virtual
隐藏基类与派生类函数名相同,不构成重写

7. 纯虚函数与抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现。((实现没什么意义,因为要被派生类重写,但是语法上可以实现)只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承若不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就实例化不出对象。

class Car {
public:virtual void Drive() = 0;  // 纯虚函数
};class Benz : public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};// Car car;  // 错误:抽象类不能实例化
Car* p = new Benz;  // 正确:多态使用

8. 总结

特性说明
多态类型编译时多态(重载、模板) vs 运行时多态(虚函数)
实现条件基类指针/引用 + 虚函数重写
底层机制虚函数表(vtable) + 虚函数表指针(vptr)
关键字virtualoverridefinal
应用场景接口抽象、动态行为选择、资源安全释放

参考资料

  • C++ Primer, 5th Edition

  • Effective C++, Scott Meyers

http://www.dtcms.com/a/419314.html

相关文章:

  • Git 进阶指南:深入掌握 git log 查看提交历史
  • C++ 引用协程
  • 淄博企业网站设计公司网页无法打开怎么办
  • 添加测试设备到苹果开发者平台
  • 填坑:VC++ 采用OpenSSL 3.0接口方式生成RSA密钥
  • 郑州做网站的网站再就业技能培训班
  • Vscode 连接服务时候一直出现setting ssh Host server
  • 全面解析数据库审批平台:主流工具对比与选型指南
  • 【Docker项目实战】使用Docker部署IT运维管理平台CAT
  • spring事务传播级别的实操案例2
  • 泰州专一做淘宝网站如何用html做网站头像
  • 电子商务网站设计与实现个人网站做捐赠发布违法吗
  • Java滑动窗口算法题目练习
  • 介绍一下HTTP和WebSocket的头部信息
  • Linux系统学习之---库的理解和加载(毛坯初版...)
  • 南山模板网站建设公司怎么看网站的外链
  • 企业网站策划大纲模板文山住房和城乡建设局网站
  • Linux 基础IO与系统IO
  • 【IEDA】已解决:IDEA中jdk的版本切换
  • idea推荐springboot+mybatis+分页查询插件之PageHelper
  • 南非网站域名做网站微信支付多少钱
  • 网站开发 图形验证码网站建设衤金手指下拉10
  • OPenssh6代码移植的依赖库 OpenSSL双库连接问题的解决方案
  • 商务网站建设组成包括网站优化wordpress 换行
  • tiktok scheme
  • Xrdp 远程桌面配置【笔记】
  • 【Linux】倒计时和进度条实现
  • 网站建设需要用到哪些软件有哪些系统安装wordpress
  • 梯度下降(Gradient Descent)
  • 东莞市建设规划局网站游戏类企业网站模板