【C++专题】static_cast, dynamic_cast, const_cast探讨
首先回顾一下C++类型转换:
C++类型转换分为:隐式类型转换和显式类型转换。
第1部分. 相关概念解释
上行转换(up-casting):把子类的指针或引用转换成基类表示。
下行转换(down-casting):把基类指针或引用转换成子类表示。
类型转换不安全性来源于两个方面:
其一是类型的窄化转化,会导致数据位数的丢失;比如int类型转short。float类型转int。
其二是在类继承链中,将父类对象的地址(指针)强制转化成子类的地址(指针)。
因此上行转换一般是安全的,下行转换很可能是不安全的。子类中包含父类,所以上行转换(只能调用父类的方法,引用父类的成员变量)一般是安全的。但父类中却没有子类的任何信息,而下行转换会调用到子类的方法、引用子类的成员变量,这些父类都没有,所以很容易“指鹿为马”或者干脆指向不存在的内存空间。
值得一说的是,不安全的转换不一定会导致程序出错,比如一些窄化转换在很多场合都会被频繁地使用,前提是程序员足够小心以防止数据溢出;下行转换关键看其“本质”是什么,比如一个父类指针指向子类,再将这个父类指针转成子类指针,这种下行转换就不会有问题。
第2部分. 隐式类型转换
这种转换是不需要显式的强制转换的,这部分很简单。
但是注意一点:void指针赋值给其他指定类型指针时,不存在隐式转换,编译出错。
第3部分. 显式类型转换
被称为“强制类型转换”(cast)
C 风格: (type-id)
C++风格: static_cast、dynamic_cast、reinterpret_cast、和const_cast。
下面分别对这四种显式转换说明一下:
(1) static_cast
用法:static_cast < type-id > ( expression )
说明:该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
对于类只能在有联系的指针类型间进行转换。可以在继承体系中把指针转换来、转换去,但是不能转换成继承体系外的一种类型。
它主要有如下几种用法:
- 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
- 把void指针转换成目标类型的指针(不安全!!)
- 把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
转换安全性示例:
class A { ... }; class B { ... }; class D : public B { ... }; void f(B* pb, D* pd) { D* pd2 = static_cast<D*>(pb); // 不安全, pb可能只是B的指针,但这个仅仅是个不安全,代码还是可以编译运行的 B* pb2 = static_cast<B*>(pd); // 安全的 A* pa2 = static_cast<A*>(pb); // 错误A与B没有继承关系 ... }
对于不相关类指针之间的转换。参见下面的例子:
// class type-casting #include <iostream> using namespace std; class CDummy { float i,j; }; class CAddition { int x,y; public: CAddition (int a, int b) { x=a; y=b; } int result() { return x+y;} }; int main () { CDummy d; CAddition * padd; padd = (CAddition*) &d; cout << padd->result(); return 0; }
CAddition与CDummy类没有任何关系了,但main()中C风格的转换仍是允许的padd = (CAddition*) &d,这样的转换没有安全性可言。如果在main()中使用static_cast,像这样:
int main () { CDummy d; CAddition * padd; padd = static_cast<CAddition*> (&d); cout << padd->result(); return 0; }
编译器就能看到这种不相关类指针转换的不安全,报出如下图所示的错误:
注意这时不是以warning形式给出的,而直接是不可通过编译的error。从提示信息里可以看到,编译器说如果需要这种强制转换,要使用reinterpret_cast(稍候会说)或者C风格的两种转换。
总结:static_cast最接近于C风格转换了,但在无关类的类指针之间转换上,有安全性的提升。
用法:dynamic_cast < type-id > ( expression )
说 明:该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个 引用。它有三个重要的约束条件,
第一、必须用于类与子类之间的转换;
第二、必须用于指针或引用类型的转换;
第三、下行转换时要求基类必须有虚函数(基类中包含至少一个虚函数)。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
class Base { public: int m_iNum; virtual void foo(); }; class Derived:public Base { public: char *m_szName[100]; }; void func(Base *pb) { Derived *pd1 = static_cast<Derived *>(pb); Derived *pd2 = dynamic_cast<Derived *>(pb); }
在上面的代码段中,如果pb实际指向一个Derived类型的对象,pd1和pd2是一样的,并且对这两个指针执行Derived类型的任何操作都是安全的;
如果pb实际指向的是一个Base类型的对象,那么pd1将是一个指向该对象的指针,对它进行Derived类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针(即0,因为dynamic_cast失败)。
另外要注意:Base要有虚函数,否则会编译出错;static_cast则没有这个限制。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类 的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
为了让dynamic_cast能正常工作,必须让编译器支持运行期类型信息(RTTI),所以基类要有虚函数才可以:
#include <iostream> using namespace std; class CBase { }; class CDerived: public CBase { }; int main() { CBase b; CBase* pb; CDerived d; CDerived* pd; pb = dynamic_cast<CBase*>(&d); // ok: derived-to-base pd = dynamic_cast<CDerived*>(&b); // wrong: base-to-derived }
在最后一行代码有问题,编译器给的错误提示如下图所示
把类的定义改成:
class CBase { virtual void dummy() {} }; class CDerived: public CBase {};
再编译,结果如下图所示:
编译都可以顺利通过了。这里我们在main函数的最后添加两句话:
cout << pb << endl; cout << pd << endl;
输出pb和pd的指针值,结果如下:
我们看到一个奇怪的现象,将父类经过dynamic_cast转成子类的指针竟然是空指针!这正是dynamic_cast提升安全性的功能,dynamic_cast可以识别出不安全的下行转换,但并不抛出异常,而是将转换的结果设置成null(空指针)。
再举一个例子:
#include <iostream> #include <exception> using namespace std; class CBase { virtual void dummy() {} }; class CDerived: public CBase { int a; }; int main () { try { CBase * pba = new CDerived; CBase * pbb = new CBase; CDerived * pd; pd = dynamic_cast<CDerived*>(pba); if (pd==0) cout << "Null pointer on first type-cast" << endl; pd = dynamic_cast<CDerived*>(pbb); if (pd==0) cout << "Null pointer on second type-cast" << endl; } catch (exception& e) { cout << "Exception: " << e.what(); } return 0; }
输出结果是:Null pointer on second type-cast
两个dynamic_cast都是下行转换,第一个转换是安全的,因为指向对象的本质是子类,转换的结果使子类指针指向子类,天经地义;第二个转换是不安全的,因为指向对象的本质是父类,“指鹿为马”或指向不存在的空间很可能发生!
另外,dynamic_cast还支持交叉转换(cross cast)。如下代码所示。
class Base { public: int m_iNum; virtual void f(){} }; class Derived1 : public Base { }; class Derived2 : public Base { }; void foo() { derived1 *pd1 = new Drived1; pd1->m_iNum = 100; Derived2 *pd2 = static_cast<Derived2 *>(pd1); //compile error Derived2 *pd2 = dynamic_cast<Derived2 *>(pd1); //pd2 is NULL delete pd1; }
在函数foo中,使用static_cast进行转换是不被允许的,将在编译时出错;而使用dynamic_cast的转换则是允许的,结果是空指针。
(3) reinpreter_cast
用法:reinpreter_cast<type-id> (expression)
说明:type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
(4) const_cast
用法:const_cast<type_id> (expression)
说明:该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
class B { public: int m_iNum; } void foo() { const B b1; b1.m_iNum = 100; //comile error B b2 = const_cast<B>(b1); b2. m_iNum = 200; //fine }
上面的代码编译时会报错,因为b1是一个常量对象,不能对它进行改变;使用const_cast把它转换成一个常量对象,就可以对它的数据成员任意改变。注意:b1和b2是两个不同的对象。
最后的总结:
1.哪些强制的显示转换时非法的?
不想关的两个对象显示的转换时不合法的。
派生类和基类之间的下行转换是不合法的,这里说的下行转换指的是把一个真正的基类对象或者指向基类对象的指针转换成派生类对象或者派生类对象的指针。
上面的两种情况都是不合法的,上面说的不合法的意思是这种转换是不合理的,转换完了之后得到的对象是不能用的。
下面还有一个概念是不安全,所谓的安全是能够转换,并且转换完了之后能够正确的使用;或者转换完了不能够使用(上面的两种情况),但是编译器会报错,不让我对他们进行强制转换。不安全的意思就是明明转换完了不能够合理的使用,你还给我转换了让我用,这就是不安全。
2.各种显示的转换是怎么解决这些问题的?
C语言的强制类型转换,是能够转换各种的指针的,这里是很不安全的,这里的不安全的意思是能够强制转换,但是转换完了之后不能用。C语言的转换是最暴力的,最不安全的。
static_cast:是不相关的两个对象之间的转换变得安全,这里的安全的意思是他不会让你显示转换,编译会报错,之所以不让你强制转换,是因为转了之后也不能用。但是下行转换它还是暴力的转换了,很可能转换得到一个不能使用的结果。这就是不安全的。
举个例子,把基类指针显式的转换为派生类指针,如果基类指针指向了派生了,这时还可以。但是如果基类指针真的就指向了一个基类,那么这种转换仍然会进行,但是得到的结果却不能使用。
dynamic_cast:使下行转换也变得安全,如果不小心把指向基类的基类指针转换成了派生类指针,这时它会告诉你转换失败,返回一个NULL指针回来,这样的做法就是安全的。因为得不到好的结果的转换它不会去做,提示转换失败。
注意:dynamic_cast必须提供运行时信息,也就是说基类中必须有虚函数。
对于来自同一个基类的两个派生类,这两个派生类显式转换就是交叉转换,这种转换在static_cast中属于不想关的两个类,会有compile错误。在dynamic_cast中会转换失败,返回NULL,所以都是安全的。
一道例题
class ClassA { public: virtual ~ ClassA() { } virtual void FunctionA() { } }; class ClassB { public: virtual void FunctionB() { } }; class ClassC: public ClassA, public ClassB { public: }; ClassC aObject; ClassA *pA = &aObject; ClassB *pB = &aObject; ClassC *pC = &aObject;
假设定义了ClassA* pA2,下面正确的代码是: pA2=static_cast<ClassA*>(pB); void* pVoid=static_cast<void*>(pB); pA2=static_cast<ClassA*>(pVoid); pA2=pB; pA2=static_cast<ClassA*>(static_cast<ClassC*>(pB));//正确答案BD
分析:A是不想关的两个类转换,编译器会报错,因为static_cast是这种转换变得安全了。B直接跟C语言的暴力转换一个,强制转换成void在暴力强制转化成想要的东西。程序是不会报错的,但是最后转换的结果不能用。C这种隐式的存在是不存在的。D先用static_cast下行转换,虽然不安全,但是pB确实是指向ClassC的指针,是可以转换的,不会报错。然后用static_cast上行转换。