关于C++多态性的一些总结

2023-05-16

    在任何一门面向对象的编程语言中,多态性(polymorphism)都是非常重要的一个概念。在面向对象的三大元素中,封装使代码的模块化变得简单,继承则可以扩展已有的代码,而多态则是为了满足接口的重用。所谓的多态,通俗的来讲,其实就是让不同的对象在接受到相同的消息时能够做出不同的反应,好像有多个类型一般,这体现了对象的自恰性。打个比方,两军对阵,同样是鸣锣,可能一方军队在听到锣声时会采取撤退的行动;而另一方的军队听到锣声后,则可能会采取发动进攻的行动。关键在于双方军队各自都有对听到锣声这一信号而采取什么样的行动有所对应。多态也是一样。在满足继承关系的两个类中,只要在基类中定义了一个虚函数,且子类中对该虚函数提供了有效的覆盖版本,那么在通过指向子类的基类指针或引用子类的基类引用调用该函数时,会由调用对象的具体类型来确定调用哪个对象的函数,这种现象称之为多态(运行期多态)。在C++中,多态性的实现有两种形式.——动态的多态性(运行时的多态性)和静态的多态性(编译时的多态性),要么在运行时决定,要么在编译时决定。其中动态的多态性正是通过虚函数表来实现的。下面先让我们来看一段代码(运行环境:64位Ubuntu 14.04 LTS + gcc 4.8.2):

#include <iostream>
using namespace std;
class Shape{
public:
	Shape(void){}
	virtual void show(void){
		cout << “Shape” << endl;
	}
	virtual void foo(void){
		cout << “Shape::foo” << endl;
	}
};
class Circle:public Shape{
public:
	Circle(void){}
	void show(void){
		cout << “Circle” << endl;
	}
};
class Rectangle:public Shape{
public:
	Rectangle(void){}
	void show(void){
		cout << “Rectangle” << endl;
	}
};
int main(void){
	Circle c;
	Rectangle r;
	Shape* p = &c;
	p->show();			//Cricle
	p = &r;
	p->show();			//Rectangle
	Shape& pr = c;
	pr.show();			//Circle
	Shape& pt = r;
	pt.show();			//Rectangle
	return 0;
}

从这幅图中可以看出,在Shape对象中有一个虚表指针(_vptr_Shape),指向了Shape中的虚函数表,而虚函数表高地址部分内容中有两个函数指针,分别指向了Shape基类中的show函数和foo函数。整个多态的过程是这样的:在实例化Circle对象时会先实例化Shape子对象,这时虚表指针指向了Shape类中的虚函数表。当Shape子对象构建完成后会去构建Circle的独有部分,这个时候会将虚表指针的值改为Circle类的虚函数表的地址。由于Circle提供了对show函数的有效覆盖,而没有提供对foo函数的有效覆盖,所以在Circle对象的虚表指针(_vptr_Circle)所指向的虚函数表中,Circle::show函数代替了Shape::show函数,而foo函数仍然为Shape::foo函数。当我们使用一个指向子类的基类指针或引用子类的基类引用时,操作系统会先根据指针所指向的对象找到虚表指针,再根据虚表指针找到对应的类中的虚函数表,再来调用相应的函数,这就是通过虚函数表来实现运行时的多态的原理
要实现运行时多态必须满足以下几个条件:
1. 在基类中定义了虚函数且子类中提供了对基类虚函数的有效覆盖
2. 必须通过指向子类的基类指针或引用子类的基类引用来调用
3. 只有成员函数形式的运算符重载才能实现多态性,全局函数形式的运算符重载无法实现多态
4. 调用虚函数的指针也有可能是基类的this指针,同样满足多态的条件,但在构造函数和析构函数中除外

有效的覆盖(override)应满足的条件:
1. 必须是使用virtual声明的成员函数,不能是静态成员函数或全局函数
2. 覆盖版本中必须带有和基类虚函数中完全相同的函数签名,即函数名,形参表和常属性
3. 如果基类中的虚函数的返回类型是基本数据类型,那么子类中的覆盖版本必须返回相同的基本数据类型;如果基类中的虚函数返回的是类类型的指针或引用,那么允许子类中的覆盖版本返回其子类类型的指针或引用
4. 如果基类版本的虚函数带有异常说明,那么子类覆盖版本不能说明比基类版本的虚函数更多的异常说明(可以少不可以多)
5. 无论基类版本位于基类中的public,private,protected部分中,子类中的覆盖版本可以出现在子类中包括public,private,protected的任何部分

虽然运行时多态使用形式简单,但是它会给程序带来一定的开销,对性能的影响主要体现在以下几点:
1. 虚函数表本身会增加内存空间的开销
2. 与普通函数相比,虚函数的调用 要多出几个步骤,增加了运行时间的开销
3. 动态绑定会妨碍编译器来通过内联优化代码(最突出)

既然动态多态性(运行时多态)是通过虚函数表来实现,那么静态的多态性(编译时多态)则是通过什么来实现的呢?先来看一下下面的 代码(运行环境:64位Ubuntu 14.04 LTS + gcc 4.8.2):

#include <iostream>
using namespace std;
class Circle{
public:
	void show(void){	
		cout << ”Circle” << endl;
	}
};
class Rectangle{
public:
	void show(void){
		cout << ”Rectangle” << endl;
	}
};
template<typename Shape>
void drawAny(Shape& shape){
	shape.show();
}
int main(void){
	Circle c;
	Rectangle r;
	drawAny(r);
	drawAny(c);
	return 0;
}    
运行结果如下:
Rectangle
Circle
从上面代码运行结果可以看出,我们的drawAny实现了多态。在面对同样的函数调用时,我们通过传递不同的参数来实现不同的函数调用。这种多态性称之为编译期多态性,这是因为在编译的过程中,编译器在看到函数模板定义的时候还不知道其类型,这时编译期会先忽略类型,做一些跟类型无关的语法检查,然后生成一个内部表示。当编译器看到对该函数模板的调用时,便会结合具体的参数类型生成相应函数的二进制代码。也就是说,在上面的那个例子中,我们利用了函数模板的隐式推断,将我们要调用的类类型对象通过引用的方式传递给了函数模板,编译器通过对参数类型的确认,确定了被调用的函数所属的对象,进而确定了调用的是哪个具体函数,实现了多态。这种方法相对于通过使用虚函数表实现的运行时多态,实现较为复杂,不如虚函数简单易用,但是它的运行效率要比通过使用虚函数表实现的多态方式要高。
除此之外,我们也可以使用函数重载来实现多态,这一样也是属于编译期多态。来看一段代码(运行环境:64位Ubuntu 14.04 LTS + gcc 4.8.2):
#include <iostream>
using namespace std;
class Circle{
public:
	void show(void){
		cout << ”Circle” << endl;
	}
};
class Rectangle{
public:
	void show(void){
		cout << ”Rectangle” << endl;
	}
};
void drawAny(Circle & circle){
	circle.show();
}
void drawAny(Rectangle & rect){
	rect.show();
}

int main(void){
	Circle c;
	Rectangle r;
	drawAny(r);
	drawAny(c);
	return 0;
}
运行结果如下:
Rectangle
Circle
由上面代码可以看出,使用函数重载同样也可以实现多态性。面对同样的函数调用,都能得到不同的正确结果。这种方法实现起来非常简单,但是过程繁复,而且不具备可扩展性。程序中的需要实现多态特性的类个数少的话还行,但如果多呢?可复用性和可扩展性都太差,一般都不建议使用。在这里简单的提一下重载(overload)、覆盖(override)和隐藏()的区别:在同一作用域中,函数名相同,参数表不同,则构成重载关系;在具有继承关系的父子类中,带有virtual关键字,且函数签名完全相同,则构成覆盖关系;若不在同一作用域中,函数名相同,则构成隐藏关系
总结:
1. 使用虚函数实现多态时,由于编译器内置的支持,使用起来较为简单,而使用模板实现多态较为复杂,使用函数重载实现多态则非常简单,但是缺乏可复用性和可扩展性。
2. 使用运行时多态会增加程序的开销,而编译期多态对程序的开销几乎没有影响。
3. 使用模板来实现的多态无法通过基类对象指针数组来实现对多个不同子类对象的多态操作,而使用虚函数可以达到这一目标

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于C++多态性的一些总结 的相关文章

  • [Linux] CentOS7安装Chrome后yum不可以使用的解决方法

    症状 xff1a http dl google com linux chrome rpm stable x86 64 repodata repomd xml Errno 14 curl 52 Empty reply from server
  • [Java] 报错 java.lang.IllegalArgumentException: host parameter is null

    因为URL中存在中文字符 xff0c 需要编码 如 http baidu com 文件1 String encodeUrl 61 java span class hljs preprocessor net span span class h
  • Ubuntu 录屏

    GNOME录屏软件 Ubuntu 附带的屏幕录像机 xff0c 当您点击 Ctrl 43 Shift 43 Alt 43 R 时触发 它会自动开始录制整个屏幕 当您再次点击同一热键时 xff0c 截屏将结束并保存 点击录制热键时 xff0c
  • 【Odroid-XU4开发板】【安装Ubuntu18.04】【安装ROS】并读取串口数据的踩坑日记

    Odroid XU4开发板安装Ubuntu18 04安装ROS并读取串口数据的踩坑日记 文章目录 Odroid XU4开发板安装Ubuntu18 04安装ROS并读取串口数据的踩坑日记前言一 系统镜像准备二 烧录软件准备3 装机4 安装ro
  • 消除VS中C6054:可能没有为字符串"XXX"添加字符串零终止符的警告

    常见的是用 strlen 函数统计字符串的长度的时候 下面经常出现波浪线 xff08 即报警告C6054 xff09 xff0c 如下图所示 xff1a 怎么解决去掉这个警告呢 xff1f 64 wowpH 在这之前需要了解一下 strle
  • docker pull下载的image存在什么地方去了

    结论 xff1a 所有放入镜像文件都放在虚拟硬盘文件里面 windows上安装的docker其实本质上还是借助与windows平台的hyper v技术来创建一个linux虚拟机 xff0c 你执行的所有命令其实都是在这个虚拟机里执行的 xf
  • Eclipse CDT初步使用教程

    我用过Source insight和VS xff0c 感觉不顺手就没用了 xff0c 所以也没有深入研究过 xff0c 其他的如Code Blocks xff0c vim xff0c Clion Emacs我也没用过 xff0c 不好做评价
  • eclispe设置断点无效(No source file named)

    最近不知道做了什么操作导致eclipse的断点无效 xff0c 表现为gdb只认相对路径的断点 xff0c 不认绝对路径 xff0c 而eclipse打断点使用的是绝对路径 xff0c 导致无效 xff0c 输出信息是 xff1a No s
  • lemon源码基本概念整理

    1 数据结构 1 1 字符串存储 定义一个x1a的全局变量 xff0c 存放 y文件经过词法分析器分割出来的字符串 span class token keyword struct span s x1 span class token pun
  • lemon源码分析

    基本概念见上篇 lemon源码基本概念整理 1 follow集 对于如下4条产生式 program 61 expr TK SEM expr 61 expr TK IMPL expr expr 61 TK LPAREN expr TK RPA
  • Tcl脚本初步学习

    1 命令 Tcl 是一门基于命令的脚本语言 xff0c 每个命令通过换行符或分好隔开 每条命令都包含一个或多个单词 xff0c 第一个单词是命令名 xff0c 其他单词是命令的参数 xff0c 如 xff1a 命令 命令名 参数 set a
  • SQLite3源码学习(31) WAL日志的锁机制

    1 锁的原理 先来回顾一下回滚日志的文件锁 xff0c 之前的锁是针对数据库文件加锁的 xff0c 有4种类型 xff0c 分别是shared reserverd pending和exclusive 在WAL日志模式下不再使用原来的锁 xf
  • QGC调试px4固件飞控

    文章目录 前言一 开源软件地址二 硬件接线三 无人机调试1 刷固件2 选机架3 校准传感器4 校准遥控器5 飞行模式设置6 电源设置7 查看电机8 试飞 总结 前言 开源无人机调试 xff1a 硬件 xff1a pixhawk 2 4 8
  • CMake 基本使用方法

    1 学习背景 C语言工程使用make来构建工程 xff0c 但是对于大型工程来说文件的依赖关系很复杂 xff0c 手写makefile非常麻烦 xff0c 一般开源代码的构建方式都是使用autotool来配置编译环境和自动生成makefil
  • 嵌入式操作系统学习(3)FreeRTOS的任务调度机制

    1 任务状态 FreeRTOS可以创建多个任务 xff0c 但是对于单核cpu来说 xff0c 在任意给定时间 xff0c 实际上只有一个任务被执行 xff0c 这样就可以把任务分成2个状态 xff0c 即运行状态和非运行状态 当任务处于运
  • GTK+开发环境搭建

    一般讲到GUI程序开发 xff0c 大家都会想到C 43 43 等面向对象的高级语言 xff0c 而认为C语言不能做界面 C语言也可以用来写界面 xff0c GTK 43 就是一个用来写界面的库 xff0c 它本身就是用C语言写的 xff0
  • ▲什么是迭代器?迭代器有什么作用?

    什么是迭代器 xff1f 先说说它是干嘛的吧 xff01 迭代器的作用是用来访问容器 xff08 用来保存元素的数据结构 xff09 中的元素 xff0c 所以使用迭代器 xff0c 我们就可以访问容器中里面的元素 没错 xff01 这和访
  • 【C/C++】面试题:函数指针

    问题1 xff1a 如何把一个类的成员函数赋值给空指针 xff1f 他的回答是 xff1a 函数指针应该加类域 正确 接下来再问 xff0c 如果要赋值给一个没有类域的指针 xff0c 应该怎么办 xff1f 他说 xff1a 不能那样赋值
  • 机器学习环境搭建:GTX970+Ubutnu1404_64bit+TensorFlow(GPU)

    xfeff xfeff TF的GPU版本需要安装CUDA和cuDNN xff0c 而在Ubuntu中安装显卡驱动稍微麻烦一点 下面的安装比较简单 xff1a 1 检查显卡支持情况 https developer nvidia com cud
  • USB设备的插入检测

    1 USB的插入检测机制 xff1a USB端口的D 43 和D 均用一个15k的电阻接地 xff0c 当无设备接入时 xff0c 均处于低电平 xff1b 在设备端在D 43 xff08 表示高速设备或者全速设备 xff09 或者D xf

随机推荐