首页 > 编程语言 >[C++ 从入门到精通] 16.RTTI、dynamic_cast、typeid、虚函数表

[C++ 从入门到精通] 16.RTTI、dynamic_cast、typeid、虚函数表

时间:2023-12-28 14:32:29浏览次数:37  
标签:typeid phuman 16 子类 dynamic Human 指针 Man




文章预览:

  • 一. RTTI是什么
  • 二. dynamic_cast类型(指针/引用)转换
  • 2.1 C风格的强制类型转换
  • 2.2 指针转换(常见用法)
  • 2.3 引用转换
  • 三. typeid运算符
  • 四. type_info类
  • 五. RTTI与虚函数表



一. RTTI是什么

RTTI(Run-Time Type Identification):通过运行时类型信息,程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。

前面我们提过父类指针可以new一个子类对象:

Human* phuman = new Man;
Human &a = *phuman;  // *phuman表示指针phuman所指向的对象

如果父类Human有多个子类,那么通过RTTI可以在系统运行时知道,父类Human指针phuman到底指向其哪个子类的对象。

RTTI我们可以把这个程序看成是一种系统提供给我们的一种能力,或者一种功能。这种功能或者能力是通过2个运算符来体现:

  1. dynamic_cast运算符:能够将基类的指针或者引用安全的转换为派生类的指针或者引用。
    应用:因为父类指针phuman无法调用子类Man对象成员,可以使用dynamic_cast将父类Human指针转换成子类Man指针。
  2. typeid运算符:返回指针或者引用所指对象的实际类型。

补充*:想让RTTI两个运算符能够过正常工作,基类中必须至少要有一个virtual虚函数,不然这两个运算符工作的结构就可能跟我们预期不一致。因为只有虚函数的存在,这两个运算符才会使用指针或者引用所绑定的对象的动态类型。

class Human
{
public:
	virtual void print() { cout << "This is 人类" << endl; }
};

class Man :public Human 
{
public:
	virtual void print() { cout << "This is 男人" << endl; }
};

int main() 
{   
	Human * phuman = new Men;
	phuman->print();

	system("pause");
	return 0;
}

[C++ 从入门到精通] 16.RTTI、dynamic_cast、typeid、虚函数表_虚函数

可以看到,父类和子类中都有virtual虚函数的前提下,如果父类指针指向子类对象,那么父类指针执行的对象就是子类对象里的virtual虚函数This is 男人

但是如果子类Man的成员函数print()不是虚函数(即父类中无同名虚函数),那么即使父类指针是指向的子类Man对象,正常情况下依然不能调用子类函数print()

问题每次想通过父类指针调用子类中的非虚函数,都需要在父类中加上同名虚函数吗?

显然这种方式太麻烦了,这时RTTI这种技术就派上用场了,通过dynamic_cast将父类指针转换成子类指针即可。


二. dynamic_cast类型(指针/引用)转换

优点

如果dynamic_cast运算符能够转换成功,说明这个指针实际上是要转换到的那个类型,即dynamic_cast运算符能够帮我们做安全检查。

2.1 C风格的强制类型转换
Human* phuman = new Man;
Man* p = (Man*)(phuman); // 用c语言风格的强制类型转换,硬把指针转换成Men*;
p->print();              // 能够正常的调用Men类的非虚成员函数print();
2.2 指针转换(常见用法)
class Human
{
public:
	virtual void print(); //使用dynamic_cast转换,父类中必须要有虚函数的存在
};

class Man :public Human
{
public:
	void printMan() { cout << "This is 男人" << endl; }
};
int main() 
{ 
	Human* phuman  = new Man;
	Man* pman = dynamic_cast<Man*>(phuman);
	if (pman != nullptr) {
	    // 可以在这里通过指针操作类Man里边的成员函数,成员变量都能够操作并且安全的操作 
	    pman->printMan();   
	} else {
	    // 转换失败:假设Human有子类Man和Women,指向子类Man的指针phuman被强制转换成子类Women的子针,则会转换失败
	    // Women* pman = dynamic_cast<Women*>(phuman);  这里就会转换失败
	    cout << "phuman不是一个Man类型" << endl;
	}
}

输出:

[C++ 从入门到精通] 16.RTTI、dynamic_cast、typeid、虚函数表_开发语言_02


这里的printMan并不是虚函数,通过dynamic_cast将基类指针转换成了子类指针从而完成的对子类成员函数的调用。

注意:使用dynamic_cast转换,父类中必须要有虚函数的存在,否则会报错:

[C++ 从入门到精通] 16.RTTI、dynamic_cast、typeid、虚函数表_c++_03

2.3 引用转换

如果用dynamic_cast转换失败,则系统会抛出一个std::bad_cast异常。

Human* phuman  = new Man;
Human &q = *phuman; // 这就是引用
try {
    Man& manbm = dynamic_cast<Man&>(q);
    // 转换成功
    cout << "phuman实际是一个Man类型" << endl;
    // 在这里操作类Men里边的成员函数,成员变量都能够操作并且安全的操作
    manbm.printMan();
} catch(std::bad_cast) {
    // 转换失败
    cout << "phuman不是一个Man类型" << endl;
}

三. typeid运算符

typeid运算符是用来获取一个指针/引用/表达式的类型信息,拿到对象类型信息,typeid就会返回一个常量对象的引用,这个常量对象是一个标准库类型type_info(类/类类型)

1.定义

typeid(类型或表达式);  //类型可以是指针或引用

2.包含文件typeid是一个标准库typeinfo中的函数,所以需要添加如下代码:

#include <typeinfo>

3.代码举例

Human* phuman = new Man;
Human& q = *phuman;
cout << typeid(*phuman).name() << endl; // 通过typeid确认phuman指向什么类型 —— class Man;
cout << typeid(q).name() << endl;       // 通过typeid确认q指向什么类型 —— class Man
char a[10] = {5, 1};
int b = 666;
cout << typeid(a).name() << endl;       // char[10]
cout << tyepid(b).name() << endl;       // int
cout << tyepid(19.6).name() << endl;    // double
cout << tyepid("asd").name() << endl;   // char const[4]

4.目的typeid主要是为了比较两个指针是否指向同一种类型的对象

1)两个指针定义的类型相同(Human),不管他们new的是啥,typeid返回的对象类型都相等

该例不太符合我们的期盼和要求:

Human* phuman = new Man;
Human* phuman2 = new Women;
if (typeid(phuman) == typeid(phuman2)) { // 成立
    cout << "phuman和phuman2是同一种类型[看指针定义]" << endl;
}

2)比较对象时,看的是new出来的是哪个对象或者该指针指向的是哪个对象,和定义该指针时定义的类型没关系。

Human* phuman = new Man;    //实际指向Man对象
Human* phuman3 = phuman;    //实际指向Man对象
if (typeid(*phuman) == typeid(Man)) { // 成立
    cout << "phuman指向Man" << endl;
}
if (typeid(*phuman) == typeid(*phuman3)) { // 成立
    cout << "phuman和phuman3指向的对象类型相同" << endl;
}
if (typeid(*phuman) == typeid(Human)) { //父类Human中没有虚函数时,该条件成立
    cout << "phuman和phuman3指向的对象类型相同" << endl;
}

注意1)和2)的写法,2)的写法(带*)才是满足我们期望要求的

切记: 基类必须要有虚函数,只有这样,编译器才会对typeid()中的表达式求值。否则如果某个类型不含有虚函数,则typeid()返回的是表达式的静态类型(定义时的类型),既然是定义的类型,编译器就不需要对表达式求值,也能知道表达式的静态类型。


四. type_info类

1.定义

const type_info& type_info变量 = typeid(类型或表达式);

2.包含文件:typeid是一个标准库typeinfo中的函数,所以需要添加如下代码:

#include <typeinfo>

3.用法

1)获取所指对象的名字.name,返回一个C风格字符串

Human* phuman = new Man;
const type_info& tp = typeid(*phuman);
cout << tp.name() << endl; // class Men

2)==、!=

Human* phuman2 = new Man;
const type_info& tp2 = typeid(*phuman2);
if(tp == tp2) {    // 成立
    cout << "tp和tp2类型相同" << endl;
}
Human* phuman3 = new Women;
const type_info& tp3 = typeid(*phuman3);
if(tp == tp3) {    // 不成立
    cout << "tp和tp3类型相同" << endl;
}

五. RTTI与虚函数表

C++中,如果类里含有虚函数。编译器就会对该类产生一个虚函数表

虚函数里有很多项,每一个项都是一个指针。每个指针指向的是这个类里的各个虚函数的入口地址(方便以后进行调用)。

虚函数表项里,第一个表项很特殊,它指向的不是虚函数的入口地址,它指向的实际上是咱们这个类所关联的type_info对象。

Human* phuman = new Man;
const type_info& tp = typeid(*phuman);  //phuman指向Man这个对象

phuman对象里有一个我们看不见的指针,这个指针指向是这个对象所在的类Man里的虚函数表。


下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。


标签:typeid,phuman,16,子类,dynamic,Human,指针,Man
From: https://blog.51cto.com/u_16436086/9014821

相关文章

  • Ubuntu16下QEMU6.1.0安装和使用完整流程
    目录一、测试环境二、环境搭建1、安装依赖库2、安装pyhton3.82.1、下载Python3.8.172.2、创建新版python的安装目录(便于卸载)2.3、安装依赖库2.4、解压安装包2.5、安装2.6、添加环境变量2.7、配置python3默认版本为3.83、安装gcc73.1、下载安装:3.2、指令安装:三、QEMU安装1、下载解......
  • 文心一言 VS 讯飞星火 VS chatgpt (166)-- 算法导论13.1 6题
    六、用go语言,在一棵黑高为k的红黑树中,内部结点最多可能有多少个?最少可能有多少个?文心一言:红黑树是一种自平衡的二叉查找树,其中每个节点都有一个颜色属性,可以是红色或黑色。红黑树满足以下性质:每个节点或者是红色,或者是黑色。根节点是黑色。每个叶节点(NIL或空节点)是黑色。......
  • ARC168
    [ARC168A]<Inversion>之前打了,忘了,懒得想了,咕。$\texttt{Code}$#include<bits/stdc++.h>usingnamespacestd;#defineintlonglong#defineilinline#definereregisterconstintN=3e5+113;intn,ans;chara[N];ilintread(){reintx=0,f=1;char......
  • AtCoder Regular Contest 168 F Up-Down Queries
    洛谷传送门AtCoder传送门貌似是第三道问号题?感觉前面这个转化不是人能想到的。。。考虑维护\(y\)的差分序列。更进一步地,我们类比slopetrick,维护一个可重集,里面有\(y_{i+1}-y_i\)个\(i\)(为了方便我们让每次操作时\(y_{m+1}\)加\(1\))。那么一次操作就相当于,插......
  • 「悦数图数据库」获 2023 年度 IT168 创新解决方案奖
    近日,由国内知名 IT 垂直门户媒体 IT168 举办的 2023 年度技术卓越奖评选结果正式公布,悦数图数据库荣获人工智能领域创新解决方案奖,充分肯定了悦数在大语言模型和图数据库领域的技术能力和行业前瞻性。图技术结合大模型技术,未来新方向RAG,即Retrieval-AugmentedGeneration,是......
  • Navicat Premium16 永久破解版激活教程
    0.前期准备Navicat160版本与注册机:链接:https://pan.baidu.com/s/16CXh8sSWEShr25cUDvHMcw提取码:ngl0  1.开始操作安装好NavicatPremium16后,管理员打开打开注册机 复制软件路径,点击patch 点击Generate!再复制 断开网络打开Navicat,点击注册 输入永久许可证......
  • P5163 WD与地图
    更好的阅读体验P5163WD与地图喵喵题,但其实没有那么难。删边倒序转成加边是显然的,询问可以通过值域线段树合并实现,修改,合并,查询都是好做的。考虑如何维护动态加边的SCC。难点是每个时刻缩点后的图是一个DAG,并不像无向图的搜索树一样好维护,而且新加入的边可能不会立刻构成SC......
  • ARC167D Good Permutation 题解
    ARC167D看到排列并且有\(i\getsa_i\),就可以直接建出图来,显然是若干个不相干的环。如果不求字典序最小,就可以直接不在同一个环中的\(i,j\)直接交换就可以了,因为它要求了最小化操作数。如果求字典序最小,直接从前往后扫一遍,可以用set维护不在这个环中且\(j>i\)的最小值,如果......
  • 软件设计16
    [实验任务一]:多次撤销和重复的命令模式某系统需要提供一个命令集合(注:可以使用链表,栈等集合对象实现),用于存储一系列命令对象,并通过该命令集合实现多次undo()和redo()操作,可以使用加法运算来模拟实现。实验要求:1. 提交类图;  2. 提交源代码;JAVApackagetext01;importj......
  • 聚合支付项目-16
    1、商户服务需求概述1.1商户注册惠民支付为商户提供聚合支付业务,线下商户和线上商户都可以使用惠民支付平台。什么是线下和线上商户?1)线下场所支付商户使用线下场所支付的商户是指有实体经营场所的商家,也称为地面商户,一般包含酒店、餐厅、酒吧、美容、美发、媒体、影楼、家政......