在C/C++中,类型转换发生在这种情况下:为了实现不同类型的数据之间进行某一操作或混合运算,编译器必须把它们转换成同一种类型的数据。
C/C++语言中的类型转换分为两种:一种是隐式转换,特指那些由编译器完成的类型转换;另一种是显式转换,特指那些由开发人员显式进行的数据类型转换。
说明:
隐式转换在编译过程中由编译器按照一定的规则自动完成,无须任何人为干预。显式转换由人为因素显式干预完成。与隐式转换相比,显式转换使得开发人员更容易获取所需类型的数据,阅读代码的人也更易明白开发人员的真实意图。
存在大量的隐式转换也是 C/C++语言常受人诟病的焦点之一。隐式转换虽然可以给开发人员带来一定的便利,如使代码更加简洁、减少不必要的冗余,但它所带来的副作用也是不可小觑的,它常常使开发人员变得苦不堪言。
C/C++的隐式转换主要发生在如下两种情况下。
1.内置类型间的隐式转换
内置数据类型的隐式转换发生在如下这些条件下:在混合类型的表达式中,操作数被转换成相同的类型;用作if 语句或循环语句的条件时,被转换为bool类型;用于switch语句时,被转换为整数类型;用来初始化某个变量(包括函数实参、retum语句)时,被转换为变量的类型。内置类型转换时,转换级别如图 2-7所示,且在隐式转换时,总是由低级别到高级别转换。
double | > | float | > | long longunsigned long long | > | longunsigned long | > | intunsigned int | > | shortunsigned short | > | charunsigned char |
表2-7内置类型转换级别
同时内置类型的转换级别还遵循下述8条规则:
●除char 和 signed char 外,任何两个有符号的整数类型都具有不同的级别(Rank)。
●有符号整数类型的级别高于比它小的有符号整数类型的级别。
●无符号整数类型的级别等于其对应的有符号整数类型的级别。
●标准整数类型的级别高于同样大小的扩展整数类型的级别(比如在longlong为 64 位,且int64为扩展整数类型的情况下,前者的级别高于后者)。
●布尔类型的级别低于任何一个标准的整数类型级别。char16_t、char32_t、wchar_t 的级别等于它们底层类型的级别。
●相同大小的两个有符号扩展整数类型间的级别高低由实现定义。
●整数类型级别低于浮点数级别,而双精度浮点数的级别高于单精度浮点数的级别。
隐式转换规则
●为防止精度损失,类型总是被提升为较高级别的类型。
●所有含有小于整型类型的算术表达式在计算之前其类型均被转换为整型。
在编码中关于内置类型转换的隐式转换过程及其产生的副作用,可以参考下面这段代码:
int nValue = 100;
float fValue = 50.1233f;
double dValue = 100;
cout <<(nValue +fValue) <<endl; // nValue被提升为float 类型,提升后值为100.0
void Output(double dPutValue);
void Output(float fPutValue);
...
Output(dValue); //调用double 类型的 Output 函数
Output(fValue); //调用float 类型的Output函数
Output(100.4); //调用错误,编译器无法决定到底调用哪个函数版本
2.non-explicit构造函数接受一个参数的用户定义类对象之间的隐式转换 首先看下面这段代码:
// CTest测试类
class CTest
{
public
CTest (int n) {m nNum =n;} // 普通构造函数
Virtual ~CTest (){ }
private:
int m nNum;
};
void Func(CTest test); //完成某一功能的一个函数
int main()
{
Func(100);//将100作为参数传给Func,并执行函数
}
在上述代码中,当调用Func()函数时会发现形式参数和实参的类型不匹配。但是编译器发现形参类CTest 有一个只有一个int 类型参数的构造函数,所以编译器会以100 为实参调用CTest的构造函数构造临时对象,然后将此临时对象传给Func()函数。也许你会惊讶:编译器默默地为我们做了这么多工作,真是奇妙。这些工作发生得那么悄无声息,如果误用了,会不会引起难以预料的错误?答案是肯定的。
其实这些问题是共存的,编译器为程序员提供了隐式转换的便利,所以如果出现了错误也是程序员需要负责的问题。权利和义务对等原则在这里依然适用。为了体现这种对等原则,如程序员想避免隐式转换带来的麻烦,必须履行相应的义务一限制隐式转换。C++提供了两种有效途径解决隐式转换控制。
1)根据需要自定义具名转换函数
首先看下面的代码:
class String
{
public:
operator const char*();// 在需要时,String对象可以转换成const char* 指针
};
//上面的定义将使很多表达式通过编译(编译器启用了隐式转换)
//假设s1和s2均是String类型的字符串
intx=s1-s2; //可以编译,但行为不确定
const char*p = s1-5: //可以编译,但行为不确定
p=s1+ '0'; //可以编译,但不是开发人员期望的结果
if( s1="0"){..} //可以编译,但不是开发人员期望的结果
为了避免此类问题的出现,建议使用自定义转换具名函数代替转换操作符。如下述代码就是一段较好的代码:
class String
{
public:
const char* as char pointer() const; // String对象转换成 const char*指针
};
//假设s1和s2均是String类型的字符串
Int x=s1-s2; //编译错误
const char*p =s1 - 5; //编译错误
p=s1 + '0'; //编译错误
if( s1=="0" ) {...} //编译错误
2)使用 explicit 限制的构造函数
这种方法针对的是具有一个单参数构造函数的用户自定义类型。代码如下:
class Widget
{
public
Widget(unsigned int widgetizationFactor);
Widget(const char*name,const Widget*other =0);
};
Widget widget1-100; //可编译通过
Widget widget1-"my window"; //可编译通过
上述代码中,unsigned int 类型和 char*类型变量都可以隐式转换为 Widget 类型对象。
为了控制这种隐式转换,C++引入了explicit关键字,在构造函数声明时添加explicit 关键字可禁止此类隐式转换。再看添加了explicit 关键字的上述代码编译情况:
class Widget
{
public:
explicit Widget(unsigned int widgetizationFactor);
explicit Widget(const char* name,const Widget* other =0);
};
Widget widget1-100; //编译错误
Widget widget1="my window"; //编译错误
最后讲述一个重要的隐式转换理念。如果有这么一个问题:在隐式转换过程中,转型真是什么都没做,仅仅是告诉编译器把某种类型视为另外一种类型吗?大部分程序员会说是的。其实这个观念是错误的。也许你不同意这种说法,但是看完下面的两个例子,你一定会惊呆的。
示例1如下:
Class CBaseA
{
Public:
virtual void Func1) {8
};
Class CBaseB
{
Public:
virtual void Func2() {}
};
Class CDrived:public CBaseA, public CBaseB
{
Public:
virtual void Func1() {}
virtual void Func2() {}
};
CDrived d;
CDrived*pd=&d:
CBaseB*pb=&d;
printf("d's location is %dr'n", pd);
printf("d's location is %dr\n", pb);
现在的问题是:代码中 pd 和 pb 两个指针的值是相等的吗?笔者在 Visual Studio2010上运行的结果如下:
d's location is 2685200
d's location is 2685204
我们分析一下上述示例为什么会有这样的运行结果:我们仅仅是建立一个基类指针指向一个子类对象,然后再建立一个子类指向此子类,最后两个指针的值就不同了。这种情况下两者会有一个偏移量,在运行时会施加于子类指针上,用以取得正确的基类指针值。
小心陷阱:
不仅多重继承对象拥有一个以上的地址,即使在单一继承对象中也会发生这样的情况。所以请注意,在类型隐式转换过程中,应该避免做出“对象在C++中如何分布布局”的假设。
示例2 如下:
char cValue1 =255;
printf("cValue1 =%drn", cValuel);
现在这段代码的运行结果是什么?编译器会输出“cValue1=255”吗?如果把这段代码在Visual Studio 2010 编译器上运行一下,你会发现输出结果不是“cValuel=255”,而是“cValue1 =-1”。我们分析一下为何会出现这样的问题。
●printf 在执行时,输入参数的类型是 int 型,而 cValue1 的类型却是char 型,所以函数 printf在执行时 cValue1会提升 int 型临时变量 temp。
●temp 在提升时遵循如下规则:数据提升时进行符号位扩展,cValue1=255=1111,1111B 符号位为1,所以提升为int类型时temp=1111,1111,1111,1111,1111,1111,1111,1111B。temp 的原码是-1,所以上述代码输出-1。
注意:整型值类型提升规则是,补码进行符号位扩展,得到的补码值即提升后的变量值。
上面的两个例子仅仅是隐式转换过程中最常见的两类隐式转换。通过上述两个简单的例子可以看出,编译器在进行隐式转换时并不是我们想象的什么都不做,仅仅是赋值而已,在隐式转换过程中会进行很多微妙的处理,这才是我们在使用隐式转换时应注意的地方。
请谨记
●在使用编译器隐式类型转换时,一定要注意,能减少隐式转换使用时尽量减少隐式转换的使用。
●除非明确知道隐式转换时编译器发生什么,否则在编程时不要对编译器隐式转换进行任何假设。