程序开发环境:VS2012+Win32+Debug
数据类型在编程中经常遇到,虽然可能存在风险,但我们却乐此不疲的进行数据类型的转换。
1. 隐式数据类型转换
数据类型转换,到底做了些什么事情呢?实际上,数据类型转换的工作相当于一条函数调用,若有一个函数撰文负责从double转换到int(假设函数是dtoi),则下面的转换语句:
double d=4.48;
int i=d; //报告警告
等价于i=dtoi(d)。函数dtoi的原型应该是:int dtoi(double)或者是int dtoi(const double&)。有些类型的数据转换时绝对安全的,所以可以自动进行,编译器不会给出任何警告,如由int型转换成double型。另一些转换会丢失数据,编译器只会给出警告,并不算一个语法错误,如上面的例子。各种基本数据类型(不包括void)之间的转换都属于以上两种情况。
以上两种不显示指明数据类型的转换就是隐式数据类型转换。隐式数据类型转换无处不在,主要出现在以下几种情况。
(1)算术运算式中,低类型能够转换为高类型。
(2)赋值表达式中,右边表达式的值自动隐式转换为左边变量的类型,并赋值给他。
(3)函数调用中参数传递时,系统隐式地将实参转换为形参的类型后,赋给形参。
(4)函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数。
编程原则:请尽量不要使用隐式类型转换,即使是隐式的数据类型转换是安全的,因为隐式类型数据转换降低了程序的可读性。
2. 显示数据类型转换
显示数据类型转换是显示指明需要转换的类型,首先考察如下程序。
#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
short arr[]={65,66,67,0};
wchar_t *s;
s=arr;
wcout<<s<<endl;
getchar();
}
由于short int和wchar_t是不同的数据类型,直接把arr代表的地址赋给s会导致一个编译错误:error C2440:“=”:无法从“short[4]”转换为“wchar_t”。
为了解决这种“跨度较大”的数据类型转换,可以说还用显示的“强制类型转换”机制,把语句s=arr;改为s=(wchar_t*)arr;就能顺利通过编译,并输出:ABC。
强制类型转换在C语言中就已经存在,到了C++语言中可以继续使用。在C风格的强制类型转换中,目标数据类型被放在一堆圆括号中,然后置于源数据类型的表达式前。在C++语言中,允许将目标数据类型当做一个函数来使用,将源数据类型表达式置于一对圆括号中,这就是所谓的“函数风格”的强制类型转换。以上两种强制转换没有本质区别,只是书写形式上略有不同。即:
(T)expression //C-style cast
T(expression) //function-style cast
可将它们称为旧风格的强制类型转换。在上面的程序中,可以用以下两种书写形式实现强制类型转换:
s=(wchar_t*)arr;
typedef wchar_t* LSPTR; s= LSPTR(arr);
3.C++中新式类型转换
在C++语言中,增加了四中内置的类型转换操作符:const_cast、static_cast、dynamic_cast和reinterpret_cast。它们具有统一的语言形式:type_cast_operator(expresiion)。下面分别介绍。
3.1 const_cast
const_cast主要用于解除常指针和常量的const和volatile属性。也就是说,把cosnt type*转换成type*类型或将const type&转换成type&类型,但是要注意,一个变量本身被定义为只读变量,那么它永远是常变量。const_cast取消的是对间接引用时的改写限制(即只针对指针或者引用),而不能改变变量本身的const属性。如下面的语句就是错误的。
const int i;
int j=const_cast<int>(i);
正确示例如下:
void constTest{
const int a=5;
int *p=NULL;
p=const_cast<int*>(&a);
(*p)++;
cout<<a<<endl;//输出6
}
程序正常运行,输出6。
3.2 static_cast
static_cast相当于传统的C语言中那些“较为合理”的强制类型转换,较多的使用于基本数据类型之间的转换、基类对象指针(或引用)和派生类对象指针(或引用)之间的转换、一般的指针和void*类型的指针之间的转换等。static_cast操作对于类型转换的合理性会作出检查,对于一些过于“无理”的转换会加以拒绝。例如下面的转换:
double d=3.14;
double* p=static_cast<double*>(d);
这是一种非常诡异的转换,在编译时会遭到拒绝。
另外,对于一些看似合理的转换,也可能被static_cast拒绝。这时要考虑别的方法。如下面的程序。
#include <iostream>
using namespace std;
class A{
char ch;
int n;
public:
A(char c,int i):ch(c),n(i){}
};
int main(int argc,char* argv[])
{
A a(‘s‘,2);
char* p=static_cast<char*>(&a);
cout<<*p;
getchar();
}
这个程序无法通过编译,就是说,直接将A*类型转换为char*是不允许的,这时可以通过void*类型作为中介实现转换。修改后的程序如下。
void* q=&a;
char* p=static_cast<char*>(&q);
这样,程序就可以通过编译,输出s。可见,如果指针类型之间进行转换,一定要注意转换的合理性,这一点必须由程序员自己负责。指针类型的转换以为这对原数据实体的内容的重新解释。
虽然const_cast是用来去除变量的const限定,但是static_cast却不是用来去除变量的static引用。其实这是很容易理解的,static决定的是一个变量的作用域和生命周期,比如:在一个文件中将变量定义为static,则说明这个变量只能在本Package中使用;在方法中定义一个static变量,该变量在程序开始存在直到程序结束;类中定义一个static成员,该成员随类的第一个对象出现时出现,并且可以被该类的所有对象所使用。
对static限定的改变必然会造成范围性的影响,而const限定的只是变量或对象自身。但无论是哪一个限定,它们都是在变量一出生(完成编译的时候)就决定了变量的特性,所以实际上都是不容许改变的。这点在const_cast那部分就已经有体现出来。
在实践中,static_cast多用于类类型之间的转换。这时,被转换的两种类型之间一定存在派生与继承的关系。见如下程序。
#include <iostream>
using namespace std;
class A{};
class B{};
int main(int argc,char* argv[])
{
A* pa;
B* pb;
A a;
pa=&a;
pb=static_cast<B*>(pa);
getchar();
}
改程序无法通过编译,原因是类A与类B没有任何关系。综上所述,使用static_cast进行类型转换时要注意如下几点。
(1)static_cast操作符的语法形式是static_cast(expression),其中,expression外面的圆括号不能省略,哪怕expression是一个简单的变量。
(2)通过static_cast只能进行一些相关类型之间的“合理”转换。如果是类类型之间的转换,源类型和目标类型之间必须存在继承关系,否则会得到编译错误。
(3)static_cast所进行的是一种静态转换,是在编译时决定的。通过编译后,空间和时间效率实际上等价于C方式的强制类型转换。
(4)在C++中,只想派生类对象的指针可以隐式转换为指向基类对象的指针。而要把指向积累对象的指针转换为指向派生类对象的指针,就需要借助static_cast操作符来完成,其转换的风险是需要程序员自己来承担。当然使用dynamic_cast更为安全。
(5)static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
3.3 dynamic_cast
dynamic_cast是一个完全的动态操作符,只能用于指针或者引用间的转换。而且dynamic_cast运算符所操作的指针或引用的对象必须拥有虚函数成员,否则出现编译错误。
原因是dynamic_cast牵扯到的面向对象的多态性,其作用就是在程序运行的过程中动态的检查指着或者引用指向的实际对象是什么以确定转换是否安全,而C++的类的多态性则依赖于类的虚函数。
具体的说,dynamic_cast可以进行如下的类型转换。
(1)在指向基类的指针(引用)与指向派生类的指针(引用)之间进行的转换。基类指针(引用)转换为派生类指针(引用)为向下转换,被编译器视为安全的类型转换,也可以使用static_cast进行转换。派生类指针(引用)转换为基类指针(引用)时, 为向上转换,被编译器视为不安全的类型转换,需要dynamic_cast进行动态的类型检测。当然,static_cast也可以完成转换,只是存在不安全性。
(2)在多重继承的情况下,派生类的多个基类之间进行转换(称为交叉转换:crosscast)。如父类A1指针实际上指向的是子类,则可以将A1转换为子类的另一个父类A2指针。
3.3.1 dynamic_cast的向下转换
dynamic_cast在向下转换时(downcast),即将父类指针或者引用转换为子类指针或者引用时,会严格检查指针所指的对象的实际类型。参见如下程序。
#include <iostream>
using namespace std;
class A{
public:
int i;
virtual void show(){
cout<<"class A"<<endl;
}
A(){int i=1;}
};
class B:public A{
public:
int j;
void show(){
cout<<"class B"<<endl;
}
B(){j=2;}
};
class C:public B{
public:
int k;
void show(){
cout<<"class C"<<endl;
}
C(){k=3;}
};
int main(int argc,char* argv[])
{
A* pa=NULL;
B b,*pb;
C *pc;
pa=&b;
pb=dynamic_cast<B*>(pa);
if(pb){
pb->show();
cout<<pb->j<<endl;
}
else
cout<<"Convertion failed"<<endl;
pc=dynamic_cast<C*>(pa);
if(pc){
pc->show();
cout<<pc->k<<endl;
}
else
cout<<"Convertion failed"<<endl;
getchar();
}
程序输出结果是:
class B
2
Convertion failed
由于指针pa所指的对象的实际类型是class B,所以将pa转换为B*类型没有问题,而将pa转换成C*类型时则失败。当指针转换失败时,返回NULL。
3.3.2 dynamic_cast的交叉转换
交叉转换(crosscast)是在两个“平行”的类对象之间进行。本来它们之间没有什么关系。将其中的一种转换为另一种是不可行的。但是,如果类A和类B都是某个派生类C的基类,而指针所指的对象本身就是一个类C的对象,那么该对象既可以被视为类A的对象,也可以被视为类B的对象,类型A*(A&)和B*(B&)之间的转换就成为可能。见下面的例子。
#include <iostream>
using namespace std;
class A{
public:
int num;
A(){num=4;}
virtual void funcA(){}
};
class B{
public:
int num;
B(){num=5;}
virtual void funcB(){}
};
class C:public A,public B{};
int main(int argc,char* argv[])
{
C c;
A* pa;
B* pb;
pa=&c;
cout<<pa->num<<endl;
pb=dynamic_cast<B*>(pa);
cout<<"pa="<<pa<<endl;
if(pb)
{
cout<<"pb="<<pb<<endl;
cout<<"Conversion succeeded"<<endl;
cout<<pb->num<<endl;
}
else
cout<<"Conversion failed"<<endl;
getchar();
}
程序输出结果是:
可以看出,pa转换成pb之后,其值产生了变化,也就是说,在类C的对象中,类A的成员和类B的成员所占的位置(距离对象首地址的偏移量)是不同的。类B的成员要靠后一些,所以将A*转换为B*的时候,要对指针的位置进行调整。如果将在程序中的dynamic_cast替换成static_cast,则程序无法通过编译,因为这是编译器认为类A和类B是两个“无关”的类。
3.4 reinterpret_cast
reinterpret_cast是一种最为“狂野”的转换。它在C++四中新的转换操作符中的能力是最强的,其转换能力与C的强制类型转换不分上下。正是因为其过于强大的转换能力,reinterpret_cast是C++语言中最不提倡使用的一种数据类型转换操作符,应该尽量避免使用。
主要用于转换一个指针为其他类型的指针,也允许将一个指针转换为整数类型,反之亦然。这个操作符能够在非相关的类型之间进行。不过其存在必有其价值,在一些特殊的场合,在确保安全性的情况下,可以适当使用。它一般用于函数指针的转换。见如下程序。
#include <iostream>
using namespace std;
typedef void (*pfunc)();
void func1()
{
cout<<"this is func1(),return void"<<endl;
}
int func2()
{
cout<<"this is func2(),return int"<<endl;
return 1;
}
int main(int argc,char* argv[])
{
pfunc FuncArray[2];
FuncArray[0]=func1;
FuncArray[1]=reinterpret_cast<pfunc>(func2);
for(int i=0;i<2;++i)
(*FuncArray[i])();
getchar();
}
程序输出结果:
this is func1(),return void
this is func2(),return int
由函数指针类型int(*)()转换为void(*)(),只能通过reinterpret_cast进行,用其他的类型转换方式都会遭到编译器的拒绝。而且从程序的意图来看,这里的转换是“合理”的。不过,C++是一种强制类型安全的语言,即使是用interpret_cast,也不能任意地将某种类型转换为另一种类型。C++编译器会设法保证“最低限度”的合理性。
语言内置的类型转换操作符无法胜任的工作需要程序员手动重载相关转换操作符来完成类型转换。
4. 手动重载相关类型转换操作符
在各种各样的类型转换中,用户自定义的类类型与其他数据类型间的转换要引起注意。这里要重点考察如下两种情况。
4.1不同类对象的相互转换
由一种类对象转换成另一种类对象。这种转换无法自动进行,必须定义相关的转换函数,其实这种转换函数就是类的构造函数,或者将类类型作为类型转换操作符函数进行重载。此外,还可以利用构造函数完成类对象的相互转换,见如下程序。
#include <iostream>
using namespace std;
class Student{
char name[20];
int age;
public:
Student(){};
Student(char *s, int a){
strcpy(name,s);
age=a;
}
friend class Team;
};
class Team{
int members;
Student monitor;
public:
Team(){};
Team(const Student& s):monitor(s),members(0){};
void Display()const{
cout<<"members‘ number :"<<members<<endl;
cout<<"monitor‘s name :"<<monitor.name<<endl;
cout<<"monitor‘s age :"<<monitor.age<<endl;
}
};
ostream& operator<<(ostream& out,const Team &t){
t.Display();
return out;
}
int main(int argc,char* argv[])
{
Student s("吕吕",23);
cout<<s;
getchar();
}
程序输出结果:
members’ number :0
monitor’s name :吕吕
monitor’s age :23
本来,输出操作符operator<<并不接受Student类对象作为参数,但由于可通过类Team的构造函数将Student类对象转换成Team类对象,所以输出操作可以成功进行。类的单参数构造函数实际上充当了类型转换函数。
4.2基本类型与类对象的相互转换
4.2.1基本类型转换为类对象
这种转换仍可以借助于类的构造函数进行的。也就是说,在类的若干重载的构造函数中,有一些接受一个基本数据类型作为参数,这样就可以实现从基本数据类型到类对象的转换。
4.2.2类对象转换为基本数据类型
由于无法为基本数据类型定义构造函数,所以由对象想基本数据类型的转换必须借助于显示的转换函数。这些转换函数名由operator后跟基本数据类型名构成。下面是一个具体的例子。
#include <iostream>
using namespace std;
class A{
public:
operator int(){
return 1;
}
operator double(){
return 0.5;
}
};
int main(int argc,char* argv[])
{
A obj;
cout<<"Treating obj as an interger, its value is: "<<(int)obj<<endl;
cout<<"Treating obj as a double, its value is: "<<(double)obj<<endl;
getchar();
}
程序输出结果:
Treating obj as an interger, its value is: 1
Treating obj as a double, its value is: 0.5
在一个类中定义基本类型转换的函数,需要注意一下几点:
(1)类型转换函数只能定义为一个类的成员函数,而不能定义为外部函数。类型转换函数与普通成员函数一样,也可以在类体中声明,在类外定义。
(2)类型转换函数通常是提供给类的客户使用的,所以应将访问权限设置为public,否则无法被显示的调用,隐式的类型转换也无法完成。
(3)类型转换函数既没有参数,也不显示的给出返回类型。
(4)转换函数必须有“return目的类型数据;”的语句,即必须返回目的类型数据最为函数的返回值。
(5)一个类可以定义多个类型转换函数。C++编译器将根据目标数据类型选择合适的类型转换函数。在可能出现二义性的情况下,应显示地使用类型转换函数进行类型转换。
5.总结
(1)综上所述,数据类型转换相当于一次函数调用。调用的的结果是生成了一个新的数据实体,或者生成一个指向原数据实体但解释方式发生变化的指针(或引用)。
(2)编译器不给出任何警告也不报错的隐式转换总是安全的,否则必须使用显示的转换,必要时还要编写类型转换函数。
(3)使用显示的类型转换,程序猿必须对转换的安全性负责,这一点可以通过两种途径实现:一是利用C++语言提供的数据类型动态检查机制;而是利用程序的内在逻辑保证类型转换的安全性。
(4)dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast用于类层次间的向上转换和向下转换,还可以用于类间的交叉转换。在类层次间进行向上转换,即子类转换为父类,此时完成的功能和static_cast是相同的,因为编译器默认向上转换总是安全的。向下转换时,dynamic_cast具有类型检查的功能,更加安全。类间的交叉转换指的是子类的多个父类之间指针或引用的转换。
dynamic_cast能够实现运行时动态类型检查,依赖于对象的RTTI(Run-Time Type Information),通过虚函数表找到RTTI确定基类指针所指对象的真实类型,来确定能否转换。
(5)interpre_cast类似于C的强制类型转换,多用于指针(和引用)类型间的转换,权利最大,也最危险。static_cast全力较小,但大于dynamic_cast,用于普通的转换。进行类层次间的下行转换如果没有动态类型检查,是不安全的。
(6)const-cast只用于去除指针或者引用类型的const和volatile属性,变量本身的属性不能被去除。
在进行类型转换时,请坚持如下原则:
(1)子类指针(或引用)转换为父类指针(或引用)编译器认为总是是安全的,即向上转换,请使用static_cast,而非dynamic_cast,原因是static_cast效率高于dynamic_cast。
(2)父类指针(或引用)转换为子类指针(或引用)时存在风险,即向下转换,必须使用dynamic_cast进行动态类型检测。
(3)总领性原则:不要使用C风格的强制类型转换,而是使用标准C++中的四个类型转换符:static_cast、dynamic_cast、reinterpret_cast、和const_cast。
参考文献
[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.
版权声明:本文为博主原创文章,未经博主允许不得转载。