【【高级程序设计语言C++】C++多态的概念及原理

news2024/12/28 20:00:04

  • 1. 多态的概念
  • 2. 多态的定义及实现
    • 2.1. 多态的条件
    • 2.2. 虚函数
    • 2.3. 虚函数的重写
    • 2.4. 虚函数重写的两个例外
    • 2.5. C++11的override和final
    • 2.6. 重载、重写、重定义的对比
  • 3. 抽象类
    • 3.1. 概念
    • 3.2. 实现继承和接口继承的对比
  • 4. 多态的原理
    • 4.1. 虚函数表
    • 4.2. 多态原理
    • 4.3. 动态绑定和静态绑定
    • 4.4. 多继承的虚表

1. 多态的概念

在C++中,多态性是面向对象编程的一个重要概念,它允许不同的对象对同一个消息做出不同的响应。多态性使得程序具有灵活性和可扩展性,能够根据具体的对象类型来选择不同的行为。

C++中的多态性通过虚函数(Virtual Function)和基类指针或引用来实现。虚函数是在基类中声明为虚函数的函数,它可以在派生类中重写(覆盖)以实现特定的行为。基类指针或引用可以指向派生类的对象,并通过虚函数来调用相应的方法。

简单的说就是不同的对象做相同的事,会有不同的行为。举个例子,有人去买票,假如他是个普通老百姓,那么他买票就得是全价。假如他是学生,拿着学生证买票,那么他买票就半价。假如他是个军人,那么他买票就可以优先。

下面是一个简单的示例,说明C++中多态性的概念:

class Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Shape" << endl;
	}
};
class Circle : public Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Circle" << endl;
	}
};
class Rectangle : public Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Rectangle" << endl;
	}
};
int main()
{
	Shape* s1 = new Circle();
	Shape* s2 = new Rectangle();
	s1->draw();
	s2->draw();
	Shape* s3 = new Shape();
	s3->draw();
	return 0;
}

在上面的示例中,shape1和shape2是基类指针,分别指向Circle和Rectangle的对象。当调用shape1->draw()时,实际上调用的是Circle类中重写的draw()函数,同样,当调用shape2->draw()时,实际上调用的Rectangle类中重写的draw()函数。

2. 多态的定义及实现

2.1. 多态的条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Circle继承了Shape。Shape对象画图形,Circle对象画圆。

在继承中要实现多态的两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2. 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Shape" << endl;
	}
};

2.3. 虚函数的重写

虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数

class Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Shape" << endl;
	}
};
class Circle : public Shape
{
public:
	//void draw()    //这样省略virtual的写法也是允许的
	virtual void draw()
	{
		cout << "draw a Circle" << endl;
	}
};
class Rectangle : public Shape
{
public:
	//void draw()   //只要父类的虚函数写了virtual关键字,子类继承下来都可以不写
	virtual void draw()
	{
		cout << "draw a Rectangle" << endl;
	}
};

2.4. 虚函数重写的两个例外

虚函数的重写/覆盖有三同:返回值相同,参数相同,函数名相同。

但是有两个例外。

  1. 协变

子类重写父类的虚函数时,返回值可以不相同,但是子类虚函数的返回值和父类虚函数的返回值要构成父子类关系,且返回的类型必须是父类的指针或者引用。

class A{};
class B : public A {};
class Shape
{
public:
	virtual A* draw()
	{
		cout << "draw a Shape" << endl;
	}
};
class Circle : public Shape
{
public:
	virtual B* draw()
	{
		cout << "draw a Circle" << endl;
	}
};
  1. 析构函数的重写

假如在父类的析构函数加上virtual关键字,那么即使子类的析构函数不加virtual关键字,此时子类和父类的析构函数也会形成重写,即使函数名不相同。这是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名字统一变成了destructor。

class Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Shape" << endl;
	}
	virtual ~Shape()
	{
		cout << "delete Shape()" << endl;
	}
};
class Circle : public Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Circle" << endl;
	}
	~Circle()
	{
		cout << "delete Circle()" << endl;
	}

};
int main()
{
	Shape* s1 = new Shape();
	Shape* s2 = new Circle();
	delete s1;
	delete s2;
	return 0;
}

输出结果:

img

2.5. C++11的override和final

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Shape
{
public:
	virtual void draw () final
	{
		cout << "draw a Shape" << endl;
	}
};
class Circle : public Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Circle" << endl;
	}

};

报错结果:

img

  1. override 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Shape
{
public:
	virtual void draw ()
	{
		cout << "draw a Shape" << endl;
	}
};
class Circle : public Shape
{
public:
	virtual void draw() override
	{
		cout << "draw a Circle" << endl;
	}

};

2.6. 重载、重写、重定义的对比

  1. 重载是同一作用域,函数名相同,参数不同
  2. 重写(Override)是指派生类中重新定义(覆盖)基类中已经声明为虚函数的函数
  3. 重定义(Redeclaration)是指在派生类中重新定义(覆盖)基类中的函数,但不使用virtual关键字

3. 抽象类

3.1. 概念

在C++中,抽象类(Abstract Class)是指包含至少一个纯虚函数的类。纯虚函数是通过在函数声明末尾添加= 0来声明的,表示该函数没有实现,需要在派生类中进行重写。

**抽象类不能被实例化,只能作为基类来派生其他类。**它的主要目的是为了提供一个通用的接口,定义了一组纯虚函数,要求派生类必须实现这些函数。抽象类的存在可以约束派生类的行为,确保派生类具有某些特定的功能或行为。

class Shape
{
public:
	virtual void draw() = 0{}
};
class Circle : public Shape
{
public:
	virtual void draw() 
	{
		cout << "draw a Circle" << endl;
	}
};
class Rectangle : public Shape
{
public:
	virtual void draw()
	{
		cout << "draw a Rectangle" << endl;
	}
};
int main()
{
	Shape* s1 = new Circle;
	Shape* s2 = new Rectangle;
	s1->draw();
	s2->draw();
	return 0;
}

3.2. 实现继承和接口继承的对比

在C++中,实现继承(Implementation Inheritance)和接口继承(Interface Inheritance)是两种不同的继承方式。

  1. 实现继承(也称为类继承):实现继承是指派生类继承基类的成员和实现。派生类可以使用基类中的成员变量和成员函数,并且可以重写基类中的虚函数。实现继承通过使用关键字publicprotectedprivate来指定基类的访问权限。

示例:

class Base {
public:
    int publicVar;
    void publicFunc();
protected:
    int protectedVar;
    void protectedFunc();
private:
    int privateVar;
    void privateFunc();
};

class Derived : public Base {
    // 派生类继承了Base类的成员和实现
};

在上面的示例中,派生类Derived通过public关键字继承了Base类的成员和实现。这意味着Derived类可以访问Base类的public成员和实现,但无法直接访问Base类的protectedprivate成员。

  1. 接口继承:接口继承是指派生类只继承基类的纯虚函数,而不继承成员变量或实现。接口继承通常用于定义一组接口规范,要求派生类实现这些接口。接口继承通过使用关键字public来指定基类的访问权限,并将基类中的成员函数声明为纯虚函数。

示例:

class Interface {
public:
    virtual void pureVirtualFunc() = 0;
};

class Derived : public Interface {
public:
    void pureVirtualFunc() override {
        // 派生类实现接口中的纯虚函数
    }
};

在上面的示例中,Interface类是一个接口类,它只包含一个纯虚函数pureVirtualFunc()。派生类Derived通过public关键字继承了Interface类,并实现了接口中的纯虚函数。

接口继承的目的是为了实现多态性,通过基类指针或引用来操作派生类对象,实现统一的接口调用。接口继承也可以用于定义一组接口规范,要求派生类实现这些接口。

4. 多态的原理

4.1. 虚函数表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	int num = sizeof(b);
	cout << num << endl;
	return 0;
}

输出结果:

img

一个类的大小,是计算成员变量的大小,然后根据内存对齐来确定的。但是Base类中只有一个int,大小应该为4字节,但是为什么会是8呢?

通过调试,可以看到Base类中不仅只有_b一个成员,还有一个指针。

img

vs2019的平台这里默认是32位的,所以指针的大小是4个字节,再加上原来的成员变量,大小就是8个字节。

其中这里多出来的_vfptr成为虚函数表,是一个函数指针数组。

4.2. 多态原理

class Base
{
public:
	virtual void func()
	{
		cout << "Base::func()" << endl;
	}
	int _a;
};
class Derived : public Base
{
public:
	virtual void func()
	{
		cout << "Derived::func()" << endl;
	}
	int _b;
};

int main()
{
	Base b;
	Derived d;
	return 0;
}

通过调试可以发现,继承之间的关系和多态的原理如下图:

img

img

_vfptr中存放的就是Base的虚函数的函数指针。

img

那么当Derived继承了Base,也会继承这个虚表。

img

但是这两个虚表的地址是不一样的,内容也有所不同

img

img

但也有相同的内容,里面有一个是Base的函数指针,另一个却变成了Derived的函数指针。那么就证明了Derived继承Base,也会继承了虚表,此时的虚表是由Base的虚表拷贝一份到Derived的虚表,如果Derived重写了基类中的某个函数,那么就会覆盖Derived虚表中的函数指针。

img

4.3. 动态绑定和静态绑定

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

输出结果:

img

img

基类指针或引用可以指向派生类的对象,并根据对象的实际类型来动态地选择调用相应的虚函数。这样,在运行时可以根据对象的实际类型来调用正确的函数,实现多态性。如果直接使用基类对象调用虚函数,将无法实现多态性,只能调用基类中的函数。成为动态绑定,因为他是在运行的时候,然后根据对象的类型来确定使用哪个函数,完成多态。

C++的静态绑定是指在编译时确定函数调用的具体实现。它是通过函数的静态类型来确定调用哪个函数的过程。

4.4. 多继承的虚表

class Base1 {
public:
	virtual void func1() {cout << "Base1::func1" << endl;}
	virtual void func2() {cout << "Base1::func2" << endl;}
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() {cout << "Base2::func1" << endl;}
	virtual void func2() {cout << "Base2::func2" << endl;}
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() {cout << "Derive::func1" << endl;}
    virtual void func3() {cout << "Derive::func3" << endl;}
private:
    int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        VFPTR f = vTable[i];
        f();
    }
	cout << endl;
}
int main()
{
    Derive d;
    VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
    PrintVTable(vTableb1);
    VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
    PrintVTable(vTableb2);
    return 0;
}

运行结果:

img

img

通过上图可以知道,子类重写了的虚函数的函数地址会覆盖父类原来虚函数的函数地址,而子类没有重写的虚函数会存放在第一个类的虚表当中。

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

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

相关文章

(34)继电器开关

文章目录 前言 34.1 装有IOMCU的自动驾驶仪上的继电器引脚 34.2 通过任务规划器定义继电器引脚 34.3 飞行员控制继电器 34.4 任务控制继电器 34.5 任务规划器控制继电器 前言 "继电器"是自动驾驶仪上的一个数字输出引脚&#xff0c;可在 0V 和 3.3V 或 5V 之间…

商城-学习整理-基础-分布式组件(三)

目录 一、前言二、Spring Cloud&Spring Cloud Alibaba1、Spring Cloud 与Spring Cloud Alibaba简介2、为什么使用Spring Cloud Alibaba3、版本选择4、项目中的依赖 三、Spring Cloud Alibaba-Nacos作为注册中心1、Nacos1&#xff09;、下载 nacos-server2&#xff09;、启动…

【C++ 程序设计】第 1~9 章:常见知识点汇总

目录 一、C 语言简介 二、面向对象的基本概念 三、类和对象进阶 四、运算符重载 五、类的继承与派生 六、多态与虚函数 七、输入/输出流 八、文件操作 九、函数模板与类模板 一、C 语言简介 知识点名称内容C语言的发展简史★★1. C 语言是 C 语言的前身 &…

让GPT人工智能变身常用工具-上

1.密码生成器:GPT为您创建安全密码 想象GPT作为您的个人密码生成器,负责从头到尾为您创建复杂且安全的密码。您只需要告诉他您的密码需求,比如密码的长度,是否包含大写字母、小写字母、数字或特殊字符,他会立即为您生成一个复杂但经过深度设计的密码。 例子: 我希望您…

第八届中国开源年会(COSCon'23)启动!

*海报设计师&#xff1a;朱亿钦&#xff08;居居&#xff09; 一年一度的开源盛会&#xff0c;COSCon23 第八届中国开源年会&#xff0c;将于10月28~29日&#xff0c;在四川成都市高新区菁蓉汇召开&#xff01;本次大会的主题是&#xff1a;“开源&#xff1a;川流不息、山海相…

(css)滚动条样式

(css)滚动条样式 效果&#xff1a; /*滚动条整体样式*/ ::-webkit-scrollbar {width: 2px;/*高宽分别对应横竖滚动条的尺寸*/height: 10px; } ::-webkit-scrollbar-thumb {/*滚动条里面小方块*/border-radius: 10px;width: 2px;height: 60px;background: linear-gradient(0deg,…

leetcode 78. 子集

2023.7.22 本题为回溯系列的一道标准模板题。 如果将回溯问题抽象为一棵树的话&#xff0c;那么之前的组合、分割问题都是为了找到这棵树的叶子节点&#xff0c;而子集是要找到这棵树的所有节点。 然后要注意&#xff0c;子集是无序的&#xff0c;即{1&#xff0c;2}和{2&#…

幂等性设计与实现

文章目录 前言1.全局唯一ID1.1 前端防止重复提交1.2 token机制1.3 数据库表加唯一约束 2.幂等下 ABA问题 与乐观锁2.1 乐观锁2.2 如何解决ABA问题&#xff1f; 3.分布式锁和事务3.1 分布式锁&#xff1a;3.2. 分布式事务 前言 幂等性&#xff08;Idempotence&#xff09;是一个…

导航、开源镜像、Prompt ( AI 提示词 )、AI工具集、chatgpt镜像

1、导航 网站 众多网址导航中&#xff0c;哪个最好&#xff1f;理由是什么&#xff1f; &#xff1a;https://www.zhihu.com/question/19899559 除了百度&#xff0c;其他搜索引擎&#xff1a; 综合类搜索导航(Anywhere Anything)&#xff1a;http://lackar.com/aa/ 渗透师 导…

开源QianWei搭建音乐网站,并实现公网连接

开源QianWei搭建音乐网站&#xff0c;并实现公网连接 1、前言2、本地网页搭建2.1环境使用2.2 支持组建选择2.3 网页安装 3、本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4、公网访问测试5、结语 1、前言 音乐是我们生活和工作中不可或缺的调剂&#xff0c;它能让我们心…

二,jmeter的简介还有一些参数的说明

文章目录 一、jmeter简介及安装1. 简介2. 安装 二、jmeter设置语言三、jmeter文件路径说明四、编写jmeter脚本五、乱码的处理&#xff1a;1. 请求内容出现乱码处理方法2. 响应内容出现乱码处理方法 一、jmeter简介及安装 1. 简介 Apache 托管的开源java工具接口测试、自动化测…

Sublime Text 设置中文

文章目录 1. Subime Text 官网2. 中文设置 1. Subime Text 官网 https://www.sublimetext.com/ 2. 中文设置 打开 sublime&#xff0c;ctrl shift p&#xff0c;在对话框搜索 Install Package Control&#xff0c;点击 会弹出一个消息框&#xff0c;表示插件列表加载完成…

【每日一题】42. 接雨水

【每日一题】42. 接雨水 42. 接雨水题目描述解题思路 42. 接雨水 题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输…

【STM32CubeMX】HC_SR04模块测距

前言 本文章介绍了基于STM32F103的HAL库&#xff0c;完成对HC_SR04超声波模块测距的基本思路和工程案例。 环境 STM32F103C6T6系统板&#xff0c;72MHz主频基于STM32CubeMX生成的HAL库代码硬件连接&#xff1a; PB12 — Echo(HC_SR04)&#xff0c;PB13 — Trig(HC_SR04)PB9 —…

第二十二章:Non-local Neural Networks ——非局部神经网络

0.摘要 卷积和循环操作都是一次处理一个局部邻域的基本构建模块。在本文中&#xff0c;我们提出了非局部操作作为捕捉长程依赖关系的通用构建模块族。受计算机视觉领域经典的非局部均值方法[4]的启发&#xff0c;我们的非局部操作将一个位置的响应计算为所有位置特征的加权和。…

LiveNVR监控流媒体Onvif/RTSP功能-拉转Onvif/RTSP/RTMP/FLV/HLS直播流流媒体服务视频广场页面集成视频播放集成说明

LiveNVR拉转Onvif/RTSP/RTMP/FLV/HLS直播流流媒体服务视频广场页面集成视频播放集成说明 1、视频页面集成1.1、关闭接口鉴权1.2、视频广场页面集成1.2.1、隐藏菜单栏1.2.2、隐藏播放页面分享连接 1.3、其它页面集成 2、播放分享页面集成2.1、获取 iframe 代码2.2、html 集成ifr…

ext4 mballoc之buddy算法

buddy bitmap 根据《Ext4文件系统介绍 - 理论篇_nginux的博客-CSDN博客》我们知道磁盘上有1block 大小(默认4K&#xff09;data block bitmap&#xff0c;每bit位代表一个block的使用情况&#xff0c;1代表占用&#xff0c;0代表空闲。data block bitmap 可以表示4 * 1024 * …

代码随想录| 图论02●695岛屿最大面积 ●1020飞地的数量 ●130被围绕的区域 ●417太平洋大西洋水流问题

#695岛屿最大面积 模板题&#xff0c;很快.以下两种dfs&#xff0c;区别是看第一个点放不放到dfs函数中处理&#xff0c;那么初始化的area一个是1一个是0 int dir[4][2]{0,1,0,-1,1,0,-1,0};void dfs(int x, int y,int n, int m, int &area,vector<vector<bool>…

HTML入门教程||HTML 属性||HTML 元素

HTML 元素 HTML 元素 HTML 文档由 HTML 元素定义&#xff0c;HTML 元素指的是从开始标签&#xff08;start tag&#xff09;到结束标签&#xff08;end tag&#xff09;的所有代码。 HTML 元素 开始标签 *元素内容结束标签 *<p>这是一个段落</p><a href"…

《Docker和服务器无状态化:容器化应用的优势,构建高可伸缩性和灵活性》

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…