C++算数类型的隐式转换

最近看《C++ Primer》第五版中文版,看到第四章算术类型转换部分,感觉部分内容书上阐述不太详尽,因此另做了些测试和记录。

对算术类型隐式转换的说明

C++的算数类型包含了字符、整型、布尔型以及浮点型,算术类型之间能够相互转换,将一种算术类型转换为另一种算术类型就被称为算术转换。这种转换可以显示的进行,即手动进行强制类型转换,例如:

char c = 0;
int a = (int)c; // C语言强制类型转换语法(也是旧式C++强制类型转换语法)

显示的转换完全在程序员的掌控之下,安全自理。这里主要分析隐式的算术类型转换,因为隐式转换往往不容易被注意到,更容易犯错。

隐式转换由编译器进行,会发生隐式转换的场景在书的P141页有说明。就算术类型的隐式类型转换而言,有2条原则:向宽类型转;既有整型又有浮点型则向浮点型转。正如书中所说,这样转换就能够尽可能的避免损失精度。具体如何进行转换呢?结合书中表述,这里以表达式op1 + op2为例说明。

假设op1和op2是不同的算术类型,此时,显然该表达式会发生隐式类型转换。具体的转换过程可概括如下两步

第一步:
对操作数进行整型提升,提升后的类型在确保能够容纳原类型所有可能的值的前提下尽可能的小,如下图所示:
在这里插入图片描述
第二步:
对提升后的操作数类型分情况讨论:
1、如果两者都是带符号类型或都是无符号类型,那么两相比较,将其中较小的类型转换成较大的类型;
2、如果一个操作数的类型是带符号的,另一个是无符号的,那么就比较这两个类型的大小,若在当前平台上,上述带符号类型能够存储所有上述无符号类型的所有值,那么无符号类型转为带符号类型,否则转换方向相反。

举例如下:

int a = -1;
unsigned int b = 0;
// int转换为unsigned int,输出为4294967295
cout << a + b << endl;

signed char d = -1;
unsigned char e = 0;
// signed char以及unsigned char都被整型提升为int,输出为-1
cout << d + e << endl;

一种更复杂的情况

仅有两个操作数的表达式的情况是很好理解的,书上讲的也很清楚了。而一个表达式中有多个不同类型的操作数的情况则稍微麻烦一些。第一步的整型提升没有什么区别。第二步的类型转换则需要注意,就op1 + op2 + op3来说,假设整型提升之后op3的类型最大,且能容纳op1和op2的类型,那op1、op2是否会统一转换成op3的类型再计算呢?答案是否定的。编译器的隐式类型转换行为似乎是按照运算符的结合律来的,根据加号的结合律,op1和op2是在一起计算的,因此将op1、op2按前文所述的规则进行隐式类型转换,计算的结果再和op3一起进行隐式类型转换。

还是举例说明:

// 例中a、b在一起计算时,会发生int到unsigned int的转换,而b、c在一起计算则发
// 生的是unsigned int到long long int的转换,因此输出的结果看上去比较正常
int a = -1;
unsigned int b = 0;
long long int c = 0;
cout << a + b + c << endl; // 4294967295
cout << b + a + c << endl; // 4294967295
cout << b + c + a << endl; // -1

// 不同于上例,本例会发生整型提升,signed char、unsigned char会先被提升成int类
// 型,不会发生负数到无符号数的转换,所以结果都比较正常
signed char d = -1;
unsigned char e = 0;
int f = 0;
cout << d + e + f << endl; // -1
cout << e + d + f << endl; // -1
cout << e + f + d << endl; // -1

补充1:负数到无符号数转换的规则

上面的示例中会看到4294967295这样的奇怪结果,这其实是32位的-1转换到32位的无符号数的转换结果。这里就将说明C++中负数到无符号数的转换规则。

首先明确一下什么时候才会发生这样的转换:一是强制类型转换,二是上文所述的当有符号类型与无符号类型一起运算且无符号类型(位宽)不小于有符号类型,即有符号类型无法容纳无符号类型的所有值,此时有符号类型只能向无符号类型转换。无论上述何种情况下的转换,负数到无符号数的转换都遵循一定的规则,三是将负数赋值给一个无符号类型的变量。当然可能还存在其它情况,不过上述三种情况最为常见。

《C++ Primer》书的2.1.3节中有这样的表述:当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型能表示的数值的总个数进行取模后的余数。这段话很明确,但问题是在书的4.2节又有这样的表述:运算符%俗称取余或取模运算符,书中并未区分取模和取余运算,而将取余运算应用到上述规则时,发现存在问题:

将-1赋值给unsigned char类型的变量,-1肯定不在unsigned char的表示范围之内,因此需要将-1转换为unsigned char类型,按照规则,转换后的值为-1%256,按照书中4.2节描述的取余运算,-1%256=-(1%256)=-1,即按照规则,转换后的值为-1,显然是有问题的。

取余运算与取模运算

为了搞清楚书中的取模究竟指什么操作,我们有必要先了解一下取模与取余运算的区别。其实取余和取模运算是不完全一致的,具体的,对于整型数a,b来说,取模运算或者求余运算的方法都是:
1、求整数商:c = a/b;
2、计算模或者余数:r = a - c*b.
但求模运算和求余运算在第一步不同:取余运算在取c的值时,向0取整;而取模运算在计算c的值时,向负无穷方向取整。

正确的理解上述转换规则

显然,书中4.2节所说的是取余运算。而2.1.3节表述的规则所用的是取模运算。就取模运算来说,-1%256=255,这样就对了。这么看来,有时候将取余和取模严格区别开来,是能够把问题说的更清楚的。

以上应该就说清楚了负数到无符号数转换的规则。顺带说一句,给有符号类型的变量赋一个超出该类型表示范围的值,结果是未定义的

PS:

假设要将一个int型负数,转换成unsigned int型的无符号数,两者都是32位,则不难发现,将负数转换成无符号类型后的值,在内存中的形态(该值的补码,对于无符号数也就是其原码)和该负数在内存中的形态(该负数的补码)是一致的,也就是说将该负数的补码按照无符号类型来解读,得到的结果就是其转换后的结果,而按照有符号类型来解读,得到的就是该负数本身。

补充2:溢出是未定义行为,编译器不会提升类型以应对之

当我们进行某些运算导致溢出时,编译器不会主动提升类型,溢出是未定义行为,应当杜绝。比如说,对一个负数进行去负操作以期获取其绝对值,因为有符号数表示范围不是对称的,所以有可能造成溢出,需要认识到这时候编译器是不会主动提升类型的,要想避免溢出,则我们需要手动进行类型提升。

举例如下:

// 发生溢出的例子
int i = 0x80000000;
cout << i << endl; // -214748364
// 溢出,不能对输出的值抱有什么期待
cout << -i << endl; // -2147483648
// 对一个溢出的数进行强制类型转换,结果也是不确定的
cout << (long long int)-i << endl; // -2147483648
// 将i的类型提升后,再取负,就不会溢出,此时就能得到期待的绝对值了
cout << -(long long int)i << endl; // 2147483648

// 因整型提升而未发生溢出的例子
signed char t = 0x80;
// 因果关系是,因整型提升————将t提升为int型,再取负,因此未发生溢出,而非相反
cout << -t << endl; // 128

注:

1、测试的程序在VS2015和GCC6.3.0上均测试过,两者行为一致;
2、本人系初学菜鸟,所述内容如有错误欢迎各路人才在评论区批评指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值