基础知识复习:
- 正数在内存中的表示形式:以原码表示的,比如1在在32位机器上为0x00000001
- 负数:以补码表示的,比如-1在32位机器的表示是0xffffffff(最高位表示符号位,关于补码表示,看我后面的参考链接)
- unsigned char转更长字节的类型比如unsigned int,因为是无符号数(总是大于等于0的数)转换,则在高位补0即可,比如unsigned char a=0x01;unsigned int(a)的值就是0x00000001
- unsigned int转unsigned char,直接保留低位。比如unsigned int a=1(内存中是0x00000001),(unsigned char)a的值还是1(内存中是0x01,即仅保留了最低的8个位)
- char转int型(都是有符号型类型之间的转换),那就可能是负数转换,或者正数转换了。比如char a=0x01(因为高位为0,说明这是个正数了,而正数原码和补码一样),转成(int)a的值就是0x00000001(因为是正数,所以还是高位补0);但是若char a=0xff(十进制的-1),(int)a的值就是0xffffffff(因为是负数,所以高位补1)
- int型转char型,还是直接保留低位,比如int a=1(0x00000001);(char)a的值是1(0x01),这是对的,也符合我们的截断思维。若八位 a=-2,即1111 1110转为4位,直接截断得到第四位,即1110,这个数对应的值还是-2(1110是有符号数,那么怎么还原知道表示多少呢,就再求一次它的补码即可,或者(逆过程)先-1再求反码即得原码,除了符号位不变,其他位取反后加1,得到1010,最高位是符号位,说明这是负数,这就是-2,),说明截断后还是正确的值,还是符合我们自然的思维。
- 负数(最高位为1表负数)在内存中是以原码的补码存在的,如
-5表示为( 原码):1000 0101 ===> 反码 :1111 1010 ===> 补码:1111 1011
在java中大数据强转位小范围数据类型:去高位
浮点型转为整型:去小数位,再去高位
正文:
- 不同长度类型变量的运算(大于、小于、不等于、加减等都是运算)规则:如果操作数中存在至少一个无符号数,则所有操作数都被转化为无符号数,举例:int a = -1;unsigned int b = 2;a / b;此时运算过程会自动把-1当作无符号数来对待(0xffffffff)那这个数就是一个非常大的正数了,然后做除法,得到的就不是-0.5了,这样就是错的。同理,如果都是有符号数,那这个运算就是正确的,比如int a = -1;int b = 2;a / b;这样就能得到正确结果,所以同符号型数做运算没有问题。
- C语言隐式转换规则简单来说就是先进行整型提升,再进行类型对齐。类型对齐时以size最大的类型为基准进行提升。
对任何一个混合运算表达式,如果表达式中没有比int型更高的类型,则所有参与运算的数值先转换成int型后在进行运算。类型提升的过程中不会发生任何精度损失。
现在我们来分析几个实际中遇到的例子:
例:unsgined int a=6;
int b=-20;
char c;
(a+b>6)?(c=1):(c=0); // a+b的过程就是进行了都变为unsigned int型运算的,导致负数的b出了问题
实际输出c=1;因为a+b,b先转换为unsigned int变为一个非常大的正数,所以a+b>6成立
例:
#include<iostream>
using namespace std;
int main()
{
unsigned uint = 10;
if(uint>-1) // 大于小于加减等等都是运算符,这里都自动转为了uint型比较,而-1的内存中为0xffffffff,当看出无符号型,这就是一个非常大的数,当然这里得判断返回就是false了
{
cout << "yes" << endl;
}
else
{
cout << "no" << endl;
}
return 0;
}
如上这段代码,比较一个无符号数 10 和一个负数 -1 ,最后的输出结果却是:
no
1
10 > -1 是很显然的事,但是在程序中无符号数和有符号的负数之间进行比较时却出现了问题。
注意:char在很多编译器中默认是有符号signed char类型,而在单片机keil中char默认是unsigned char类型!!!
例:在32位单片机中,char a=-1;if(a==-1),这里返回的是false,为什么呢,因为机器是32位(编译器也这样认为的)的,这里的常数-1就会被当作32位的数对待,即int -1,是0xffffffff(补码存在),而char a=-1的过程是把0xffffffff截断得到低8位为0XFF赋值给无符号型a,值为0xff,然后根据上面说的,现在是无符号和有符合两个数做运算,会转为无符号型,即扩充到大类型的无符号型,即unsigned int,a被扩为0x000000ff(因为a是无符号型,即机器当作它为正数,正数扩充是高位补0),此时0x000000ff和0xffffffff(-1的补码)比较,当然就是false了。但是当明确说明signed char a=-1,这时候比较就是true了,因为负数扩充前面是补1,所以就是相等了。
这里也说明一个问题,不同编译器会认为常数有不一样的类型对待,比如代码里的常数5,如果是32位单片机的编译器keil会认为是16位的short型的5,而一个更大的数99999可能keil又编译为32位的int型(或者unsigned int型)这都是编译器自动的而且都有可能的,而常数5在8位单片机可能就会认为是unsigned char 5,所以为了不产生歧义和便于移植,程序中用到常数都是直接在常数末尾加一个后缀表示,其中的u为unsigned,l为long,f为float,而浮点型常量后缀只有f或F,l或L,没有u或U,因为浮点数一般都为有符号。即常使用的有u,ul,ui。比如最常见的0u。习惯性会在大数后面加ul(注,这是好习惯,有利于平台移植,也能防止溢出)。
1、C语言中,常数分为整型和浮点型。
2、默认存储类型
整型:signed int
浮点型:double
注意:整形和浮点型的数是能直接比较大小的
再举一个我自己刚刚遇到的问题例子:
unsigned int a=2;
unsigned int b=5;
int c = a-b;
此时调试发现c并不等于-2,其实用上面的理论来分析,就是,a-b运算属于同类型同符号数的运算(会向上转类型,已经都是int型了,所以就不用再转了,而也都是同符号型,所以符号也不用转了),得出的结果也会用一个同类型同符号的变量(这个同类型同符号的unsigned int是无法存放-2的,所以造成溢出)来暂存,然后再赋值给c的,这样一个溢出的变量赋值给c就是错误的结果了。只有这样改一下即可int c = (int)a-(int)b;即先强制转换一下,而且两个都得强转为int,因为根据上面一条理论,混合符号运算会转为无符号型,这里两个都转为int型,那就是同有符号型的了,就能对了。
所以对于32位的cpu,定义变量最好就定义为int型,因为cpu总线宽度就是32位,一次内存读取就是读取到4个字节,这样直接就能得到需要的变量数据,如果定义成long long型8个字节,cpu就得访问内存读取两次才能得到需要的变量数据。如果为了节省内存,定义为char型,由于cpu一次读取4个字节,实际上这时候还需要截断得到低位才能得到需要的变量数据,也可能会造成性能损耗。所以内存够用情况下,32位cpu就一般定义为4字节的int型使用,cpu性能才是最高的,且由于代码中的常数也一般是int型,所以这样同类型同符号数之间的运算基本不会出那些隐藏的莫名其妙问题。
总结:把握好核心思想:混合符号型(至少有一个无符号型)数字运算,会自动转为无符号型。不同大小类型之间运算会提升为最大类型。
类型从小往大转,从大类型往小转,或者从小往大转(负数),或者从大类型往小转(负数),都是对的,符合我们的自然思维。
1。操作数全为有符号数,即使类型大小不一样,没有问题
2。操作数全为无符号数,即使类型大小不一样,没有问题
3。操作数混合了有符号数,无符号数,如果有正数有负数,很有可能出问题
所以写代码时候,一定要同符号类型(同时有符号还是无符号变量或者常数)进行运算,可以避免正负数问题带来的错误问题,这样可以减少这些隐形难以发现的错误。
同时最好同大小类型(同时int,char,double等),这样可以避免截断时候溢出等可能带来的错误。
非常值得一看的参考文章:
负数在计算机中如何表示,计算机中负数为什么用补码表示?
用“UL”避免Keil C51大整数常量运算溢出错误