C++Primer学习(4.11 类型转换)

4.11 类型转换
在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换(conversion),那么它们就是关联的。
举个例子,考虑下面这条表达式,它的目的是将iva1初始化为6:

int ival=3.541+3;// 编译器可能会警告该运算损失了精度

加法的两个运算对象类型不同:3.541的类型是double,3的类型是int。C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicit conversion)
算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。在上面的例子中,3转换成 double类型,然后执行浮点数加法,所得结果的类型是double。
接下来就要完成初始化的任务了。在初始化过程中,因为被初始化的对象的类型无法改变,所以初始值被转换成该对象的类型。仍以这个例子说明,加法运算得到的double类型的结果转换成 int 类型的值,这个值被用来初始化ival。由double向int 转换时忽略掉了小数部分,上面的表达式中,数值6被赋给了ival。
何时发生隐式类型转换
在下面这些情况下,编译器会自动地转换运算对象的类型:
1)在大多数表达式中,比int类型小的整型值首先提升为较大的整数类型。
2)在条件中,非布尔值转换成布尔类型。
3)初始化过程中,初始值转换成变量的类型:在赋值语句中,右侧运算对象转换成左侧运算对象的类型
4)如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
5)如第6章将要介绍的,函数调用时也会发生类型转换。
4.11.1 算术转换
算术转换(arithmetic conversion)的含义是把一种算术类型转换成另外一种算术类型这一点在 2.1.2节(第32页)中已有介绍。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是1ongdouble,那么不论另外一个运算对象的类型是什么都会转换成 long double。还有-种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。
整型提升
整型提升(integral promotion)负责把小整数类型转换成较大的整数类型。对于boo1、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 long long 中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
如果某个运算符的运算对象类型不一致,这些运算对象将转换成同一种类型。但是如果某个运算对象的类型是无符号类型,那么转换的结果就要依赖于机器中各个整数类型的相对大小了。
像往常一样,首先执行整型提升。如果结果的类型匹配,无须进行进一步的转换。如果两个(提升后的)运算对象的类型要么都是带符号的、要么都是无符号的,则小类型的运算对象转换成较大的类型。
如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的。例如,假设两个类型分别是 unsigned int 和 int,则 int 类型的运算对象转换成 unsigned int 类型。需要注意的是,如果int型的值恰好为负值,其结果将以2.1.2节(第32页)介绍的方法转换,并带来该节描述的所有副作用。
剩下的一种情况是带符号类型大于无符号类型,此时转换的结果依赖于机器。如果无符号类型的所有值都能存在该带符号类型中,则无符号类型的运算对象转换成带符号类型。如果不能,那么带符号类型的运算对象转换成无符号类型。例如,如果两个运算对象的类型分别是long和unsigned int,并且int和 long 的大小相同,则 long 类型的运算对象转换成unsigned int类型;如果long类型占用的空间比int更多,则unsigned int类型的运算对象转换成1ong类型。
理解算术转换
要想理解算术转换,办法之一就是研究大量的例子:
在这里插入图片描述
在这里插入图片描述
在第一个加法运算中,小写字母"a’是char 型的字符常量,它其实能表示一个数字值(参见 2.1.1节,第30页)。到底这个数字值是多少完全依赖于机器上的字符集,在我们的环境中,'a’对应的数字值是 97。当把’a’和一个 long double 类型的数相加时,char类型的值首先提升成 int 类型,然后int 类型的值再转换成1ong double 类型。最终我们把这个转换后的值与那个字面值相加。最后的两个含有无符号类型值的表达式也比较有趣,它们的结果依赖于机器。
4.11.2 其他隐式类型转换
除了算术转换之外还有几种隐式类型转换,包括如下几种。
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:
int ia[10];//含有10个整数的数组
int* ip = ia;// ia转换成指向数组首元素的指针
当数组被用作 decltype 关键字的参数,或者作为取地址符(&)、sizeof及typeid(第19.2.2节,732页将介绍)等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组(参见3.5.1节,第102页),上述转换也不会发生。我们将在6.7节(第221页)看到,当在表达式中使用函数类型时会发生类似的指针转换.
指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型:指向任意非常量的指针能转换成void*;指向任意对象的指针能转换成const void*。15.2.2节(第530页)将要介绍,在有继承关系的类型间还有另外一种指针转换的方式,
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp=get_string();
if(cp)/*...*///如果指针cp不是0,条件为真
while(*cp)/*...*///如果*cp不是空字符,条件为真

转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用(参见2.4.1节,第54页和2.4.2节,第56页):

int i;
const int &j=i;//非常量转换成const int的引用
const int *p= &i;//非常量的地址转换成const的地址
int &r=j,*q=p;//错误:不允许const转换成非常量

相反的转换并不存在,因为它试图删除掉底层const。
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。在7.5.4节(第263页)中我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。
我们之前的程序已经使用过类类型转换:一处是在需要标准库string类型的地方使用C风格字符串(参见3.5.5节,第111页);另一处是在条件部分读入istream:

string s,t="a value";//字符串字面值转换成string类型
while(cin>>s)//while的条件部分把cin转换成布尔值

条件(cin>>s)读入cin的内容并将cin作为其求值结果。条件部分本来需要一个布尔类型的值,但是这里实际检査的是istream 类型的值。幸好,I0库定义了从istream向布尔值转换的规则,根据这一规则,cin自动地转换成布尔值。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值是true;相反,如果最后一次读入不成功,转换得到的布尔值是false.
4.11.3 显式转换
有时我们希望显式地将对象强制转换成另外一种类型。例如,如果想在下面的代码中执行浮点数除法:
int i,j;
double slope =i/j;
就要使用某种方法将i和/或j显式地转换成double,这种方法称作强制类型转换(cast)。
WARNING:虽然有时不得不使用强制类型转换,但这种方法本质上是非常危险的。
命名的强制类型转换
一个命名的强制类型转换具有如下形式:
cast-name(expression);
其中,type是转换的目标类型而expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast 中的一种。dynamic cast 支持运行时类型识别,我们将在 19.2节(第730页)对其做更详细的介绍。cast-name指定了执行的是哪种转换。
static_cast
任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:

//进行强制类型转换以便执行浮点数除法
double slope=static_cast<double>(j)/i;

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息:但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于 void*指针(参见 2.3.2节,第50页)中的值:

void *p= &d;//正确:任何非常量对象的地址都能存入void*
//正确:将void*转换回初始的指针类型
double *dp=static_cast<double*>(p);

当我们把指针存放在 void*中,并且使用static cast 将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符将产生未定义的后果。
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能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast改变表达式的类型:

const char *cp;
//错误:static_cast不能转换掉const性质
char *q=static_cast<char*>(cp);
static_cast<string>(cp);//正确:字符串字面值转换成string类型
const_cast<string>(cp);//错误:const_cast只改变常量属性

const_cast常常用于有函数重载的上下文中,关于函数重载将在6.4节(第208页)进行详细介绍。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。举个例假设有如下的转换

int *ip;
char *pc =reinterpret_cast<char*>(ip);

我们必须牢记 pc所指的真实对象是一个int而非字符,如果把 pc 当成普通的字符指针使用就可能在运行时发生错误。例如:

string str(pc);//用pc初始化str

可能导致异常的运行时行为。
使用reinterpret cast是非常危险的,用pc初始化str的例子很好地证明了这一点。其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。当我们用一个int的地址初始化pc 时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。接下来再使用pc时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。最终的结果就是,在上面的例子中虽然用pc初始化str 没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。查找这类问题的原因非常困难,如果将ip强制转换成pc的语句和用pc初始化string 对象的语句分属不同文件就更是如此。
WARNING:
reinterpret_cast本质上依赖于机器。要想安全地使用reinterpret_cast必须对涉及的类型和编译器实现转换的过程都非常了解。
建议:避免强制类型转换
强制类型转换干扰了正常的类型检查(参见2.2.2节,第42页),因此我们强烈建议程序员避免使用强制类型转换。这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。在有重载函数的上下文中使用const_cast无可厚非,关于这一点将在6.4节(第208页)中详细介绍;但是在其他情况下使用constcast也就意味着程序存在某种设计缺陷。其他强制类型转换,比如staticcast和dynamic cast,都不应该频繁使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。
旧式的强制类型转换
在早期版本的C++语言中,显式地进行强制类型转换包含两种形式
type (expr);//函数形式的强制类型转换
(type)expr;//C语言风格的强制类型转换
根据所涉及的类型不同,旧式的强制类型转换分别具有与const_cast,static_cast或reinterpret_cast 相似的行为。当我们在某处执行旧式的强制类型转换时,如果换成const_cast和static_cast也合法,则其行为与对应的命名转换一致。如果替换后不合法,则旧式强制类型转换执行与reinterpret_cast 类似的功能

char *pc=(char*)ip;//ip是指向整数的指针

的效果与使用reinterpret_cast一样。
WARNING:
与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值