第十五章之(三)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类的指针,指向了另一个对象的地址,而这个对象的类型有三种可能:A类A类的基类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。

 

备注:

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

时间: 2024-12-02 10:27:15

第十五章之(三)RTTI的相关文章

第十五章 接口[《.net框架程序设计》读书笔记]

.net框架|笔记|程序|设计 第十五章 接口 摘要: 接口的应用及完全限定名方式定义接口的应用. 一. 接口与继承 l C#支持单实现继承和多接口继承 l 接口中可以定义:事件.无参属性(属性).含参属性(索引器):C#不允许接口定义任何静态成员(CLR却允许定义静态成员):CLR不允许接口定义实例字段和构造器. l 缺省为public abstract 方法,但不可用任何修饰符进行修饰(包括public) l 将值类型转换为接口类型(假设其实现了某个接口),则值类型被装箱为引用类型,以调用其

第十五章-数据访问部件的应用及编程(三)(1)

字段部件在应用程序中始终是不可见的部件.在程序运行过程中是如此,在程序设计阶段也是如此,但是它在应用中起着非常重要的作用,可以说它是所有数据浏览部件从数据库表中显示.编辑数据的基础.这是因为字段部件直接对应着数据库表中的字段,浏览和修改表中的数据必须要通过字段部件,同时字段部件所拥有的属性可以用来说明数据库表中对应的字段的数据类型.当前的字段值.显示格式.编辑格式等,字段部件的事件如OnValidate可以用来设定输入字段值时进行有效性检验. 数据库表的每一列在应用程序中都有其对应的一个字段部件

第十五章-数据访问部件的应用及编程(三)(4)

15.6.2.3 删除字段部件 用字段编辑器Fields Editor为数据集部件创建好的字段部件都会显示在字段编辑器的Fields列表框中,如果用户认为其中的一些字段部件不合适或不再需要时,可以单击这些不需要的字段部件,然后单击鼠标右键弹出一佣弹出式菜单,从弹出式菜单中选择Delete菜单项,便可删除相应的字段部件,如果在弹出式菜单中单击Select All菜单项,然后选择Delete菜单项,这样会删除已创建好的所有的字段部件.某一个字段部件被删除以后,通过单击Add Fields菜单项可以重

第十五章-数据访问部件的应用及编程(三)(3)

15.6.1.4 字段部件的访问 字段部件对应着数据库表中实际的字段,用户要读写数据库表中的字段值其实是通过访问相应的字段部件进行的.在前面的章节中我们介绍过在Delphi的数据库应用程序中有两类字段部件:一类是利用字段编辑器创建的永久性字段部件:另一类是随着数据集部件被激活(被打开)而动态生成的字段部件.对于永久性字段部件的访问可以直接调用使用字段部件的名字进行.假设我们在设计阶段利用字段编辑器创建了对应于Customer.DB表中Company字段的字段部件Table1Company,下面的

第十五章-数据访问部件的应用及编程(三)(2)

表15.6中的属性并不是所有类型的字段部件都拥有的,如一个TStringField类型的字段部件是没有Currency.MaxValue.MinValue和DisplayFormat属性的,一个TFloatField类型的字段部件是没有Size属性的. 对于布尔型属性,在设计过程中的Object Inspector中双击该属性,该属性的值将会在True和False之间来回切换,其他属性需要用户输入属性值或从下拉式列表框中选取属性值.所有的属性都可以通过程序代码进行设置.大多数属性可以独立地设置,

Flash基础理论课 第十五章 3D基础Ⅰ

返回"Flash基础理论课 - 目录" 前面我们做的一切都是二维的(有时只有一维),但是已经可以做出非常酷的东东了.现在,将它们带入到下一个等级. 创建 3D 图形总是那么另人兴奋.新加入的这个维度似乎将物体真正地带入到了生活中.如何在Flash 中实现 3D 在无数的书籍和教学软件中都有介绍.但是我不打算跳过这些内容,我们会很快地将所有基础的知识讲完.随后,将前面章节中讨论的运动效果放到三维空间中.说得详细些,将给大家介绍速度,加速度,摩擦力,反弹,屏幕环绕,缓动,弹性运动,坐标旋转

WF从入门到精通(第十五章):工作流和事务

学习完本章,你将掌握: 1.了解传统的事务模型以及这种模型在哪些地方适合去使用,哪些地方不适合使用 2.懂得在哪些地方不适合传统的事务以及什么时候是补偿事务的恰当时机 3.看看怎样回滚或补偿事务 4.看看怎样修改默认的补偿顺序 如果你是写软件的,你迟早需要去理解事务处理.事务处理(transactional processing)在这个意义上是指写那些把信息记录到一个持久化资源的软件,这些持久化资源如数据库.Microsoft消息队列(它在底层使用了一个数据库).带事务文件系统的Windows

Flash基础理论课 第十五章 3D基础 Ⅲ

返回"Flash基础理论课 - 目录" 缓动与弹性运动 在3D中的缓动与弹性运动不会比2D中的难多少(第八章的课题).我们只需要为z轴再加入一至两个变量. 缓动 对于缓动的介绍不算很多.在2D中,我们用tx和ty最为目标点.现在只需要再在z轴上加入tz.每帧计算物体每个轴到目标点的距离,并移动一段距离. 让我们来看一个简单的例子,让物体缓动运动到随机的目标点,到达该点后,再选出另一个目标并让物体移动过去.注意后面两个例子,我们又回到了 Ball3D 这个类上.以下是代码(可以在Easi

Flash基础理论课 第十五章 3D基础Ⅱ

返回"Flash基础理论课 - 目录" Z排序 在添加了多个物体后代码中显现出了一个新问题--称为z排序.Z排序就像它的名字一样:物体如何在z轴上进行排序,或者说哪个物体在前面.由于物体都使用纯色,所以看起来不是很明显.为了让效果更加明显,请将 Ball3D的 init方法改为以下代码,并运行刚才那个程序: public function init():void { graphics.lineStyle(0); graphics.beginFill(color); graphics.d