将类型名作为强制类型转换运算符的做法是C语言的老式做法,C++语言为保持兼容而予以保留。
C++ 引入了四种功能不同的强制类型转换运算符以进行强制类型转换:static_cast、reinterpret_cast、const_cast 和 dynamic_cast。
强制类型转换是有一定风险的,有的转换并不一定安全,如把整型数值转换成指针,把基类指针转换成派生类指针,把一种函数指针转换成另一种函数指针,把常量指针转换成非常量指针等。C++ 引入新的强制类型转换机制,主要是为了克服C语言强制类型转换的以下三个缺点。
- 没有从形式上体现转换功能和风险的不同。
例如,将 int 强制转换成 double 是没有风险的,而将常量指针转换成非常量指针,将基类指针转换成派生类指针都是高风险的,而且后两者带来的风险不同(即可能引发不同种类的错误),C语言的强制类型转换形式对这些不同并不加以区分。
- 将多态基类指针转换成派生类指针时不检查安全性,即无法判断转换后的指针是否确实指向一个派生类对象。
- 难以在程序中寻找到底什么地方进行了强制类型转换。
强制类型转换是引发程序运行时错误的一个原因,因此在程序出错时,可能就会想到是不是有哪些强制类型转换出了问题。如果采用C语言的老式做法,要在程序中找出所有进行了强制类型转换的地方,显然是很麻烦的,因为这些转换没有统一的格式。而用 C++ 的方式,则只需要查找_cast
字符串就可以了。甚至可以根据错误的类型,有针对性地专门查找某一种强制类型转换。例如,怀疑一个错误可能是由于使用了 reinterpret_cast 导致的,就可以只查找reinterpret_cast
字符串。
C++ 强制类型转换运算符的用法如下:
1 强制类型转换运算符 <要转换到的类型> (待转换的表达式) 2 例如: 3 double d = static_cast <double> (3*5); //将 3*5 的值转换成实数
下面分别介绍四种强制类型转换运算符。
1) static_cast
在C++语言中static_cast用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。例如将整型数据转换为浮点型数据。
[例1]C语言所采用的类型转换方式:
int a = 10; int b = 3; double result = (double)a / (double)b;
例1中将整型变量a和b转换为双精度浮点型,然后相除。在C++语言中,我们可以采用static_cast关键字来进行强制类型转换,如下所示。
[例2]static_cast关键字的使用:
int a = 10; int b = 3; double result = static_cast<double>(a) / static_cast<double>(b);
在本例中同样是将整型变量a转换为双精度浮点型。采用static_cast进行强制数据类型转换时,将想要转换成的数据类型放到尖括号中,将待转换的变量或表达式放在元括号中,其格式可以概括为如下形式:
用法:static_cast <类型说明符> (变量或表达式)
它主要有如下几种用法:
(1)用于类层次结构中基类和派生类之间指针或引用的转换
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的
(2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证
(3)把空指针转换成目标类型的空指针
(4)把任何类型的表达式转换为void类型
注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。
static_cast:可以实现C++中内置基本数据类型之间的相互转换。
如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。static_cast 用法示例如下:
1 #include <iostream> 2 using namespace std; 3 class A 4 { 5 public: 6 operator int() { return 1; } 7 operator char*() { return NULL; } 8 }; 9 int main() 10 { 11 A a; 12 int n; 13 char* p = "New Dragon Inn"; 14 n = static_cast <int> (3.14); // n 的值变为 3 15 n = static_cast <int> (a); // 调用 a.operator int,n 的值变为 1 16 p = static_cast <char*> (a); // 调用 a.operator char*,p 的值变为 NULL 17 n = static_cast <int> (p); // 编译错误,static_cast不能将指针转换成整型 18 p = static_cast <char*> (n); // 编译错误,static_cast 不能将整型转换成指针 19 return 0; 20 }
2) const_cast
在C语言中,const限定符通常被用来限定变量,用于表示该变量的值不能被修改。
而const_cast则正是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
用法:const_cast<type_id> (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
[例3]一个错误的例子:
const int a = 10; const int * p = &a; *p = 20; //compile error int b = const_cast<int>(a); //compile error
在本例中出现了两个编译错误,第一个编译错误是*p因为具有常量性,其值是不能被修改的;另一处错误是const_cast强制转换对象必须为指针或引用,而例3中为一个变量,这是不允许的!
[例4]const_cast关键字的使用
#include<iostream> using namespace std; int main() { const int a = 10; const int * p = &a; int *q; q = const_cast<int *>(p); *q = 20; //fine cout <<a<<" "<<*p<<" "<<*q<<endl; cout <<&a<<" "<<p<<" "<<q<<endl; return 0; }
在本例中,我们将变量a声明为常量变量,同时声明了一个const指针指向该变量(此时如果声明一个普通指针指向该常量变量的话是不允许的,Visual Studio 2010编译器会报错)。
之后我们定义了一个普通的指针*q。将p指针通过const_cast去掉其常量性,并赋给q指针。之后我再修改q指针所指地址的值时,这是不会有问题的。
最后将结果打印出来,运行结果如下:
10 20 20
002CFAF4 002CFAF4 002CFAF4
【解释上述为何打印结果比较奇怪】
“因为编译器对常量优化了,在输出时直接用值替换了,你不妨在const前面加个volatile试试
让编译器不要优化这个变量再看看结果”。
即可以使用反汇编查看寄存器的变化。
vs2010 在debug模式下:调试->窗口->反汇编
查看运行结果,问题来了,指针p和指针q都是指向a变量的,指向地址相同,而且经过调试发现002CFAF4地址内的值确实由10被修改成了20,这是怎么一回事呢?为什么a的值打印出来还是10呢?
其实这是一件好事,我们要庆幸a变量最终的值没有变成20!变量a一开始就被声明为一个常量变量,不管后面的程序怎么处理,它就是一个常量,就是不会变化的。试想一下如果这个变量a最终变成了20会有什么后果呢?对于这些简短的程序而言,如果最后a变成了20,我们会一眼看出是q指针修改了,但是一旦一个项目工程非常庞大的时候,在程序某个地方出现了一个q这样的指针,它可以修改常量a,这是一件很可怕的事情的,可以说是一个程序的漏洞,毕竟将变量a声明为常量就是不希望修改它,如果后面能修改,这就太恐怖了。
在例4中我们称“*q=20”语句为未定义行为语句,所谓的未定义行为是指在标准的C++规范中并没有明确规定这种语句的具体行为,该语句的具体行为由编译器来自行决定如何处理。对于这种未定义行为的语句我们应该尽量予以避免!
从例4中我们可以看出我们是不想修改变量a的值的,既然如此,定义一个const_cast关键字强制去掉指针的常量性到底有什么用呢?我们接着来看下面的例子。
【举例说明const_cast使用的真正场景】
1 #include<iostream> 2 using namespace std; 3 int main(int argc, char const *argv[]) 4 { 5 const int a = 12; 6 const int *ap = &a; 7 int* tmp = const_cast<int*>(ap); 8 *tmp = 11; 9 cout<<a; 10 return 0; 11 }
const_cast的目的并不是为了让你去修改一个本身被定义为const的值,因为这样做的后果是无法预期的。const_cast的目的是修改一些指针/引用的权限,如果我们原本无法通过这些指针/引用修改某块内存的值,现在你可以了。
3) dynamic_cast
对引用进行dynamic_cast,失败抛出一个异常bad_cast,成功返回正常cast后的对象引用。
前言
RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。RTTI并不是什么新的东西,很早就有了这个技术,但是,在实际应用中使用的比较少而已。而我这里就是对RTTI进行总结,今天我没有用到,并不代表这个东西没用。学无止境,先从typeid函数开始讲起。
typeid函数
typeid的主要作用就是让用户知道当前的变量是什么类型的,比如以下代码:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 int main() 6 { 7 short s = 2; 8 unsigned ui = 10; 9 int i = 10; 10 char ch = 'a'; 11 wchar_t wch = L'b'; 12 float f = 1.0f; 13 double d = 2; 14 15 cout<<typeid(s).name()<<endl; // short 16 cout<<typeid(ui).name()<<endl; // unsigned int 17 cout<<typeid(i).name()<<endl; // int 18 cout<<typeid(ch).name()<<endl; // char 19 cout<<typeid(wch).name()<<endl; // wchar_t 20 cout<<typeid(f).name()<<endl; // float 21 cout<<typeid(d).name()<<endl; // double 22 23 return 0; 24 }
代码如下:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B : public A 12 { 13 public: 14 void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 struct C 18 { 19 void Print() { cout<<"This is struct C."<<endl; } 20 }; 21 22 int main() 23 { 24 A *pA1 = new A(); 25 A a2; 26 27 cout<<typeid(pA1).name()<<endl; // class A * 28 cout<<typeid(a2).name()<<endl; // class A 29 30 B *pB1 = new B(); 31 cout<<typeid(pB1).name()<<endl; // class B * 32 33 C *pC1 = new C(); 34 C c2; 35 36 cout<<typeid(pC1).name()<<endl; // struct C * 37 cout<<typeid(c2).name()<<endl; // struct C 38 39 return 0; 40 }
是的,对于我们自定义的结构体和类,tpyeid都能支持。在上面的代码中,在调用完typeid之后,都会接着调用name()函数,可以看出typeid函数返回的是一个结构体或者类,然后,再调用这个返回的结构体或类的name成员函数;其实,typeid是一个返回类型为type_info类型的函数。那么,我们就有必要对这个type_info类进行总结一下,毕竟它实际上存放着类型信息。
type_info类
去掉那些该死的宏,在Visual Studio 2012中查看type_info类的定义如下:
在type_info类中,复制构造函数和赋值运算符都是私有的,同时也没有默认的构造函数;所以,我们没有办法创建type_info类的变量,例如type_info A;这样是错误的。那么typeid函数是如何返回一个type_info类的对象的引用的呢?我在这里不进行讨论,思路就是类的友元函数。
typeid函数的使用
typeid使用起来是非常简单的,常用的方式有以下两种:
1.使用type_info类中的name()函数返回对象的类型名称
就像上面的代码中使用的那样;但是,这里有一点需要注意,比如有以下代码:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B : public A 12 { 13 public: 14 void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 int main() 18 { 19 A *pA = new B(); 20 cout<<typeid(pA).name()<<endl; // class A * 21 cout<<typeid(*pA).name()<<endl; // class A 22 return 0; 23 }
我使用了两次typeid,但是两次的参数是不一样的;输出结果也是不一样的;当我指定为pA时,由于pA是一个A类型的指针,所以输出就为class A *;当我指定*pA时,它表示的是pA所指向的对象的类型,所以输出的是class A;所以需要区分typeid(*pA)和typeid(pA)的区别,它们两个不是同一个东西;但是,这里又有问题了,明明pA实际指向的是B,为什么得到的却是class A呢?我们在看下一段代码:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 virtual void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B : public A 12 { 13 public: 14 void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 int main() 18 { 19 A *pA = new B(); 20 cout<<typeid(pA).name()<<endl; // class A * 21 cout<<typeid(*pA).name()<<endl; // class B 22 return 0; 23 }
好了,我将Print函数变成了虚函数,输出结果就不一样了,这说明什么?这就是RTTI在捣鬼了,当类中不存在虚函数时,typeid是编译时期的事情,也就是静态类型,就如上面的cout<<typeid(*pA).name()<<endl;输出class A一样;当类中存在虚函数时,typeid是运行时期的事情,也就是动态类型,就如上面的cout<<typeid(*pA).name()<<endl;输出class B一样,关于这一点,我们在实际编程中,经常会出错,一定要谨记。
2.使用type_info类中重载的==和!=比较两个对象的类型是否相等
这个会经常用到,通常用于比较两个带有虚函数的类的对象是否相等,例如以下代码:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 virtual void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B : public A 12 { 13 public: 14 void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 class C : public A 18 { 19 public: 20 void Print() { cout<<"This is class C."<<endl; } 21 }; 22 23 void Handle(A *a) 24 { 25 if (typeid(*a) == typeid(A)) 26 { 27 cout<<"I am a A truly."<<endl; 28 } 29 else if (typeid(*a) == typeid(B)) 30 { 31 cout<<"I am a B truly."<<endl; 32 } 33 else if (typeid(*a) == typeid(C)) 34 { 35 cout<<"I am a C truly."<<endl; 36 } 37 else 38 { 39 cout<<"I am alone."<<endl; 40 } 41 } 42 43 int main() 44 { 45 A *pA = new B(); 46 Handle(pA); 47 delete pA; 48 pA = new C(); 49 Handle(pA); 50 return 0; 51 }
这是一种用法,呆会我再总结如何使用dynamic_cast来实现同样的功能。
dynamic_cast的内幕
在这篇《static_cast、dynamic_cast、const_cast和reinterpret_cast总结》的文章中,也介绍了dynamic_cast的使用,对于dynamic_cast到底是如何实现的,并没有进行说明,而这里就要对于dynamic_cast的内幕一探究竟。首先来看一段代码:
代码如下:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 virtual void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B 12 { 13 public: 14 virtual void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 class C : public A, public B 18 { 19 public: 20 void Print() { cout<<"This is class C."<<endl; } 21 }; 22 23 int main() 24 { 25 A *pA = new C; 26 //C *pC = pA; // Wrong 27 C *pC = dynamic_cast<C *>(pA); 28 if (pC != NULL) 29 { 30 pC->Print(); 31 } 32 delete pA; 33 }
在上面代码中,如果我们直接将pA赋值给pC,这样编译器就会提示错误,而当我们加上了dynamic_cast之后,一切就ok了。那么dynamic_cast在后面干了什么呢?
dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。我在《COM编程——接口的背后》这篇博文中总结的那样,当类中存在虚函数时,编译器就会在类的成员变量中添加一个指向虚函数表的vptr指针,每一个class所关联的type_info object也经由virtual table被指出来,通常这个type_info object放在表格的第一个slot。当我们进行dynamic_cast时,编译器会帮我们进行语法检查。如果指针的静态类型和目标类型相同,那么就什么事情都不做;否则,首先对指针进行调整,使得它指向vftable,并将其和调整之后的指针、调整的偏移量、静态类型以及目标类型传递给内部函数。其中最后一个参数指明转换的是指针还是引用。两者唯一的区别是,如果转换失败,前者返回NULL,后者抛出bad_cast异常。对于在typeid函数的使用中所示例的程序,我使用dynamic_cast进行更改,代码如下:
代码如下:
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class A 6 { 7 public: 8 virtual void Print() { cout<<"This is class A."<<endl; } 9 }; 10 11 class B : public A 12 { 13 public: 14 void Print() { cout<<"This is class B."<<endl; } 15 }; 16 17 class C : public A 18 { 19 public: 20 void Print() { cout<<"This is class C."<<endl; } 21 }; 22 23 void Handle(A *a) 24 { 25 if (dynamic_cast<B*>(a)) 26 { 27 cout<<"I am a B truly."<<endl; 28 } 29 else if (dynamic_cast<C*>(a)) 30 { 31 cout<<"I am a C truly."<<endl; 32 } 33 else 34 { 35 cout<<"I am alone."<<endl; 36 } 37 } 38 39 int main() 40 { 41 A *pA = new B(); 42 Handle(pA); 43 delete pA; 44 pA = new C(); 45 Handle(pA); 46 return 0; 47 }
这个是使用dynamic_cast进行改写的版本。实际项目中,这种方法会使用的更多点。
4) reinterpret_cast
在C++语言中,reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。
用法:reinterpret_cast<type_id> (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,因此在使用过程中需要特别谨慎!
例7:
int *a = new int; double *d = reinterpret_cast<double *>(a);
去const属性用const_cast。
基本类型转换用static_cast。
多态类之间的类型转换用daynamic_cast。
不同类型的指针类型转换用reinterpreter_cast。