前言:刚找到新工作,这两天忙着搬家,添加一些日用品,好方便日后使用,浪费了蛮多时间的。所以到现在才补上这一章节。
之前跳过了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。
备注:
这小节我学的还是有点头晕,可能是因为没有和实际结合使用的原因。也许遇见了实际情况,就知道该如何使用了。