【C++哲学】面向对象的三大特性之 多态 - 实践

news/2025/10/4 17:51:19/文章来源:https://www.cnblogs.com/wzzkaifa/p/19125782

                                                        拾Ծ光:个人主页

欢迎来到我的专栏:《C++》,《数据结构》,《C语言》

目录

一、多态的概念及实现

1、什么是多态?

2、虚函数

• 虚函数的重写/覆盖

• override和final关键字

3、多态的实现⭐️

• 多态实现的条件

• 多态场景下的一个经典面试题

4、虚函数重写的特殊场景

• 析构函数的重写⭐️

• 重载/重写/隐藏的区别

• 协变(了解)

5、纯虚函数和抽象类

二、多态的原理‼️

1、虚函数表指针

2、虚函数表

3、多态的底层实现

4、动态绑定和静态绑定

三、总结

一、多态的概念及实现

1、什么是多态?

多态顾名思义就是有多种形态。多态是C++面向对象编程的最重要的特性之一,多态分为:编译时多态(静态多态)运行时多 态(动态多态),这里我们重点讲运行时多态。编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过不同的参数类型来达到多种形态。而之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时一般归为静态,运行时归为动态。

运行时多态,具体点讲就是去完成某个行为(函数),可以传不同的对象来完成不同的行为,就达到了多种形态。比如买票这个行为,当对象是普通人时,是全价买票;对象是学生买时,是优惠买票(5折或75折);当对象是军人时,是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是”(>^ω^<) 喵“,传狗对象过去,就是"汪汪"。

2、虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

class Person {
public:
// 这里在成员函数BuyTicket前面加了virtual,则BuyTicket就是虚函数
virtual void BuyTicket()
{cout << "买票-全价" << endl;
}
};
• 虚函数的重写/覆盖

虚函数的重写 (Override) - 发生在派生类,在基类中一定要有一个被virtual修饰的虚函数。

重写是实现多态的关键。它的定义是:在派生类中提供一个与基类中某个虚函数具有完全相同函数名、参数列表、返回值类型的函数。

虚函数重写的目就是为了在运行时,当通过基类指针或引用指向派生类对象时,能够调用派生类的版本,而不是基类的版本。这就是 “动态绑定”。

//---------------------Person类与Student类中的buyticket构成重写------------------------------
class Person {
public:virtual void buyticket(){cout << "全价票" << endl;}
};
class Student:public Person {
public:virtual void buyticket(){cout << "半价票" << endl;}
};

还需要注意一点:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(因为子类继承基类后,基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

//---------------------A类与B类中的func构成重写------------------------------
class A {
public:virtual void func(){}
};
class B:public A {
public:void func(){}
};
• override和final关键字

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

class Person {
public:virtual void buyticket(){}
};
class Student:public Person {
public:virtual void buyticket()override{}
};

如果我们不想让 派生类重写这个虚函数,那么可以用final去修饰。

3、多态的实现⭐️

• 多态实现的条件

必须是基类的指针或者引用调用虚函数

被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

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

//-------------------------------------基类----------------------------------------------
class Person {
public:virtual void buyticket(){cout << "全价票" << endl;}
};
//-------------------------------------派生类----------------------------------------------
class Student :public Person {
public:virtual void buyticket() override{cout << "半价票" << endl;}
};
void Func(Person* ptr) //基类的指针来接收参数
{ptr->buyticket();  // 用基类的指针调用虚函数
}
int main()
{Person p;  // 基类对象Student s; // 派生类对象Func(&p);  // 将基类对象作为实参,则调用基类的虚函数Func(&s);  // 将派生类对象作为实参,则调用派生类的虚函数return 0;
}

• 多态场景下的一个经典面试题

以下程序输出结果是什么()

A: A->0     B: B->1         C: A->1         D: B->0         E: 编译出错       F: 以上都不正确

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(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

答案:B

我们也许都会对答案感到不可思议,这不就应该从A或者D中选一个吗?为什么最后的答案是B呢,下面我们来分析:

首先题目中创建了一个指向B类类型对象的指针p,通过指针p来调用test函数,由于test函数是基类A的成员函数,所以隐含的参数this指针是A*类型的(即基类的指针),同时基类与子类中func函数构成虚函数的重写,虽然,派生类的func函数并没有加virtual关键字,但是由于继承的原因,任然构成重写,所以满足多态的条件,此时,就会根据真正的实参的类型来决定调用哪个虚函数(动态绑定),而指针p是派生类类型的,所以,就会调用派生类B中 的func函数,这时候我们肯定就会毫不犹豫的选择D选项,相信大多数人都能分析到这一步,

但是,最终的答案确选B,这就是这个题经典的原因之一,也是最坑的地方之一 。因为还有一点就是:

• 默认参数是静态绑定的(编译时根据调用者的类决定):test()属于基类A,因此 func() 的默认参数 val会使用 A 中声明的 val = 1(而非 B 中声明的 val = 0)。

也可以理解为(形象),虚函数重写后,派生类中的虚函数其实是由基类虚函数的函数名和参数列表与派生类的虚函数的实现部分组成的。

所以,选择B:B -> 1

4、虚函数重写的特殊场景

• 析构函数的重写⭐️

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了 vialtual修饰,派生类的析构函数就构成重写。

那么问题就来了,为什么要这样设计呢?这个问题面试中经常考察,大家一定要结合类似下面的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数。

当一个基类的指针 ptr 指向派生类对象时,如果基类的析构函数不实现为虚函数,那么当delete ptr释放该对象时,由于静态绑定(ptr是什么类类型的指针就调用哪个类的析构函数),就只调用基类的析构函数,如果派生类对象中有额外的资源,这时候就会导致内存泄漏。所以,C++中只要基类的析构函数实现为虚函数,无论派生类析构函数加不加virtual关键字,都会与基类的析构函数构成重写,即此时满足多态编译器会根据ptr指针真正指向的类类型对象来调用相应的析构函数(动态绑定),而在前面的继承中已经讲到:子类的析构函数会自动调用基类的析构函数,所以,此时即使基类中有资源,也会被释放。

//-------------------------------------基类---------------------------------------------
class A {
public:virtual ~A() // 虚函数{}
protected:int _a;
};
//-------------------------------------子类---------------------------------------------
class B :public A {
public:~B(){delete[]_b;_b = nullptr;}
protected:int* _b = new int[10]; // 派生类中有额外的资源
};
// 用一个基类的指针指向派生类对象
int main()
{A* a1 = new B;delete a1;return 0;
}

可视化分析:

• 重载/重写/隐藏的区别

这个问题面试中也有可能会考到:

• 协变(了解)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class Person {
public:virtual Person* BuyTicket()     // 返回值为基类的指针{cout << "买票-全价" << endl;return nullptr;}
};
class Student : public Person {
public:virtual Student* BuyTicket()    // 返回值为派生类的指针{cout << "买票-打折" << endl;return nullptr;}
};
void Func(Person* ptr)
{ptr->BuyTicket();
}
int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

5、纯虚函数和抽象类

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

//-------------------------------------基类(抽象类)-------------------------------------
class Car {
public:virtual void Drive() = 0; // 纯虚函数
};
//-------------------------------------派生类---------------------------------------------
class Benz :public Car {
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car {
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
int main()
{//-----------------错误示范:编译报错:error C2259: “Car”: 无法实例化抽象类-------------// Car car;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

二、多态的原理‼️

1、虚函数表指针

下面编译为32位程序的运行结果是什么()

A. 编译报错           B. 运行报错            C. 8            D. 12

class Base {
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl; // 计算b对象的大小return 0;
}

答案:D

看到这个题,我们肯定就会选C,因为一个int占4个字节,一个char占1个字节,然后对齐到4的整数倍,不就是8吗。但是,在b对象中不仅仅放了_b和_ch成员,还有一个_vfptr的指针在这两个成员的前面,我们知道在32位机器下一个指针占4个字节,所以最后b对象的大小就是12个字节。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

2、虚函数表

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

class A {
public:virtual void func(){}
};
class B :public A {
public:virtual void func(){}
};
class C :public A {
public:virtual void func(){}
};
int main()
{A a1; // 基类A a2; // 基类B b1; // 派生类B b2; // 派生类C c; // 派生类return 0;
}

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

派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函 数地址。这也是为什么,当满足多态时编译器会根据真正的对象调用相应的虚函数的原因。就是因为,派生类与基类的虚函数表中放着各自独立的虚函数的指针。

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

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:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};
int main()
{Base b;Derive d;return 0;
}

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

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

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }
};
class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }
};
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
// 输出结果:
栈:009BFB94
静态区:0017D000
堆:00E4F500
常量区:0017AB94
Person虚表地址:0017AB34
Student虚表地址:0017AB74
虚函数地址:0017148D
普通函数地址:001714C9

可以看到,Person虚表地址与Student虚表地址更加接近常量区

3、多态的底层实现

满足多态条件后,底层 不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函 数。

class Person {
public:virtual void buyticket(){cout << "全价票" << endl;}
protected:string _name;
};
class Student :public Person {
public:virtual void buyticket(){cout << "半价票" << endl;}
protected:int _id;
};
class Soldier :public Person {
public:virtual void buyticket(){cout << "优先买票" << endl;}
protected:int _codename;
};

4、动态绑定和静态绑定

• 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定

• 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数 的地址,也就做动态绑定

三、总结

多态是C++面向对象编程非常重要的一个特性,多态在我们处理一些具有相似特性的问题时,有着非常重要的作用,同时,在面试中也有许多的考点和细节。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/927404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025CSP-S模拟赛58 比赛总结

2025CSP-S模拟赛58T1 T2 T3 T4总分:;排名:。 不知道为什么又在无意义罚坐。真的是把最显然的那一档打完之后就啥也不会做了,啥也想不出来。

国外优秀的html5网站网站发展趋势

日志对于一些大一些的项目来说&#xff0c;可以在项目运行出现问题时更好的帮助 项目的维护人员快速的定位到问题出现的地方并且知道出现问题的原因&#xff0c; 并且日志也可以帮助程序员很好的进行项目的Debug&#xff0c;那么今天我就来实 现一个C编写的一个简单的日志功能。…

建设网官方网站工商执照查询官网

目录 一、SASL介绍 二、使用 SASL 进行身份验证 2.1 服务器到服务器的身份验证 2.2 客户端到服务器身份验证 三、验证功能 一、SASL介绍 默认情况下&#xff0c;ZooKeeper 不使用任何形式的身份验证并允许匿名连接。但是&#xff0c;它支持 Java 身份验证与授权服务(JAAS)…

精读C++设计模式20 —— 结构型设计模式:桥接模式 - 详解

精读C++设计模式20 —— 结构型设计模式:桥接模式 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

用纯.NET开发并制作一个智能桌面机器人(六):使用.NET开发一个跨平台功能完善的小智AI客户端

前言 前面几篇文章已经把机器人硬件控制部分的开发讲得差不多了,包括屏幕控制、舵机驱动、语音交互等功能。但是之前的外形太过简单,可动角度不够多,所以我就新改进了一个版本,叫VerdiBot(阿荫),详细视频介绍地…

潍坊网站开发公司wordpress伪原创设置

教程-上传应用公钥并获取支付宝公钥 1. 点击签名验签工具右下角的“上传公钥”会打开支付宝开放平台网页&#xff0c;输入账号登录。&#xff08;建议使用IE或Chrome浏览器。) 2. 在“我的应用”中&#xff0c;选择要配置密钥的应用&#xff0c;点击“查看”。记录对应的APPID…

肥西县住房和城乡建设局网站台州网站制作开发

很久没写过php代码了&#xff0c;二开过程中笔记如下 注意事项 打开APP_DEBUG 关于建表 .在store进行开发&#xff0c;新建表的时候需要加上store_id字段 增加页面 前端页面 增加新的菜单&#xff0c;需要在router.config.js中增加对应的配置增加新的页面&#xff0c;需要…

Gateway-过滤器 - 教程

Gateway-过滤器 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &quo…

如何构建一个成交型网站响应式网站建设有哪些好处

一个有效的安全事件响应策略的关键组成部分有哪些&#xff1f;一个有效的安全事件响应策略包括四个关键组成部分&#xff0c;它们协同工作以确保对网络安全问题的快速和有效响应。 一个有效的安全事件响应策略的关键组成部分有哪些&#xff1f; 一个有效的安全事件响应策略包括…

公司建站系统企业网盘推荐

文章目录 一、简介二、OS认证三、口令认证四、remote_login_passwordfile 详解 一、简介 在数据库管理中&#xff0c;登录认证是确保数据库安全性的重要环节。Oracle数据库提供 了两种认证方式&#xff0c;一种是“操作系统认证”&#xff0c;一种是“口令文件认证&#xff0c…

深圳网站建设认准乐云犀牛云做网站推广怎么样

概述 在公司项目中使用到 wkhtmltopdf 转换PDF&#xff0c;由于 wkhtmltox-0.12.5 版本 echarts 图形虚线样式&#xff0c;需要升级 wkhtmltox-0.12.6 版本来解决。 官网地址 wkhtmltopdf &#xff1a;https://wkhtmltopdf.org/ windows 安装 下载流程及安装流程 进入官…

RabbitMQ的安装集群、镜像队列部署

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

怎样创办一个网站十堰网络科技公司排名

这是Project Student的一部分。 其他帖子包括带有Jersey的Webservice Client&#xff0c;带有Jersey的 Webservice Server和带有Spring Data的Persistence 。 RESTful Webapp洋葱的第三层是业务层。 这就是应用程序的精髓所在–编写良好的持久性和Web服务层受到约束&#xff0…

单一训练模式适应多个机器人本体 —— skiled brain —— 机器人酷刑现场,竟是为了锻造全能大脑,网友:求AGI饶了我

单一训练模式适应多个机器人本体 —— skiled brain —— 机器人酷刑现场,竟是为了锻造全能大脑,网友:求AGI饶了我地址: https://www.bilibili.com/video/BV1chngzvExC/本博客是博主个人学习时的一些记录,不保证…

2025/10/4 总结

A 用时:1h 预期:100pts 实际:100pts 发现直接暴力的复杂度是调和级数,于是就过了。 总结:对于这种复杂度有有剪枝的题,可以先算时间复杂度看是不是可过。 B 用时:2h 预期:50pts 实际:50pts 已经会了 50pts 暴…

win10界面如何改成经典菜单?

win10界面如何改成经典菜单?win10系统是微软推出的windows操作系统,在一些功能和设置上,与原先的win7有很多不一样的地方,很多小伙伴习惯了win7的开始菜单,开始使用win10的开始菜单非常不习惯,那么win10界面如何…

Qt处理Windows平板上摄像头

Qt处理Windows平板上摄像头pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", …

邢台网站建设服务周到网站域名 英文

grad norm先降后升再降正常嘛 在深度学习中&#xff0c;梯度的范数通常被用来衡量模型参数的更新程度&#xff0c;也就是模型的学习进度。在训练初期&#xff0c;由于模型参数的初始值比较随机&#xff0c;梯度的范数可能会比较大&#xff0c;这是正常现象。随着模型的训练&…

你必须知道的TCP和UDP核心区别,快速搞懂这两大协议!

UDP与TCP详解1. TCP (Transmission Control Protocol) 概念 TCP(传输控制协议)是一种面向连接的、可靠的传输协议。它负责将数据从源主机传输到目标主机,并确保数据的完整性、顺序和正确性。 原理三次握手:在数据传…

机器学习——朴素贝叶斯详解 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …