第十五章之(三)RTTI

简介:

前言:刚找到新工作,这两天忙着搬家,添加一些日用品,好方便日后使用,浪费了蛮多时间的。所以到现在才补上这一章节。

之前跳过了RTTI,去看下一部分的类型转换运算符,被dynamic_cast搞的很头晕(主要是因为没搞懂为什么用这个),只能回头补上这一节,这时才发现,这一节是不能跳过的。

另外,这小节说实话,蛮难理解的(主要是指其原理,和为什么要有dynamic_cast这个类型转换运算符),这里简单的来概括,就是让指针只有在能安全使用转换后的类方法的情况下,才会被强制转换。

————————————————————————————————————————————————————————————


注:跳过了第十五章的异常

 

所谓RTTI,是运行阶段类型识别(Runtime TypeIdentification)的简称,这是添加到C++中的特性之一。

 

很多老式的实现不支持,另一些实现可能包含开关RITI的编译器设置。

 

RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。很多类库已经为其类对象提供了实现这种功能的方式,但是由于C++内部并不支持,因此各个厂商的机制通常互不兼容。创建一种RTTI语言标准将使得未来的库能够彼此兼容。

 

 

RTTI的用途:

假如有一个类层次结构,其中的类都是从同一个基类M派生而来的(他们可能之中可能有二代、三代甚至更多的派生类,但都不是多重继承,并且基类是M或者是M的派生类)。因此,可以让基类指针(M*)指向其中任何一个类的对象(因为基类指针可以指向派生类对象)。

 

然后,基类指针调用这样一个函数:这个函数的功能是处理一些信息后,选择一个类,并创建该类的对象(例如A类方法内,创建一个B类对象;而在C类方法内,创建一个D类对象。但此时,我们并不知道这个基类指针调用的是A类的方法,还是C类的方法,因此自然也不知道创建的什么类的对象了)。

 

然后返回创建的新对象的地址,并将这个对象的地址赋给基类指针(这是可以的,这个基类指针指向了另一个派生类的对象)。

 

那么问题来了,在这个时候,这个基类指针是指向B类对象,还是D类对象呢?

 

当我们需要解决某些问题的时候,我们需要知道这件事,因此这也就是RTTI的用途。

 

 

为什么要知道指针指向的类型:

当我们需要知道类型时,在以上的基础上,有三种情况:

①我们可能需要调用类方法的正确版本。

例如:如果我们需要调用某一个类方法时。

(1)该类方法都是虚方法,且需要调用对应类的类方法,则无需知道,指针会根据指向对象的类型调用对应的类方法。

(2)该类方法都是虚方法,但需要调用基类的类方法,需要知道类型。因为基类A可能是派生类C的基类B的私有成员(即B是A类的派生类,且是私有继承,而C类又是B类的基类),若是这种情况,则无法使用基类的方法(因为不能访问基类对象,更不要说基类的方法了)。

 

②需要使用不同的方法。例如假如是派生类B,则使用a方法,如果是派生类c,则使用b方法。那么自然需要知道类型,才能正确的使用计划之中的方法。

 

③可能出于调试目的,想跟踪生成的对象的类型。看看生成的对象,是不是自己计划中想要生成的,会不会出现想要生成一个A类对象,却生成了一个B类对象。因此,需要通过知道类型,来检测。

 

可以看出,以上三种情况,都需要知道类型。因此,需要RTTI。

 

 

RTTI的工作原理:

C++有3个支持RTTI的元素:

①如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针。如果做不到,则该运算符返回一个空指针(0)。

 

②typeid运算符返回一个指出对象的类型的值。

 

③type_info结构存储了有关特定类型的信息。

 

只能将RTTI用于包含虚函数的类层次结构,原因在于,只有在这种情况下,才应该将派生类地址赋给基类指针。

 

警告:RTTI只适用于包含虚函数的类。

也就是说,如果想使用RTTI,基类和派生类必须要有虚函数,否则无法使用。

 

 

dynamic_cast运算符:(总的来说,有的迷糊)

dynamic_cast运算符的作用在于,其用于确定和回答“是否可以安全的将对象的地址赋给特定类型的指针”。

其格式为:A*ph= dynamic_cast <B*>( pl ) ;

解释:

①一般A类是派生类,B类是A类或者A类的派生类(如果B类是A类的派生类的话,意思是将指向派生类对象的地址(因为将其进行类型转换了)的指针pl赋值给基类指针ph,因为基类指针指向派生类对象的地址是安全的,反过来则不安全);

作用是:将一个指针pl(其类型待定,也不确定其指向对象是基类还是派生类),将其类型转换为B类(类型不定)后赋值给A类的(A类是B类的基类或者是B类,但总之是基类的派生类)指针ph

此时, A类的指针,指向了另一个对象的地址,而这个对象的类型有三种可能:AA类的基类A类的派生类。只有当对象是A类和A类的派生类时,这种转换才是安全的。


③使用dynamic_cast的前提是多态(但不太明白为什么)

④之所以这么做,有一种可能是基类指针虽然可能指向派生类对象,但只能调用基类的方法。如果想要调用派生类方法,则需要转换,但强制转换不一定是安全的(因为我们不确定其是否指向派生类对象),因此需要这个命令来告诉我们是否是安全的。

⑤这个过程不转换pl的类型

 

 

举个例子,例如A类是基类,B类是A类的派生类,C类是B类的派生类(A->B->C)。

 

①当A类指针a,指向C类对象时,这是可以的(基类指针可以指向派生类对象)。注意,A类指针a的值,是C类对象的地址。

那么将这个A类指针a,强制类型转换为C类指针是否可以呢,答案是可以的。

因为这样做的结果是,C类指针c,指向一个C类对象(强制类型转换后,其值相同,只不过类型变了)。

之所以说是安全的,因为不会产生那种派生类指针调用方法时,调用的是派生类方法,却因为指向基类对象而导致在基类中不存在派生类的同名方法,从而出现调用失败。(见下面代码)(基类指针指向派生类对象之所以安全,是因为基类方法必然被派生类所继承,必然存在继承而来的方法或者同名方法)。

 

②那么假如A类指针a,指向A类对象,被强制转换为C类指针并被赋值给C类指针c(此时,派生类指针指向一个基类对象)

那么这种做法就是不安全的。

假如c调用的方法是基类有的,那么可能不出现问题。

但是假如c调用的方法,是基类A没有的(这是可能的,因为派生类指针理应可以使用派生类有方法,所以编译器无法检测出这种错误);

那就会出现问题(预料之外的问题)。

如代码:

#include<iostream>
class B
{
	int a = 0;
public:
	virtual void add() { a++; }
};
class A :private B
{
	int c = 1;
public:
	virtual void add() { B::add(); c ++; }
	void mm() { std::cout << c << std::endl; }
};

int main()
{
	using namespace std;
	B b;
	B*ph = &b;	//基类指针指向基类的地址
	A*pl = (A*)ph;		//将基类指针强行转换为派生类指针,并将地址赋值给派生类指针。此时派生类指针指向了一个基类对象的地址
	pl->mm();	//指向基类对象的派生类指针调用派生类方法。因此方法调用会输出错误的结果
	pl = dynamic_cast<A*>(ph);	//使用dynamic_cast进行转换,如果是安全的,pl则不是空指针
	if (pl == nullptr)cout << "qqqq" << endl;	//如果是空指针,则输出qqqq

	A*pll = new A;	//派生类指针指向派生类对象
	pll->mm();	//因此方法调用是正常的
	delete pll;

	A pp;
	A* qq = &pp;	//派生类指针指向派生类对象
	pll = dynamic_cast<A*>(qq);	//使用dynamic_cast进行转换,如果是安全的,则不是空指针
	if (pll == 0)cout << "3333" << endl;	//如果输出了3333,则说明是空指针,上一行代码的转换并不安全

	system("pause");
	return 0;
}

其输出结果是:

-858993460
qqqq
1
请按任意键继续. . .

可以从输出结果看到,正常指向派生类对象的mm方法输出的内容是1(因为c被初始化为1),而指向基类对象的mm方法输出的内容是错误的,甚至可能运行出错。

因此,指向基类对象的派生类指针,是不安全的。

而一个指向派生类对象的基类指针,是安全的;

并且,将该指针转换为一个不高于其派生类层次的(即是该派生类或该派生类的基类的类型的)指针,并赋值给同样不高于该派生类层次的指针,也是安全的。

 

因为有区别,所以这是dynamic_cas的存在目的。

 

上代码,用代码表示其作用:

//程序目的:随机创造一个类型的对象,并对其初始化,然后将地址以A类形式返回,并赋给A类指针,然后用A类指针调用show函数(虚函数,三个类都有)
//之后,再尝试用A类指针用dynamic_cast转化为B类指针,查看是否安全,如果安全,则调用take函数(虚的,只有B和C有)
//在第二步的时候,如果不安全,则输出一个提示信息,如果安全,因为take函数是虚的,因此会根据其指向的对象,输出对应类的take函数。
#include<iostream>
#include<ctime>
using namespace std;
class A
{
public:
	virtual void show() { cout << "This is A show." << endl; }
};

class B :public A
{
public:
	virtual void show() { cout << "This is B show." << endl; }
	virtual void take() { cout << "B take out one thing." << endl; }
};

class C :public B
{
public:
	virtual void show() { cout << "This is C show." << endl; }
	virtual void take() { cout << "C take out one thing." << endl; }
};

A* GetOne()	//随机创造A、B、C对象之一,并返回其地址(以指针形式,类型为A*)
{
	int i = rand() % 3;
	A* one;
	if (i == 0)
		one = new A;
	else if (i == 1)
		one = new B;
	else one = new C;
	return one;
}

int main()
{
	A* pp;
	srand(time(0));
	for (int i = 0; i < 5; i++)
	{
		pp = GetOne();	//随机创造一个类对象,返回其地址,用基类(A)指针指向它
		pp->show();		//基类指针根据指向对象输出对应的虚方法
		B* pa = dynamic_cast<B*>(pp);	//B类指针
		if (pa == NULL)cout << "错误的转换,pa是空指针" << endl;	//如果不安全,则输出提示信息
		else pa->take();	//如果安全,则输出对应的方法
		cout << endl;	//空一行表示间隔
		delete pp;
	}
	system("pause");
	return 0;
}


显示:

This is B show.
B take out one thing.

This is A show.
错误的转换,pa是空指针

This is B show.
B take out one thing.

This is C show.
C take out one thing.

This is A show.
错误的转换,pa是空指针

请按任意键继续. . .

总结:

①可以发现,在转换并不安全时(即B类指针pa指向的是A类对象时,基类调用派生类方法是不正确的),返回NULL。因此可以鉴别。

 

②程序可能不支持RTTI,或者支持,但是关闭了这个功能。如果是后者,那么可能编译的时候没问题,但是运行的时候出现问题。

 

③另外,dynamic_cast也可以用于引用只不过,由于没有与NULL对应的引用值,因此当请求不正确时,dynamic_cast将引发类型为bad_cast的异常。该异常是从exception类派生而来的,他是在头文件typeinfo定义的。书上另外给了一个例子,但是因为我跳过了异常章节,所以看不懂。

 

 

 

typeid运算符和type_info类:

typeid运算符使得能够确定 两个对象是否为 同种类型

 

格式为:typeid ( 对象A )== typeid ( 对象B);

 

解释:它与sizeof有些相像,可以接受两种参数(即对象A和对象B所在的位置):

①类名;

②结果为对象的表达式。(如对象名,或者是指向对象的指针使用了解除引用运算符。

 

这段看到这里是不懂的,只知道可以用“==”和“!=”来判断左右两边类型是否相同==>>typeid运算符返回一个对type_info对象的引用(这句话的意思可以看后面的解释)。其中,type_info是在头文件typeinfo(以前是typeinfo.h)中定义的一个类。type_info类重载了“==”和“!=”运算符,以便可以使用这些运算符来对类型进行比较。

 

另外:不能使用cout<< typeid( xxx)来输出,只能使用==和!=

 

例如,如果pg指向的是一个C类对象,则下述表达式的结果为boo值true,否则为false:

typeid (C) ==typeid (*pg);

 

注意:如果pg是一个空指针NULL,将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeid中声明的。

 

type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是 类的名称

其格式为:typeid ( 某类型对象).name();

如:

cout << "当前类型为:" << typeid(*pp).name()<<endl; //输出pp指针指向对象的类型

 

修改上面代码的主函数部分为:

int main()
{
	A* pp;
	srand(time(0));
	for (int i = 0; i < 5; i++)
	{
		pp = GetOne();	//随机创造一个类对象,返回其地址,用基类(A)指针指向它
		pp->show();		//基类指针根据指向对象输出对应的虚方法
		B* pa = dynamic_cast<B*>(pp);	//B类指针
		if (pa == NULL)cout << "错误的转换,pa是空指针" << endl;	//如果不安全,则输出提示信息
		else pa->take();	//如果安全,则输出对应的方法

		if (typeid(B) == typeid(*pp))cout << "对象类型为B" << endl;		//指针指向对象的类型是否为B,如果是B返回true,否则false
		else cout << "对象类型不为B" << endl;
		cout << "当前类型为:" << typeid(*pp).name() << endl;	//输出pp指针指向对象的类型
		cout << endl;	//空一行表示间隔
		delete pp;
	}
	int mm;
	cout << "int类型变量mm的类型为:" << typeid(mm).name() << endl;	//调用name()方法输出int类型的变量的类型
	system("pause");
	return 0;
}

输出结果:

This is C show.
C take out one thing.
对象类型不为B
当前类型为:class C

This is C show.
C take out one thing.
对象类型不为B
当前类型为:class C

This is B show.
B take out one thing.
对象类型为B
当前类型为:class B

This is A show.
错误的转换,pa是空指针
对象类型不为B
当前类型为:class A

This is A show.
错误的转换,pa是空指针
对象类型不为B
当前类型为:class A

int类型变量mm的类型为:int
请按任意键继续. . .

总结:

问题:书上说需要头文件typeinfo,但是我不加这个头文件也可以使用,不知道为什么。我的编译器是VS2015

 

②之前说过,typeid运算符返回的是一个对type_info对象的引用。

例如typeid(*pp) 之所以能调用name()方法,正是因为其表示的是一个类对象,name方法是该类的类方法。

 

③编译器不同时,name()方法输出的内容有可能不同(例如我目前使用的vs2015,输出的内容是class xx)。

 

 

误用RTTI的例子:

判断是否安全,应使用dynamic_cast,判断类型是否一样,用typeid。

所谓的误用,我大概概括了一下,就指的是错误的使用,反而让代码更复杂和繁琐了。

 

假如需要当指针指向不同类型时,调用对应类型对象的同名方法,则应该是使用dynamic_cast和虚函数(因为是同名方法,使用虚函数会根据指针指向的对象类型而决定选择哪个,如果是NULL,则不调用)。

例如可以将以下代码修改:

B* pa = dynamic_cast<B*>(pp);      //B类指针

if (pa == NULL)cout<<"错误的转换,pa是空指针" << endl; //如果不安全,则输出提示信息

else pa->take();         //如果安全,则输出对应的方法

 

修改为:

B* pa;      //B类指针

if (pa = dynamic_cast<B*>(pp))pa->take();     //如果安全,则输出对应的方法

else cout <<"错误的转换,pa是空指针" << endl;   //如果不安全,则输出提示信息

 

if判断语句中的pa=dynamic_cast<B*>(pp)表达式,假如是空指针,则表达式的结果为0(将0赋值给pa),执行else;如果不是空指针,则执行pa->take()。

 

 

 

问题:

书上说,如果放弃dynamic_cast和虚函数,将代码改为类似这样的:

int main()
{
	srand(time(0));
	for (int i = 0; i < 5; i++)
	{
		A *pp = GetOne();	//随机创造一个类对象,返回其地址,用基类(A)指针指向它
		if (typeid(C) == typeid(*pp))
			cout << "1" << endl;
		else if (typeid(B) == typeid(*pp))
			cout << "2" << endl;
		else
			cout << "3" << endl;
		cout << endl;	//空一行表示间隔
		delete pp;
	}
	system("pause");
	return 0;
}

可以起作用(即根据pp指向的对象决定输出哪个)。

但是我实际操作中,假如不使用虚函数,pp的类型则只为A类,而不会成为B类和C类。

只有加上虚函数后,代码才会和书上的运行结果相同。

我的编译器是VS2015,不知道是不是因为编译器的原因。

 

按书上的说法,假如if else句式中使用了typeid,则应该考虑是否应该使用dynamic_cast。

 

备注:

这小节我学的还是有点头晕,可能是因为没有和实际结合使用的原因。也许遇见了实际情况,就知道该如何使用了。


目录
相关文章
|
5月前
|
存储 安全 算法
JAVA泛型:编译时的“守护神”,你值得拥有!
【6月更文挑战第28天】Java泛型,自Java 5起,是编程的得力助手。它引入类型参数,提升代码安全性和重用性。通过泛型,编译时即检查类型,减少运行时错误,简化类型转换,如示例中泛型ArrayList确保只存String。泛型,是代码的忠诚卫士,助力编写更健壮、易读的Java代码。
28 0
|
5月前
|
安全 Java 编译器
JAVA泛型,编译时类型安全的“秘密武器”
【6月更文挑战第28天】Java泛型是JDK 5引入的特性,用于在编译时增强类型安全和代码复用。它允许类、接口和方法使用类型参数,确保运行时类型匹配,减少了类型转换错误。例如,泛型方法`&lt;T&gt; void printArray(T[] array)`能接受任何类型数组,编译器会检查类型一致性。此外,泛型提升了代码的可读性、可维护性和与容器类的配合效率,优化整体软件质量。
44 0
|
5月前
|
程序员 C++
c++primer plus 6 读书笔记 第十二章 类和动态内存分配
c++primer plus 6 读书笔记 第十二章 类和动态内存分配
|
6月前
|
存储 安全 编译器
【C++ 多态原理】深入探讨C++的运行时类型信息(RTTI)和元数据
【C++ 多态原理】深入探讨C++的运行时类型信息(RTTI)和元数据
380 1
|
6月前
|
编译器 C语言
嵌入式C 语言函数宏封装妙招
嵌入式C 语言函数宏封装妙招
55 0
|
XML 安全 Java
教你精通Java语法之第十三章、反射
Java的反射(reflection)机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到,那么,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射(reflection)机制。1. 反射的意义2. 反射重要的几个类: Class类 、Field类、 Method类、 Constructor类3. 学会合理利用反射,一定要在安全环境下使用。
63 0
|
并行计算 Java 编译器
教你精通Java语法之第十五章、Lambda表达式
Lambda表达式的优点很明显,在代码层次上来说,使代码变得非常的简洁。缺点也很明显,代码不易读。1. 代码简洁,开发迅速2. 方便函数式编程3. 非常容易进行并行计算4. Java 引入 Lambda,改善了集合操作1. 代码可读性变差2. 在非并行计算中,很多计算未必有传统的 for 性能要高3. 不容易进行调试。
53 0
【软工】原型化方法与常用动态分析方法
【软工】原型化方法与常用动态分析方法
58 0
|
编译器 C++
C++ Primer Plus 第十四章答案 C++中的代码重用
只有聪明人才能看见的摘要~( ̄▽ ̄~)~
65 0
|
C++
C++ Trick:右值引用、万能引用傻傻分不清楚
C++11标准颁布距今已经十年了,在这个标准中引入了很多新的语言特性,在进一步强化C++的同时,也劝退了很多人,其中就包含右值引用。
496 0