C++的 RTTI 观念和用途
https://www.cnblogs.com/findumars/p/6358194.html
自从1993年Bjarne Stroustrup 〔注1 〕提出有关C++ 的RTTI功能之建议﹐以及C++的异常处理(exception handling)需要RTTI;最近新推出的C++ 或多或少已提供RTTI。 然而,若不小心使用RTTI,可能会导致软件弹性的降低。本文将介绍RTTI的观念和近况﹐并说明如何善用它。
什么是RTTI﹖
在C++ 环境中﹐头文件(header file) 含有类之定义(class definition)亦即包含有关类的结构资料(representational information)。但是﹐这些资料只供编译器(compiler)使用﹐编译完毕后并未留下来﹐所以在执行时期(at run-time) ﹐无法得知对象的类资料﹐包括类名称、数据成员名称与类型、函数名称与类型等等。例如﹐两个类Figure和Circle﹐其之间为继承关系。
若有如下指令﹕
Figure *p;
p = new Circle();
Figure &q = *p;
在执行时﹐p指向一个对象﹐但欲得知此对象之类资料﹐就有困难了。同样欲得知q 所参考(reference) 对象的类资料﹐也无法得到。RTTI(Run-Time Type Identification)就是要解决这困难﹐也就是在执行时﹐您想知道指针所指到或参考到的对象类型时﹐该对象有能力来告诉您。随着应用场合之不同﹐所需支持的RTTI范围也不同。最单纯的RTTI包括﹕
●类识别(class identification)──包括类名称或ID。
●继承关系(inheritance relationship)──支持执行时期的「往下变换类型」(downward casting)﹐亦即动态变换类型(dynamic casting) 。
在对象数据库存取上﹐还需要下述RTTI﹕
●对象结构(object layout) ──包括属性的类型、名称及其位置(position或offset)。
●成员函数表(table of functions)──包括函数的类型、名称、及其参数类型等。
其目的是协助对象的I/O 和持久化(persistence) ﹐也提供调试讯息等。
若依照Bjarne Stroustrup 之建议〔注1 〕﹐C++ 还应包括更完整的RTTI﹕
●能得知类所实例化的各对象 。
●能参考到函数的源代码。
●能取得类的有关在线说明(on-line documentation) 。
其实这些都是C++ 编译完成时所丢弃的资料﹐如今只是希望寻找个途径来将之保留到执行期间。然而﹐要提供完整的RTTI﹐将会大幅提高C++ 的复杂度﹗
RTTI可能伴随的副作用
RTTI最主要的副作用是﹕程序员可能会利用RTTI来支持其「复选」(multiple-selection)方法﹐而不使用虚函数(virtual function)方法。
虽然这两种方法皆能达到多态化(polymorphism) ﹐但使用复选方法﹐常导致违反著名的「开放╱封闭原则」(open/closed principle) 〔注2 〕。反之﹐使用虚函数方法则可合乎这个原则.
Circle和Square皆是由Figure所派生出来的子类﹐它们各有自己的draw()函数。当C++ 提供了RTTI﹐就可写个函数如下﹕
void drawing( Figure *p )
{
if( typeid(*p).name() == "Circle" )
((Circle*)p) -> draw();
if( typeid(*p).name() == "Rectangle" )
((Rectangle*)p) -> draw();
}
虽然drawing() 函数也具有多型性﹐但它与Figure类体系的结构具有紧密的相关性。当Figure类体系再派生出子类时﹐drawing() 函数的内容必须多加个if指令。因而违反了「开放╱封闭原则」﹐如下﹕
很显然地﹐drawing() 函数应加以修正。
想一想﹐如果C++ 并未提供RTTI﹐则程序员毫无选择必须使用虚函数来支持drawing() 函数的多型性。于是程序员将draw()宣告为虚函数﹐并写drawing() 如下﹕
void drawing(Figure *p)
{ p->draw(); }
如此﹐Figure类体系能随时派生类﹐而不必修正drawing() 函数。亦即﹐Figure体系有个稳定的接口(interface) ﹐drawing() 使用这接口﹐使得drawing() 函数也稳定﹐不会随Figure类体系的扩充而变动。这是封闭的一面。而这稳定的接口并未限制Figure体系的成长﹐这是开放的一面。因而合乎「开放╱封闭」原则﹐软件的结构会更具弹性﹐更易于随环境而不断成长。
RTTI的常见的使用场合
一般而言﹐RTTI的常见使用场合有四﹕异常处理(exceptions handling)、动态转类型(dynamic casting) 、模块集成、以及对象I/O 。
1.异常处理── 大家所熟悉的C++ 新功能﹕异常处理﹐其需要RTTI﹐如类名称等。
2.动态转类型── 在类体系(class hierarchy) 中﹐往下的类型转换需要类继承的RTTI。
3.模块集成── 当某个程序模块里的对象欲跟另一程序模块的对象沟通时﹐应如何得知对方的身分呢﹖知道其身分资料﹐才能呼叫其函数。一般的C++ 程序﹐常见的解决方法是──在源代码中把对方对象之类定义(即存在头文件里)包含进来﹐在编译时进行连结工作。然而﹐像目前流行的主从(Client-Server) 架构中﹐客户端(client)的模块对象﹐常需与主机端(server)的现成模块对象沟通﹐它们必须在执行时沟通﹐但又常无法一再重新编译。于是靠标头文件来提供的类定义资料﹐无助于执行时的沟通工作﹐只得依赖RTTI了。
4.对象I/O ── C++ 程序常将其对象存入数据库﹐未来可再读取之。对象常内含其它小对象﹐因之在存入数据库时﹐除了必须知道对象所属的类名称﹐也必须知道各内含小对象之所属类﹐才能完整地将对象存进去。储存时﹐也将这些RTTI资料连同对象内容一起存入数据库中。未来读取对象时﹐可依据这些RTTI资料来分配内存空间给对象。
RTTI从那里来﹖
上述谈到RTTI的用途﹐以及其副作用。这众多争论﹐使得RTTI的标准迟迟未呈现出来。也导致各C++ 开发环境提供者﹐依其环境所需而以各种方式来支持RTTI﹐且其支持RTTI的范围也所不同。 目前常见的支持方式包括﹕
●由类库提供RTTI──例如﹐Microsoft 公司的Visual C++环境。
●由C++ 编译器(compiler)提供──例如﹐Borland C++ 4.5 版本。
●由源代码产生器(code generator)提供──例如Bellvobr系统。
●由OO数据库的特殊预处理器(preprocessor)提供──例如Poet系统。
●由程序员自己加上去。
这些方法皆只提供简单的RTTI﹐其仅为Stroustrup先生所建议RTTI内涵的部分集合而已。相信不久的将来﹐会由C++ 编译器来提供ANSI标准的RTTI﹐但何时会订出这标准呢﹖ 没人晓得吧﹗
程序员自己提供的RTTI
通常程序员自己可提供简单的RTTI﹐例如提供类的名称或识别(TypeID)。最常见的方法是﹕为类体系定义些虚函数如Type_na() 及Isa() 函数等。请先看个例子﹕
class Figure { };
class Rectangle : public Figure { };
class Square : public Rectangle
{ int data;
public:
Square() { data=88; }
void Display() { cout << data << endl; }
};
void main()
{ Figure *f = new Rectangle();
Square *s = (Square *)f;
s -> Display();
}
这时s 指向Rectangle 之对象﹐而s->Display()呼叫Square::Display() ﹐将找不到data值。若在执行时能利用RTTI来检查之﹐就可发出错误讯息。于是﹐自行加入RTTI功能﹕
class Figure
{ public:
virtual char* Type_na() { return "Figure"; }
virtual int Isa(char* cna) { return !strcmp(cna, "Figure")? 1:0; }
};
class Rectangle:public Figure
{ public:
virtual char* Type_na() { return "Rectangle"; }
virtual int Isa(char* cna) { return !strcmp(cna, "Rectangle")?1 : Figure::Isa(cna); }
static Rectangle* Dynamic_cast(Figure* fg) { return fg -> Isa(Type_na())?(Rectangle*)fg : 0; }
};
class Square:public Rectangle
{ int data;
public:
Square() { data=88; }
virtual char* Type_na() { return "Square"; }
virtual int Isa(char* cna) { return !strcmp(cna, "Rectangle")? 1 : Rectangle::Isa(cna); }
static Square* Dynamic_cast(Figure *fg) { return fg->Isa(Type_na())? (Square*)fg : 0; }
void Display() { cout << "888" << endl; }
};
虚函数Type_na() 提供类名称之RTTI﹐而Isa() 则提供继承之RTTI﹐用来支持「动态转类型」函数──Dynamic_cast()。例如﹕
Figure *f = new Rectangle();
cout << f -> Isa("Square") << endl;
cout << f -> Isa("Figure") << endl;
这些指令可显示出﹕f 所指向之对象并非Square之对象﹐但是Figure之对象(含子孙对象)。再如﹕
Figure *f; Square *s;
f = new Rectangle();
s = Square == Dynamic_cast(f);
if(!s)
cout << "dynamic_cast error!!" << endl;
此时﹐依RTTI来判断出这转类型是不对的。
类库提供RTTI
由类库提供RTTI是最常见的﹐例如Visual C++的MFC 类库内有个CRuntimeClass 类﹐ 其内含简单的RTTI。请看个程序﹕
class Figure:public CObject
{
DECLARE_DYNAMIC(Figure);
};
class Rectangle : public Figure
{
DECLARE_DYNAMIC(Rectangle);
};
class Square : public Rectangle
{
DECLARE_DYNAMIC(Square);
int data;
public:
void Display() { cout << data << endl; }
Square() { data=88; }
};
IMPLEMENT_DYNAMIC(Figure, CObject);
IMPLEMENT_DYNAMIC(Rectangle, Figure);
IMPLEMENT_DYNAMIC(Square, Rectangle);
Visual C++程序依赖这些宏(Macor) 来支持RTTI。现在就看看如何使用CRuntimeClass类吧﹗如下﹕
CRuntimeClass *r;
Figure *f = new Rectangle();
r = f -> GetRuntimeClass();
cout << r -> m_psClassName << endl;
这就在执行时期得到类的名称。Visual C++的类库仅提供些较简单的RTTI──类名称、对象大小及父类等。至于其它常用的RTTI如──数据项的类型及位置(position)等皆未提供。
C++编译器提供RTTI
由C++ 语言直接提供RTTI是最方便了﹐但是因RTTI的范围随应用场合而不同﹐若C++语言提供所有的RTTI﹐将会大幅度增加C++ 的复杂度。目前﹐C++ 语言只提供简单的RTTI﹐例如Borland C++ 新增typeid()操作数以及dynamic_cast<T*>函数样版。请看个程序﹕
class Figure
{ public:
virtual void Display();
};
class Rectangle : public Figure { };
class Square:public Rectangle
{ int data;
public:
Square() { data=88; }
void Display() { cout << data << endl; }
};
现在看看如何使用typeid()操作数──
Figure *f = new Square();
const typeinfo ty = typeid(*f);
cout << ty.name() << endl;
这会告诉您﹕f 指针所指的对象﹐其类名称是Square。再看看如何使用dynamic_cast<T*>函数样版──
Figure *f; Square *s;
f = new Rectangle();
s = dynamic_cast<Sqiare *>(f);
if(!s)
cout << "dynamic casting error!!" << endl;
在执行时﹐发现f 是不能转为Square *类型的。如下指令﹕
Figure *f; Rectangle *r;
f = new Square();
r = dynamic_cast<Rectangle *>(f);
if(r) r->Display();
这种类型转换是对的。
RTTI与虚函数表
在C++ 程序中﹐若类含有虚函数﹐则该类会有个虚函数表(Virtual Function Table﹐ 简称VFT )。为了提供RTTI﹐C++ 就将在VFT 中附加个指针﹐指向typeinfo对象﹐这对象内含RTTI资料.
由于该类所实例化之各对象﹐皆含有个指针指向VFT 表﹐因之各对象皆可取出typeinfo对象而得到RTTI。例如﹐
Figure *f1 = new Square();
Figure *f2 = new Square();
const typeinfo ty = typeid(*f2);
其中﹐typeid(*f2) 的动作是﹕
1.取得f2所指之对象。
2.从对象取出指向VMF 之指针﹐经由此指针取得VFT 表。
3.从表中找出指向typeinfo对象之指针﹐经由此指针取得typeinfo对象。
这typeinfo对象就含有RTTI了。经由f1及f2两指针皆可取得typeinfo对象﹐所以 typeid(*f2) == typeid(*f1)。
总结
RTTI是C++ 的新功能。过去﹐C++ 语言来提供RTTI时﹐大多依赖类库来支持﹐但各类库使用的方法有所不同﹐使得程序的可移植性(portability) 大受影响。然而﹐目前C++ 也只提供最简单的RTTI而已﹐可预见的未来﹐当大家对RTTI的意见渐趋一致时﹐C++ 将会提供更完整的RTTI﹐包括数据项和成员函数的类型、位置(offset)等资料﹐使得C++ 程序更井然有序﹐易于维护。
参考资料
[注1] Stroustrup B., “Run-Time Type Identification for C++”, Usenix C++ C
onference, Portland, 1993.
[注2] Meyer B.,Object-Oriented Software Construction, Prentice Hall, 1988
C++中的RTTI机制解析
https://blog.csdn.net/three_bird/article/details/51479175
RTTI概念
RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
RTTI机制的产生
为什么会出现RTTI这一机制,这和C++语言本身有关系。和很多其他语言一样,C++是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。
我对Java的运行时类型识别不是很熟悉,所以查了一下相关资料:Java中任何一个类都可以通过反射机制来获取类的基本信息(接口、父类、方法、属性、Annotation等),而且Java中还提供了一个关键字,可以在运行时判断一个类是不是另一个类的子类或者是该类的对象,Java可以生成字节码文件,再由JVM(Java虚拟机)加载运行,字节码文件中可以含有类的信息。
typeid和dynamic_cast操作符
RTTI提供了两个非常有用的操作符:typeid和dynamic_cast。
typeid操作符,返回指针和引用所指的实际类型;
dynamic_cast操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
我们知道C++的多态性(运行时)是由虚函数实现的,对于多态性的对象,无法在程序编译阶段确定对象的类型。当类中含有虚函数时,其基类的指针就可以指向任何派生类的对象,这时就有可能不知道基类指针到底指向的是哪个对象的情况,类型的确定要在运行时利用运行时类型标识做出。为了获得一个对象的类型可以使用typeid函数,该函数反回一个对type_info类对象的引用,要使用typeid必须使用头文件<typeinfo>,因为typeid是一个返回类型为typ_info的引用的函数所以这里有必要先介绍一下type_info类。
下面是typeinfo的源代码:
/*** *typeinfo.h - Defines the type_info structure and exceptions used for RTTI * * Copyright (c) Microsoft Corporation. All rights reserved. * Modified January 1996 by P.J. Plauger * *Purpose: * Defines the type_info structure and exceptions used for * Runtime Type Identification. * * [Public] * ****/#pragma once#ifndef _TYPEINFO_ #define _TYPEINFO_ #ifndef RC_INVOKED #include <xstddef> #include <string.h> // for type_info::hash_code()#pragma pack(push,_CRT_PACKING) #pragma warning(push,3) #pragma push_macro("new") #undef new #pragma warning(disable: 4275)#ifndef __cplusplus#error This header requires a C++ compiler ...#endif#if !defined(_WIN32)#error ERROR: Only Win32 target supported!#endifstruct __type_info_node {void *_MemPtr;__type_info_node* _Next; };extern __type_info_node __type_info_root_node;class type_info { public:size_t hash_code() const _THROW0(){ // hash name() to size_t value by pseudorandomizing transformreturn (_STD _Hash_seq((const unsigned char *) name(),_CSTD strlen(name())));}#if defined(CRTDLL) && defined(_CRTBLD)_CRTIMP_PURE #endif#ifdef _M_CEE[System::Security::SecurityCritical]#endifvirtual ~type_info() _NOEXCEPT; #if defined(_SYSCRT)_CRTIMP_PURE int __CLR_OR_THIS_CALL operator==(const type_info& _Rhs) const;_CRTIMP_PURE int __CLR_OR_THIS_CALL operator!=(const type_info& _Rhs) const; #else_CRTIMP_PURE bool __CLR_OR_THIS_CALL operator==(const type_info& _Rhs) const;_CRTIMP_PURE bool __CLR_OR_THIS_CALL operator!=(const type_info& _Rhs) const; #endif_CRTIMP_PURE bool __CLR_OR_THIS_CALL before(const type_info& _Rhs) const;_CRTIMP_PURE const char* __CLR_OR_THIS_CALL name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;_CRTIMP_PURE const char* __CLR_OR_THIS_CALL raw_name() const; private:void *_M_data;char _M_d_name[1]; #if defined(_CRTBLD) /* TRANSITION */__CLR_OR_THIS_CALL type_info(const type_info& _Rhs);type_info& __CLR_OR_THIS_CALL operator=(const type_info& _Rhs); #else public:__CLR_OR_THIS_CALL type_info(const type_info&) = delete;type_info& __CLR_OR_THIS_CALL operator=(const type_info&) = delete; private: #endif_CRTIMP_PURE static const char *__CLRCALL_OR_CDECL _Name_base(const type_info *,__type_info_node* __ptype_info_node);_CRTIMP_PURE static void __CLRCALL_OR_CDECL _Type_info_dtor(type_info *); #if defined(_CRTBLD) #if !defined(_SYSCRT)_CRTIMP_PURE static const char *__CLRCALL_OR_CDECL _Name_base_internal(const type_info *,__type_info_node* __ptype_info_node);_CRTIMP_PURE static void __CLRCALL_OR_CDECL _Type_info_dtor_internal(type_info *);public:// CRT dll import libs alias non _internal to _internal. These method definitions are// only used within the crtdll to provide targets for aliasobj in the crt import lib._CRTIMP_PURE void __CLR_OR_THIS_CALL _type_info_dtor_internal_method(void);_CRTIMP_PURE const char* __CLR_OR_THIS_CALL _name_internal_method(__type_info_node* __ptype_info_node) const; #endif #endif };#if _HAS_EXCEPTIONS_STD_BEGINusing ::type_info;_STD_END#if !defined(_CRTBLD) || !defined(_TICORE)// This include must occur below the definition of class type_info #include <exception>_STD_BEGINclass _CRTIMP_PURE bad_cast : public exception { public: #ifdef _M_CEE_PURE__CLR_OR_THIS_CALL bad_cast(const char * _Message = "bad cast"): exception(_Message){}__CLR_OR_THIS_CALL bad_cast(const bad_cast &_That): exception(_That){}virtual __CLR_OR_THIS_CALL ~bad_cast() _NOEXCEPT{} #if defined(_CRTBLD) && defined(CRTDLL) private:// This is aliased to public:bad_cast(const char * const &) to provide// the old, non-conformant constructor.__CLR_OR_THIS_CALL bad_cast(const char * const * _Message): exception((const char *)_Message){ } #endif /* _CRTBLD && CRTDLL */ #else /* _M_CEE_PURE */__CLR_OR_THIS_CALL bad_cast(const char * _Message = "bad cast");__CLR_OR_THIS_CALL bad_cast(const bad_cast &);virtual __CLR_OR_THIS_CALL ~bad_cast() _NOEXCEPT; #if defined(_CRTBLD) && defined(CRTDLL) private:// This is aliased to public:bad_cast(const char * const &) to provide// the old, non-conformant constructor.__CLR_OR_THIS_CALL bad_cast(const char * const * _Message); #endif /* _CRTBLD && CRTDLL */ #endif /* _M_CEE_PURE */ };class _CRTIMP_PURE bad_typeid : public exception { public: #ifdef _M_CEE_PURE__CLR_OR_THIS_CALL bad_typeid(const char * _Message = "bad typeid"): exception(_Message){}__CLR_OR_THIS_CALL bad_typeid(const bad_typeid &_That): exception(_That){}virtual __CLR_OR_THIS_CALL ~bad_typeid() _NOEXCEPT{} #else /* _M_CEE_PURE */__CLR_OR_THIS_CALL bad_typeid(const char * _Message = "bad typeid");__CLR_OR_THIS_CALL bad_typeid(const bad_typeid &);virtual __CLR_OR_THIS_CALL ~bad_typeid() _NOEXCEPT; #endif /* _M_CEE_PURE */};class _CRTIMP_PURE __non_rtti_object : public bad_typeid { public: #ifdef _M_CEE_PURE__CLR_OR_THIS_CALL __non_rtti_object(const char * _Message): bad_typeid(_Message){}__CLR_OR_THIS_CALL __non_rtti_object(const __non_rtti_object &_That): bad_typeid(_That){}virtual __CLR_OR_THIS_CALL ~__non_rtti_object() _NOEXCEPT{} #else /* _M_CEE_PURE */__CLR_OR_THIS_CALL __non_rtti_object(const char * _Message);__CLR_OR_THIS_CALL __non_rtti_object(const __non_rtti_object &);virtual __CLR_OR_THIS_CALL ~__non_rtti_object() _NOEXCEPT; #endif /* _M_CEE_PURE */ };_STD_END #endif // !_CRTBLD || !_TICORE#else_STD_BEGIN// CLASS bad_cast class _CRTIMP2 bad_cast: public exception{ // base of all bad cast exceptions public:bad_cast(const char *_Message = "bad cast") _THROW0(): exception(_Message){ // construct from message string}virtual ~bad_cast() _NOEXCEPT{ // destroy the object}protected:virtual void _Doraise() const{ // perform class-specific exception handling_RAISE(*this);}};// CLASS bad_typeid class _CRTIMP2 bad_typeid: public exception{ // base of all bad typeid exceptions public:bad_typeid(const char *_Message = "bad typeid") _THROW0(): exception(_Message){ // construct from message string}virtual ~bad_typeid() _NOEXCEPT{ // destroy the object}protected:virtual void _Doraise() const{ // perform class-specific exception handling_RAISE(*this);}};class _CRTIMP2 __non_rtti_object: public bad_typeid{ // report a non RTTI object public:__non_rtti_object(const char *_Message): bad_typeid(_Message){ // construct from message string}};_STD_END#endif /* _HAS_EXCEPTIONS */#endif /* RC_INVOKED */#pragma pop_macro("new") #pragma pack(pop) #pragma warning(pop)#endif // _TYPEINFO_/** Copyright (c) Microsoft Corporation. ALL RIGHTS RESERVED.* Modified January 1996 by P.J. Plauger* Modified November 1998 by P.J. Plauger* Consult your license regarding permissions and restrictions.V6.00:0009 */
对于源码可以简单解释为:
class type_info { public: //析构函数 _CRTIMP virtual ~type_info(); //重载的==操作符 _CRTIMP int operator==(const type_info& rhs) const; //重载的!=操作符 _CRTIMP int operator!=(const type_info& rhs) const; _CRTIMP int before(const type_info& rhs) const;//用于type_info对象之间的排序算法 //返回类的名字 _CRTIMP const char* name() const; _CRTIMP const char* raw_name() const;//返回类名称的编码字符串 private: //各种存储数据成员 void *_m_data; char _m_d_name[1]; //将拷贝构造函数与赋值构造函数设为了私有 type_info(const type_info& rhs); type_info& operator=(const type_info& rhs); };
因为type_info类的复制构造函数和赋值运算符都是私有的,所以不允许用户自已创建type_info的类。唯一要使用type_info类的方法就是使用typeid函数。
typeid函数
typeid函数的主要作用就是让用户知道当前的变量是什么类型的,比如使用typeid(a).name()就能知道变量a是什么类型的。typeid()函数的返回类型为typeinfo类型的引用。
typeid函数是type_info类的一个引用对象,可以访问type_info类的成员。但因为不能创建type_info类的对象,而typeid又必须反回一个类型为type_info类型的对象的引用,所以怎样在typeid函数中创建一个type_info类的对象以便让函数反回type_info类对象的引用就成了问题。这可能是把typid函数声明为了type_info类的友元函数来实现的,默认构造函数并不能阻止该类的友元函数创建该类的对象。所以typeid函数如果是友元的话就可以访问type_info类的私有成员,从而可以创建type_info类的对象,从而可以创建返回类型为type_info类的引用。
例如:
class A{ private: A(){} A(const A&){} A& operator = (const A&){} friend A& f(); };
函数f()是类A的友元,所以在函数f()中可以创建类A的对象。同时为了实现函数f()反回的对象类型是A的引用,就必须在函数f中创建一个类A的对象以作为函数f的反回值,比如函数f可以这样定义:
A &f() { A m_a; return m_a; }
因为typeid函数是type_info类的对象,也就是说可以用该函数访问type_info类的成员,即type_info类中重载的==和!=运算符,name()和before()成员函数,比如typid(a).name()和typid(a)==typid(b)等等。
class A{ private:A(){ b = 3; cout << "A\n"; } public:void name(){cout << "Class Name is A\n";}friend A &f(); private:int b; }; A &f() {A friend_A;cout << "The function of Class A\n";return friend_A; } int main() {f().name();return 0; }
运行截图:
函数f()是类A的友元,且返回一个类A的对象,因为f()函数是类A的友元,所以在函数f中可以用默认构造函数创建类A的对象,这时函数f()同时是一个函数,也是类A的对象,因此也可以访问类A中的成员。
typeid函数的使用示例:
class A{ private:int a; };class B :public A{ public:virtual void f(){ cout << "HelloWorld\n"; } private:int b; };class C :public B{ public:virtual void f(){ cout << "HelloWorld++\n"; } private:int c; };class D :public A{ public:virtual void f(){ cout << "HelloWorld--\n"; } private:int d; }; int main() {int a = 2;cout << typeid(a).name() << endl;A objA;//打印出class A cout << typeid(objA).name() << endl;B objB;//打印出class B cout << typeid(objB).name() << endl;C objC;//打印出class C cout << typeid(objC).name() << endl;//以下是多态在VC 6.0编译器不支持,但是在GCC以及微软更高版本的编译器却都是//支持的,且是在运行时候来确定类型的,而不是在编译器,会打印出class cB *ptrB=new C();cout<<typeid(*ptrB).name()<<endl;A *ptrA = new D();//打印出class A而不是class D cout << typeid(*ptrA).name() << endl;return 0; }
运行截图:
dynamic_cast强制转换运算符
该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,注意dynamic_cast转换符只能用于含有虚函数的类,其表达式为dynamic_cast<类型>(表达式),其中的类型是指把表达式要转换成的目标类型,比如含有虚函数的基类B和从基类B派生出的派生类D,则B *pb; D *pd, md; pb=&md; pd=dynamic<D*>(pb); 最后一条语句表示把指向派生类D的基类指针pb转换为派生类D的指针,然后将这个指针赋给派生类D的指针pd,有人可能会觉得这样做没有意义,既然指针pd要指向派生类为什么不pd=&md;这样做更直接呢?
因为有些时候我们需要强制转换,比如如果指向派生类的基类指针B想访问派生类D中的除虚函数之外的成员时就需要把该指针转换为指向派生类D的指针,以达到访问派生类D中特有的成员的目的,比如派生类D中含有特有的成员函数g(),这时可以这样来访问该成员dynamic_cast<D*>(pb)->g();因为dynamic_cast转换后的结果是一个指向派生类的指针,所以可以这样访问派生类中特有的成员。但是该语句不影响原来的指针的类型,即基类指针pb仍然是指向基类B的。
dynamic_cast转换符只能用于指针或者引用。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast转换操作符在执行类型转换时首先将检查能否成功转换,如果能成功转换则转换之,如果转换失败,如果是指针则反回一个0值,如果是转换的是引用,则抛出一个bad_cast异常,所以在使用dynamic_cast转换之间应使用if语句对其转换成功与否进行测试,比如pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者这样测试if(dynamic_cast<D*>(pb)){…}else{…}。
对于其它的强制转换运算符:static_cast,reinterpret_cast,const_cast,请阅读C++中“强制转换”的四大天王。
写代码是一种艺术,甚于蒙娜丽莎的微笑。
C++对象模型之RTTI的实现原理
https://blog.csdn.net/ljianhui/article/details/46487951
RTTI是Runtime Type Identification的缩写,意思是运行时类型识别。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型(int,指针等)的变量对应的类型。
C++通过以下的两个操作提供RTTI:
(1)typeid运算符,该运算符返回其表达式或类型名的实际类型。
(2)dynamic_cast运算符,该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用。
下面分别详细地说明这两个操作的实现方式。
注所有的测试代码的测试环境均为:32位Ubuntu 14.04 g++ 4.8.2,若在不同的环境中进行测试,结果可能有不同。
1、typeid运算符
typeid运算符,后接一个类型名或一个表达式,该运算符返回一个类型为std::typeinf的对象的const引用。type_info是std中的一个类,它用于记录与类型相关的信息。类type_info的定义大概如下:
class type_info
{
public:
virtual ~type_info();
bool operator==(const type_info&)const;
bool operator!=(const type_info&)const;
bool before(const type_info&)const;
const char* name()const;
private:
type_info(const type_info&);
type_info& operator=(const type_info&);
// data members
};
至于data members部分,不同的编译器会有所不同,但是都必须提供最小量的信息是class的真实名称和在type_info对象之间的某些排序算法(通过before()成员函数提供),以及某些形式的描述器,用来表示显式的类的类型和该类的任何子类型。
从上面的定义也可以看到,type_info提供了两个对象的相等比较操作,但是用户并不能自己定义一个type_info的对象,而只能通过typeid运算符返回一个对象的const引用来使用type_info的对象。因为其只声明了一个构造函数(复制构造函数)且为private,所以编译器不会合成任何的构造函数,而且赋值操作运行符也为private。这两个操作就完全禁止了用户对type_info对象的定义和复制操作,用户只能通过指向type_info的对象的指针或引用来使用该类。
下面说说,typeid对静态类型的表达式和动态类型的表达式的处理和实现。
1)typeid识别静态类型
当typeid中的操作数是如下情况之一时,typeid运算符指出操作数的静态类型,即编译时的类型。
(1)类型名
(2)一个基本类型的变量
(3)一个具体的对象
(4)一个指向不含有virtual函数的类对象的指针的解引用
(5)一个指向不含有virtual函数的类对象的引用
静态类型在程序的运行过程中并不会改变,所以并不需要在程序运行时计算类型,在编译时就能根据操作数的静态类型,推导出其类型信息。例如如下的代码片断,typeid中的操作数均为静态类型:
class X { ...... // 具有virtual函数 };
class XX : public X { ...... // 具有virtual函数};
class Y { ...... // 没有virtual函数};
int main()
{
int n = 0;
XX xx;
Y y;
Y *py = &y;
// int和XX都是类型名
cout << typeid(int).name() << endl;
cout << typeid(XX).name() << endl;
// n为基本变量
cout << typeid(n).name() << endl;
// xx所属的类虽然存在virtual,但是xx为一个具体的对象
cout << typeid(xx).name() << endl;
// py为一个指针,属于基本类型
cout << typeid(py).name() << endl;
// py指向的Y的对象,但是类Y不存在virtual函数
cout << typeid(*py).name() << endl;
return 0;
}
2)typeid识别多态类型
当typeid中的操作数是如下情况之一时,typeid运算符需要在程序运行时计算类型,因为其其操作数的类型在编译时期是不能被确定的。
(1)一个指向不含有virtual函数的类对象的指针的解引用
(2)一个指向不含有virtual函数的类对象的引用
多态的类型是可以在运行过程中被改变的,例如,一个基类的指针,在程序运行的过程中,它可以指向一个基类对象,也可以指向该基类的派生类的对象,而typeid运算符需要在运行过程中识别出该基类指针所指向的对象的实际类型,这就需要typeid运算符在运行过程中计算其指向的对象的实际类型。例如对于以下的类定义:
class X
{
public:
X()
{
mX = 101;
}
virtual void vfunc()
{
cout << "X::vfunc()" << endl;
}
private:
int mX;
};
class XX : public X
{
public:
XX():
X()
{
mXX = 1001;
}
virtual void vfunc()
{
cout << "XX::vfunc()" << endl;
}
private:
int mXX;
};
使用如下的代码进行测试:
void printTypeInfo(const X *px)
{
cout << "typeid(px) -> " << typeid(px).name() << endl;
cout << "typeid(*px) -> " << typeid(*px).name() << endl;
}
int main()
{
X x;
XX xx;
printTypeInfo(&x);
printTypeInfo(&xx);
return 0;
}
其输出如下:
从输出的结果可以看出,无论printTypeInfo函数中指针px指向的对象是基类X的对象,还是指向派生类XX的对象,typeid运行返回的px的类型信息都是相同的,因为px为一个静态类型,其类型名均为PX1X。但是typeid运算符却能正确地计算出了px指向的对象的实际类型。(注:由于C++为了保证每一个类在程序中都有一个独一无二的类名,所以会对类名通过一定的规则进行改写,所以在这里显示的类名跟我们定义的有一些不一样,如类XX的类名,被改写成了2XX。)
那么问题来了,typeid是如何计算这个类型信息的呢?下面将重点说明这个问题。
多态类型是通过在类中声明一个或多个virtual函数来区分的。因为在C++中,一个具备多态性质的类,正是内含直接声明或继承而来的virtual函数。在C++对象模型之详述C++对象的内存布局一文中,已经详细地探讨了C++对象的内存布局,并说明了多态类的对象的类型信息保存在虚函数表的索引的-1的项中,该项是一个type_info对象的地址,该type_info对象保存着该对象对应的类型信息,每个类都对应着一个type_info对象。下面就对这一说法进行验证。
使用如以的代码,对上述的类X和类XX的对象的内存布局进行测试:
typedef void (*FuncPtr)();
int main()
{
XX xx;
FuncPtr func;
char *p = (char*)&xx;
// 获得虚函数表的地址
int **vtbl = (int**)*(int**)p;
// 输出虚函数表的地址,即vptr的值
cout << vtbl << endl;
// 获得type_info对象的指针,并调用其name成员函数
cout << "\t[-1]: " << (vtbl[-1]) << " -> "
<< ((type_info*)(vtbl[-1]))->name() << endl;
// 调用第一个virtual函数
cout << "\t[0]: " << vtbl[0] << " -> ";
func = (FuncPtr)vtbl[0];
func();
// 输出基类的成员变量的值
p += sizeof(int**);
cout << *(int*)p << endl;
// 输出派生类的成员变量的值
p += sizeof(int);
cout << *(int*)p << endl;
return 0;
}
测试代码,对类XX的对象的内存布局进行测试,其输出结果如下:
从运行结果可以看到,利用虚函数表的-1的项的地址转换成一个type_info的指针类型,并调用name成员函数的输出为2XX,其输出与前面的测试代码中利用typeid的输出一致。从而可以知道,关于多态类型的计算是通过基类指针或引用指向的对象(子对象)的虚函数表获得的。
从运行的结果可以知道,类XX的对象的内存布局如下:
对于以下的代码片断:
typeid(*px).name()
可能被转换成如下的C++伪代码,用于计算实际对象的类型:
(*(type_info*)px->vptr[-1]).name();
在多重继承和虚拟继承的情况下,一个类有n(n>1)个虚函数表,该类的对象也有n个vptr,分别指向这些虚函数表,但是一个类的所有的虚函数表的索引为-1的项的值(type_info对象的地址)都是相等的,即它们都指向同一个type_info对象,这样就实现了无论使用了哪一个基类的指针或引用指向其派生类的对象,都能通过相应的虚函数表获取到相同的type_info对象,从而得到相同的类型信息。
3)typeid的识别错误的情况
从第2)节可以看到,typeid对于多态类型是通过虚函数表来计算的,若一个基类的指针指向了一个派生类,而该派生类并不存在virtual函数会出现什么情况呢?
例如,把第2)节中的X和XX类中的virtual函数全部去掉,改成以下的代码:
class X
{
public:
X()
{
mX = 101;
}
private:
int mX;
};
class XX : public X
{
public:
XX():
X()
{
mXX = 1001;
}
private:
int mXX;
};
测试代码不变,如下:
void printTypeInfo(const X *px)
{
cout << "typeid(px) -> " << typeid(px).name() << endl;
cout << "typeid(*px) -> " << typeid(*px).name() << endl;
}
int main()
{
X x;
XX xx;
printTypeInfo(&x);
printTypeInfo(&xx); // 注释1
return 0;
}
其输出如下:
从输出的结果可以看到,对于注释1的函数调用,虽然函数中基类(X)的指针px指向一个派生类对象(XX类的对象xx),但是typeid却并不没有像第2)节那样能正确地通过指针px计算出其所指对象的实际类型。
其原因在于类XX和类X都没有一个virtual函数,所以类XX和类X并不表现出多态类的性质。所以对类的指针的解引用符合第1)节中所说的静态类型,所以其类型信息是在编译时就已经确定的,并不需要在程序运行的过程中运行计算,所以其输出的类型均为1X而没有输出1XX。更进一步说,是因为类X和类XX都不存在virtual函数,所以类X和XX都不存在虚函数表,所以也就没有空间存储跟类X和XX类型有关的type_info对象的地址。
然而在C++中即使一个类不具有多态的性质,仍然允许把一个派生类的指针赋值给一个基类的指针,所以这个错误比较隐晦。
2、dynamic_cast运算符
把一个基类类型的指针或引用转换至继承架构的末端某一个派生类类型的指针或引用被称为向下转型(downcast)。dynamic_cast运算符的作用是安全而有效地进行向下转型。
把一个派生类的指针或引用转换成其基类的指针或引用总是安全的,因为通过分析对象的内存布局可以知道,派生类的对象中必然存在基类的子对象,所以通过基类的指针或引用对派生类对象进行的所有基类的操作都是合法和安全的。而向下转型有潜在的危险性,因为基类的指针可以指向基类对象或其任何派生类的对象,而该对象并不一定是向下转型的类型的对象。所以向下转型遏制了类型系统的作用,转换后对指针或引用的使用可能会引发错误的解释或腐蚀程序内存等错误。
例如对于以下的类定义:
class X
{
public:
X()
{
mX = 101;
}
virtual ~X()
{
}
private:
int mX;
};
class XX : public X
{
public:
XX():
X()
{
mXX = 1001;
}
virtual ~XX()
{
}
private:
int mXX;
};
class YX : public X
{
public:
YX()
{
mYX = 1002;
}
virtual ~YX()
{
}
private:
int mYX;
};
使用如下的测试代码,其中的类型转换均为向下转型:
int main()
{
X x;
XX xx;
YX yx;
X *px = &xx;
cout << px << endl;
XX *pxx = dynamic_cast<XX*>(px); // 转换1
cout << pxx << endl;
YX *pyx = dynamic_cast<YX*>(px); // 转换2
cout << pyx << endl;
pyx = (YX*)px; // 转换3
cout << pyx << endl;
pyx = static_cast<YX*>(px); // 转换4
cout << pyx << endl;
return 0;
}
其运行结果如下:
运行结果分析
px是一个基类(X)的指针,但是它指向了派生类XX的一个对象。在转换1中,转换成功,因为px指向的对象确实为XX的对象。在转换2中,转换失败,因为px指向的对象并不是一个YX对象,此时dymanic_cast返回NULL。转换3为C风格的类型转换而转换4使用的是C++中的静态类型转换,它们均能成功转换,但是这个对象实际上并不是一个YX的对象,所以在转换3和转换4中,若继续通过指针使用该对象必然会导致错误,所以这个转换是不安全的。
从上述的结果可以看出在向下转型中,只有dynamic_case才能实现安全的向下转型。那么dynamic_case是如何实现的呢?有了上面typeid和虚函数表的知识后,这个问题并不难解释了,以转换1为例。
1)计算指针或引用变量所指的对象的虚函数表的type_info信息,如下:
*(type_info*)px->vptr[-1]
2)静态推导向下转型的目标类型的type_info信息,即获取类XX的type_info信息
3)比较1)和2)中获取到的type_info信息,若2)中的类型信息与1)中的类型信息相等或是其基类类型,则返回相应的对象或子对象的地址,否则返回NULL。
引用的情况与指针稍有不同,失败时并不是返回NULL,而是抛出一个bad_cast异常,因为引用不能参考NULL。
---------------------
原文:https://blog.csdn.net/ljianhui/article/details/46487951