C++RTTI(运行时类型识别)

静态类型与动态类型

当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。

  1. 表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;
  2. 动态类型则是变量或表计式表示的内存中的对象的类型。动态类型直到运行时才可知。

假设我们有一个基类Animal和两个派生类Dog和Cat:

class Animal {
public:virtual void makeSound() = 0;
};class Dog : public Animal {
public:void makeSound() {cout << "Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() {cout << "Meow!" << endl;}
};

现在我们可以创建一个Animal类型的指针,根据实际的对象类型来调用不同的成员函数:

int main() {Animal* animal1 = new Dog();animal1->makeSound(); // 输出:Woof!Animal* animal2 = new Cat();animal2->makeSound(); // 输出:Meow!delete animal1;delete animal2;return 0;
}

在这个例子中,animal1和animal2都是Animal类型的指针,但在运行时它们分别指向了Dog对象和Cat对象。

通过调用makeSound()函数,可以根据实际的对象类型来输出不同的声音。这就是动态类型的体现,根据实际的对象类型来决定调用哪个函数。

在编译时确定animal1和animal2的静态类型为Animal*,并进行类型检查。

运行时得出Animal1的动态类型是Dog*,而Animal2的动态类型是Cat*

即Animal1的静态类型和静态类型不同,Animal2也是这样

如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

基类的指针或引用的静态类型可能与其动态类型不一致,读者一定要理解其中的原因。

 运行时类型识别

运行时类型识别(RTTI)的功能由两个运算符实现:

  1. typeid运算符,用于返回表达式的类型。
  2. dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。

当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

这两个运算符特别适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。

一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

然而,并非任何时候都能定义一个虚函数。

假设我们无法使用虚函数,则可以使用一个RTTI运算符。

另一方面,与虚成员函数相比,使用 RTTI 运算符蕴含着更多潜在的风险:程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。

使用 RTTI 必须要加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。

dynamic_cast 运算符

dynamic_cast运算符的使用形式如下所示:

dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)


其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。

  • 在第一种形式中e必须是一个有效的指针;
  • 在第二种形式中,e必须是一个左值;
  • 在第三种形式中,e不能是左值。

在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:

  1. e 的类型是目标type的公有派生类
  2. e的类型是目标type的公有基类
  3. e的类型就是目标type的类型

如果符合,则类型转换可以成功。否则,转换失败。

  • 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。
  • 如果转换目标是引用类型并且失败了,dynamic_cast运算符抛出一个bad_cast 异常

指针类型的dynamic_cast

举个简单的例子,假定Base类至少含有一个虚函数,Derived是Base的公有派生类

如果有一个指向Base的指针bp,则我们可以在运行时将它转换成指向Derived的指针,具体代码如下:

if (Derived *dp = dynamic_cast<Derived*>(bp)) 
{
//使用dp指向的Derived对象
}
else ( // bp 指向一个Base对象// 使用 bp 指向的Base对象}


如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指的Derived对象。

此时,if语句内部使用Derived操作的代码是安全的。

否则,类型转换的结果为0,dp为0意味着if语句的条件失败,此时else子句执行相应的Base操作。

我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。

值得注意的一点是,我们在条件部分定义了dp,这样做的好处是可以在一个操作中同时完成类型转换和条件检查两项任务。

而且,指针dp在if语句外部是不可访问的。一旦转换失败,即使后续的代码忘了做相应判断,也不会接触到这个未绑定的指针,从而确保程序是安全的。

在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。

引用类型的 dynamic_cast

引用类型的 dynamic_cast与指针类型的dynamic_cast在表示错误发生的方式上略有不同。

因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略。

当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常,该异常定义在typeinfo标准库头文件中

我们可以按照如下的形式改写之前的程序,令其使用引用类型:

void f(const Base &b)
{
try
{
const Derived &d=dynamic_cast<const Derived>(b);
//使用b引用的Derived对象 
}
catch (bad_cast){
//处理类型转换失败的情况
}
}

typeid 运算符

为RTTI提供的第二个运算符是typeid运算符,它允许程序向表达式提问:你的对象是什么类型?

typeid 表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。

typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type info的公有派生类型。

type info类定义在typeinfo头文件中。

  • typeid运算符可以作用于任意类型的表达式。
  • 和往常一样,顶层const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型
  • 不过当typeid作用于数组或函数时,并不会执行向指针的标准类型转换。也就是说,如果我们对数组a执行typeid(a),则所得的结果是数组类型而非指针类型。
  • 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。
  • 而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。

使用 typeid 运算符

通常情况下,我们使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同:

Derived *dp = new Derived;
Base *bp= dp; //两个指针都指向Derived 对象
//在运行时比较两个对象的类型if(typeid(*bp)==typeid(*dp)){
// bp和dp指向同一类型的对象
}
//检查运行时类型是否是某种指定的类型
if(typeid(*bp)==typeid(Derived)){
// bp 实际指向 Derived 对象
}

在第一个if语句中,我们比较bp和dp所指的对象的动态类型是否相同。如果相同,则条件成功。类似的,当bp当前所指的是一个Derived对象时,第二个if语句的条件满足。

注意,typeid应该作用于对象,因此我们使用*bp而非bp:

// 下面的检查永远是失败的:bp 的类型是指向 Base的指针
if (typeid(bp) == typeid(Derived)){
// 此处的代码永远不会执行
}


这个条件比较的是类型Base*和Derived。尽管指针所指的对象类型是一个含有虚函数的类,但是指针本身并不是一个类类型的对象。类型Base*将在编译时求值,显然它与perived不同,因此不论bp所指的对象到底是什么类型,上面的条件都不会满足。

当 typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型。

typeid是否需要运行时检查决定了表达式是否会被求值。

  1. 只有当类型含有虚函数时,编译器才会对表达式求值。
  2. 反之,如果类型不含有虚函数,则typeid返回表达式的静态类型;编译器无须对表达式求值也能知道表达式的静态类型。

如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。

这条规则适用于typeid(*p)的情况。如果指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针。

否则,*p将在运行时求值,此时p必须是一个有效的指针。如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。

使用 RTTI

在某些情况下RTTI非常有用,比如当我们想为具有继承关系的类实现相等运算符时。

对于两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则我们说这两个对象是相等的。在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算行必须把派生类的新成员考虑进来。

一种容易想到的解决方案是定义一套虚函数,令其在继承体系的各个层次上分别执行相等性判断。此时,我们可以为基类的引用定义一个相等运算符,该运算符将它的工作委托给虚函数equal,由equal负责实际的操作。

遗憾的是,上述方案很难奏效。虚函数的基类版本和派生类版本必须具有相同的形类型。

如果我们想定义一个虚函数equal,则该函数的形参必须是基类的引用。此时,equal函数将只能使用基类的成员,而不能比较派生类独有的成员。

要想实现真正有效的相等比较操作,我们需要首先清楚一个事实:即如果参与比较的两个对象类型不同,则比较结果为false。例如,如果我们试图比较一个基类对象和一个派生类对象,则==运算符应该返回false。

基于上述推论,我们就可以使用RTTI解决问题了。

  1. 我们定义的相等运算符的形参是基类的引用,然后使用typeid检查两个运算对象的类型是否一致。
  2. 如果运算对象的类型不一致,则==返回false;
  3. 类型一致才调用equal函数。每个类定义的equal函数负责比较类型自己的成员。这些运算符接受Base&形参,但是在进行比较操作前先把运算对象转换成运算符所属的类类型。

类的层次关系

为了更好地解释上述概念,我们定义两个示例类:

class Base {
friend bool operator==(const Base&, const Base&);
public:
// Base的接口成员
protected:
virtual bool equal(const Base&) const;
// Base的数据成员和其他用于实现的成员
};class Derived: public Base {
public:
// Derived的其他接口成员
protected:
bool equal(const Base&) const;
}; // Derived的数据成员和其他用于实现的成员


类型敏感的相等运算符

接下来介绍我们是如何定义整体的相等运算符的:

bool operator==(const Base &lhs, const Base &rhs){//如果typeid不相同,返回false;否则虚调用equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}


在这个运算符中,如果运算对象的类型不同则返回false。否则,如果运算对象的类型相同,则运算符将其工作委托给虚函数 equal。当运算对象是 Base 的对象时,调用Base::equal;当运算对象是Derived的对象时,调用Derived::equal。

虚equal函数

继承体系中的每个类必须定义自己的equal函数,而派生类的所有函数要做的第一件事都是相同的,那就是将实参的类型转换为派生类类型

boo1 Derived::equal (connt Base sths) const
{
//我们清楚这两个类型是相等的,所以转捷过程不会抛出异常
auto r =dynamic_cast<connt Derived&> (rhs);
//执行比较两个Derived对象的操作并返回结果
}


上面的类型转换永远不会失败,因为毕竟我们只有在验证了运算对象的类型相同之后才会遇用该函数。然而这样的类型转换是必不可少的,执行了举型转换后,当前函数才能访问右侧运算对象的派生类成员。

基类 equal函数

下面这个操作比其他的稍微简单一点:

bool Base::equal(const Base &rhs) const
{
// 执行比较Base对象的操作
}


无须事先转换形参的类型。*this和形参都是Base对象,因此当前对象可用的操作对于形参类型同样有效。

type_info类

type_info类的精确定义随着编译器的不同而略有差异。不过,C++标准规定type_info类必须定义在typeinfo头文件中,并且至少提供表所列的操作。

type_info的操作
t1 ==t2如果 type_info对象t1和t2表示同一种类型,返回true;否则返
回false
t1 !=t2如果 type info对象tl和t2表示不同的类型,返回true;否则返回false
t.name()返回一个C风格字符串,表示类型名字的可打印形式。类型名字的生成方式因系统而异
t1.before(t2)返回一个bool值,表示t1是否位于t2之前。before所采用的顺序关系是依赖于编译器的

除此之外,因为type info类一般是作为一个基类出现,所以它还应该提供一个公有的虚析构函数。当编译器希望提供额外的类型信息时,通常在type_info的派生类中完成。

type info类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义成删除的。

因此,我们无法定义或拷贝 type_info类型的对象,也不能为tvpe info类型的对象赋值。创建type_info对象的唯一途径是使用 typeid运算符。

type_info类的name成员函数返回一个C风格字符串,表示对象的类型名字。

对于某种给定的类型来说,name的返回值因编译器而异并且不一定与在程序中使用的名字一致。

对于name返回值的唯一要求是,类型不同则返回的字符串必须有所区别。例如:

int arr[10];
Derived d;
Base *p = &d;cout << typeid(42).name() <<","
<< typeid(arr).name () << ","
<< typeid(Sales_data).name()<<","
<< typeid(std::string).name() <<","
<< typeid(p).name() <<","
<< typeid(*p).name()<< endl;


在我的计算机上运行该程序,输出结果如下:

i, A10_i, 10Sales _data, Ss, P4Base, 7Derived


type info 类在不同的编译器上有所区别。有的编译器提供了额外的成员函Note 数以提供程序中所用类型的额外信息。读者应该仔细阅读你所用编译器的使用手册,从而获取关于type info的更多细节。
 

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

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

相关文章

百度蜘蛛池平台在线发外链-原理以及搭建教程

蜘蛛池平台是一款非常实用的SEO优化工具&#xff0c;它可以帮助网站管理员提高网站的排名和流量。百度蜘蛛池原理是基于百度搜索引擎的搜索算法&#xff0c;通过对网页的内容、结构、链接等方面进行分析和评估&#xff0c;从而判断网页的质量和重要性&#xff0c;从而对网页进行…

前端学习--品优购项目

文章目录 前端学习--品优购项目1.案例铺垫文件建立与命名必备文件网站favicon图标网站TDK三大标签SEO优化常用命名 2.LOGO SEO优化3.实际代码4.申请免费域名 前端学习–品优购项目 1.案例铺垫 文件建立与命名 一个项目中为了方便实用和查找内容会有多个文件夹&#xff0c;比如…

windows10搭建reactnative,运行android全过程

环境描述 win10,react-native-cli是0.73&#xff0c;nodeJS是20&#xff0c;jdk17。这都是完全根据官网文档配置的。react-native环境搭建windows。当然官网文档会更新&#xff0c;得完全按照配置来安装&#xff0c;避免遇到环境不兼容情况。 安装nodeJS并配置 这里文档有详…

如何制作Word模板并用Java导出自定义的内容

1前言 在做项目时会按照指定模板导出word文档,本文讲解分析需求后,制作word模板、修改模板内容,最终通过Java代码实现按照模板自定义内容的导出。 2制作word模板 2.1 新建word文档 新建word文档,根据需求进行编写模板内容,调整行间距和段落格式后将指定替换位置留空。…

文件操作(随机读写篇)

1. 铺垫 建议先看&#xff1a; 文件操作&#xff08;基础知识篇&#xff09;-CSDN博客 文件操作&#xff08;顺序读写篇&#xff09;-CSDN博客 首先要指出的是&#xff0c;本篇文章中的“文件指针”并不是指FILE*类型的指针&#xff0c;而是类似于打字时的光标的东西。 打…

竞赛 python+深度学习+opencv实现植物识别算法系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于深度学习的植物识别算法研究与实现 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;4分工作量&#xff1a;4分创新点&#xff1a;4分 &#x1f9ff; 更多…

【pytest】pytest` 中几种常用的参数化方法

pytest 是一个强大的 Python 测试框架&#xff0c;它提供了多种参数化测试的方法。参数化测试允许你使用不同的输入集来运行相同的测试逻辑&#xff0c;从而确保代码在各种条件下都能正常工作。以下是 pytest 中几种常用的参数化方法&#xff1a; 1. 使用 pytest.mark.paramet…

154 Linux C++ 通讯架构实战9 ,信号功能添加,信号使用sa_sigaction 回调,子进程添加,文件IO详谈,守护进程添加

初始化信号 使用neg_init_signals(); 在nginx.cxx中的位置如下 //(3)一些必须事先准备好的资源&#xff0c;先初始化ngx_log_init(); //日志初始化(创建/打开日志文件)&#xff0c;这个需要配置项&#xff0c;所以必须放配置文件载入的后边&#xff1b;//(4)一些初…

Web应用安全攻防战:识别十大威胁,掌握防护要点

OWASP&#xff08;Open Worldwide Application Security Project&#xff09;是一家致力于应用安全威胁研究的非盈利机构。通过对超过20万个组织进行调研分析&#xff0c;该机构每三年左右就会发布一次《Web应用安全风险Top10》报告&#xff0c;这个报告已经成为全球企业开展We…

Redis入门三(主从复制、Redis哨兵、Redis集群、缓存更新策略、缓存穿透、缓存击穿、缓存雪崩)

文章目录 一、主从复制1.单例redis存在的问题2.主从复制是什么&#xff1f;3.主从复制的原理4.主从搭建1&#xff09;准备工作2&#xff09;方式一3&#xff09;方式二 5.python中操作1&#xff09;原生操作2&#xff09;Django的缓存操作 二、Redis哨兵&#xff08;Redis-Sent…

Lilishop商城(windows)本地部署【docker版】

Lilishop商城&#xff08;windows&#xff09;本地部署【docker版】 部署官方文档&#xff1a;LILISHOP-开发者中心 https://gitee.com/beijing_hongye_huicheng/lilishop 本地安装docker https://docs.pickmall.cn/deploy/win/deploy.html 命令端页面 启动后docker界面 注…

创建springboot项目的两种方式

创建springboot项目的两种方式 文章目录 创建springboot项目的两种方式一、第一种方式&#xff1a;springboot作为parent二、第二种方式&#xff1a;import 依赖管理方式三、问题1&#xff1a;无法正确打包四、问题2&#xff1a;无法执行单元测试 一、第一种方式&#xff1a;sp…

第十四章 MySQL

一、MySQL 1.1 MySql 体系结构 MySQL 架构总共四层&#xff0c;在上图中以虚线作为划分。 1. 最上层的服务并不是 MySQL 独有的&#xff0c;大多数给予网络的客户端/服务器的工具或者服务都有类似的架构。比如&#xff1a;连接处理、授权认证、安全等。 2. 第二层的架构包括…

三个表的联合查询的场景分析-场景4:c表维护a和b表的id关联关系(一对多)

基础SQL演练&#xff0c;带详细分析&#xff0c;笔记和备忘。 目录 背景介绍 表数据 需求1&#xff1a;查询g表所有记录&#xff0c;以及关联的h的id 需求2&#xff1a;在需求1基础上&#xff0c;查出关联的h的其它字段&#xff08;name&#xff09; 需求3&#xff1a;在需…

API接口开发1688阿里巴巴官方API接口按关键词采集ALIBABA商品api接入演示

要按关键词采集阿里巴巴商品&#xff0c;你需要使用1688API接口。以下是一个简单的Python示例&#xff0c;展示了如何使用requests库调用阿里巴巴API并解析返回的数据&#xff1a; # coding:utf-8 """ Compatible for python2.x and python3.x requirement: pi…

AR智能眼镜解决方案_MTK平台安卓主板硬件芯片方案开发

AR智能眼镜&#xff0c;是一个可以让现场作业更智能的综合管控设备。采用移动互联网、大数据和云计算等技术&#xff0c;现场数据的采集与分析&#xff1b;同时实现前端现场作业和后端管理的实时连动、信息的同步传输与存储。让前端现场作业更加智能&#xff0c;后端管理更加高…

项目安全性与权限管理实践与探讨

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 一. 身份验证和授权 二. 输入验证和过滤 2.1. 添加O…

洛谷 1506.拯救oibh总部

思路&#xff1a;BFS 一道水题&#xff0c;其实就是对于flood fill的一种变形&#xff0c;我们直接用BFS就解决了&#xff0c;标志在围墙之外的0的状态&#xff0c;然后统计没有被遍历的0就行了。 上代码&#xff1a; #include<iostream> #include<stdio.h> #in…

【微服务篇】深入理解微服务注册中心与配置中心

注册中心 设计一个注册中心时&#xff0c;需要关注多个关键方面&#xff0c;以确保其稳定性、可靠性、性能和可扩展性。以下是一些重要的考虑因素&#xff1a; 服务发现机制&#xff1a;注册中心的核心功能是允许服务相互发现。这意味着当一个服务实例启动时&#xff0c;它需…

Python从零到一构建GPT模型

只用Python和 torch框架&#xff0c;从零到一构建GPT模型&#xff0c;对大语言模型入门&#xff0c;了解GPT的内部网络结构&#xff0c;是一个很好示例。 Build_GPT_from_Scratch.ipynb