在C++中,如果两种类型有关联,那么就可以相互转换(conversion)。
以加法运算为例,C++不会将两个不同类型的值相加,而是先根据类型转换规则设法先将运算对象统一后在求值。上述的类型转换是自动执行的,无须程序员的介入,因此被称为隐式转换(implicit conversion)。
算术类型之间的隐式转换被设计得尽可能避免损失精度,例如,如果表达式中既有整数类型也有浮点数类型,那么整型会转换成浮点数类型。
对于初始化来说,因为被初始化的对象类型无法改变,所以初始值被转换成该对象的类型。如果使用double型的值初始化int型的变量,那么double型的值会隐式转换成int并忽略小数部分(有些编译器会警告运算损失了精度)。
何时发生隐式类型转换
1. 在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型;
2. 在条件中,非布尔值转换成布尔值;
3. 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
4. 如果算术运算或关系运算的运算对象有多种类型,需要转换成一种类型;
5. 如1.5章要介绍的,函数调用也会发生类型转换;
1) 算术转换
算术转换(arithmetic conversion)是把一种算术类型转换成了另外一种算术类型。算术转换的规则定义了一套类型转换的层次,其中该运算符的运算对象将转换成最宽的类型。举例,如果一个运算对象的类型是long double,那么不管另一个运算对象的类型是什么都会被转换成long double。一般地,同时有浮点类型和整数类型时,整数值将转换成相应的浮点数。
整型提升
整数提升(integral promotion)负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型;否则提升成unsigned int型。常见是false提升成0,true提升成1。
较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long、unsigned longlong中个最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果其中存在无符号类型,那么转换的结果依赖于机器中各个整数的相对大小。需要进行以下处理:
1. 首先进行整型提升;
2. 如果结果的类型都是带符号或不带符号的:
a) 如果类型匹配则无须进行转换;
b) 如果类型不匹配则小类型转换成较大的类型;
3. 如果一个是有符号另一个是无符号的:
a) 无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的;
举例:如果两个类型分别是unsigned int和int,那么int类型的运算对象转换成unsigned int类型。注意:如果int型的值恰好为负数,那么转换会发生问题。
b) 带符号的类型大于无符号类型,那么转换结果依赖于机器。
i. 如果无符号类型的所有值都能存在带符号类型中,那么无符号类型的对象转换成带符号类型;
ii. 如果不能,则带符号的转换成无符号类型。
举例:两个运算对象分别是long和unsigned int,如果int和long的大小相同,那么long转换成unsigned int型;如果long比int大,则unsigned int型的对象转换成long类型的。
理解算术转换
举例说明:
bool flag; char cval;
short sval; unsigned short uval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; //'a'提升成int,然后该int值转换成long double
dval + ival; //ival转换成double
dval + fval; //fval转换成double
ival = dval; //dval转换成(切除小数部分后)int
flag = dval; //如果dval是0,则flag是false,否则flag是true
cval + fval; //cval提升成int,然后该int值转换成float
sval + cval; //sval和cval都提升成int
cval + lval; //cval转换成long
ival + ulval; //ival转换成unsigned long,无符号的类型大于等于有符号类型
usval + ival; //根据unsigned short 和int所占空间的大小进行提升
uival + lval; //根据unsigned int和long所占空间的大小进行转换
2) 其它隐式转换
除了算术转换之外,还有几种隐式转换:
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int ia[10]; //含有10个整数的数组
int* ip = ia; //ia转换成指向数组首元素的指针
当数组被用作一下情况时,不会被转换成指针:
1. decltype关键字的参数;
2. 取地址符(&);
3. Sizeof;
4. Typeid(在运行时类型识别那一节将介绍);
5. 给数组引用来绑定数组时;
指针的转换:C++还规定了几种其它的指针转换方式:
1. 包括常量整数值0或者字面值nullptr能转换成任意指针类型;
2. 指向任意非常量的指针能转换成void*;
3. 指向任意对象的指针能转换成const void*;
4. 基类指针可以指向派生类对象;
转换成布尔类型:如果指针或算术类型的值是0,那么转换结果是false;否则转换结果是true。
转换成常量:允许将指向非常量类型的指针转换成相应的常量类型的指针,引用也是这样。
类类型定义的转换:
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型转换。在1.6.5节中将看到一个例子,该例子同时提出多个转换请求,这些请求将被拒绝。
举例说明:
string s,t = "avalue"; //字符串字面值转换成string类型
while(cin >> s) //while的条件部分把cin转换成布尔值
条件(cin >> s)实际检查的是istream类型的值,IO库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值,即最后一次读入成功则返回true;否则是false。
3) 显式转换
有时我们需要显式地将对象强制转换成另外一种类型。
Tips:虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
命名的强制类型转换
一个命名的强制类型转换有如下形式:
cast-name<type>(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是左值,那么结果是左值。Cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。dynamic_cast支持运行时类型识别,一般只支持子类向基类的转换,将在运行时类型识别那一节做更详细的介绍。
static_cast
任何具有明确定义的类型转换,只要不包含底层cosnt,都可以使用static_cast。举例如下:
//进行强制类型转换以便执行浮点数除法
int i,j;
double slope = static_cast<double>(j) / i;
static_cast有两个主要的作用:
1. 把一个较大的算术类型转换成较小的类型。如果不使用static_cast,那么编译器一般会警告有精度损失,使用了static_cast则该警告消失;
2. 对于编译器无法自动执行的类型转换也非常有用,比如可以使用static_cast找回存在于void*指针中的值,即将void*指针转回其原来的类型。
const_cast
const_cast只能改变对象的底层const:
const char *pc;
char *p = const_cast<char*>(pc); //正确,但是通过p写值是未定义的行为
这种将常量转换成非常量的行为称为“去掉const性质(cast away the const)”。去掉某个对象的const性质可以使编译器不再阻止我们对该对象的写操作。但是使用const_cast获得写操作的权限然后执行写操作会产生未定义的操作。
只有const_cast才能改变表达式的常量属性,同时它也只能做改变常量属性这一件事,不能改变表达式的类型。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。使用reinterpret_cast可以将某个类型强制转换为另外一个类型。如:
int *ip;
char *pc = reinterpret_cast<char*>(ip);
此时如果调用以下代码:
string str(pc);
编译器不会报错,因为已经显式地转换了格式,但是可能导致异常的运行时行为。因此使用reinterpret_cast是非常危险的。
Tips: reinterpret_cast本质上依赖于机器,要想安全地使用reinterpret_cast必须对设计的类型和编译器实现转换的过程都非常了解。所以尽量避免使用强制类型转换,特别是reinterpret_cast,每次使用了一条强制类型转换语句时,都应反复斟酌能否以其他方式实现相同的目标。
旧式的强制类型转换
在早期的C++语言中,显式类型转换包含两种形式:
type(expr); //函数形式的强制类型转换
(type) expr; //C语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast、static_cast、reinterpret_cast相似的行为。
Tips:与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不清晰,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。