接上面的博客,继续介绍数字表示
第二节 整数表示
在本节中,我们会介绍两种不同的整数表示方式:一种只能表示非负数,另一种既能表示正数,还能表示负数和0。后面我们会发现它们在数学属性和机器级实现方面的密切相关。我们还会研究扩展或者收缩一个已编码整数以适应不同长度表示的效果。
我们首先引入一些数学术语,用于精确定义和描述计算机如何编码和操作整数。在后面我们会使用这些数学术语,在这里列出让读者提前知会。
符号 | 含义 |
B2T | 二进制转换为补码 |
B2U | 二进制转换为无符号数 |
T2U | 补码转换为无符号数 |
U2T | 无符号数转换为补码 |
T2B | 补码转换为二进制 |
U2B | 无符号数转换为二进制 |
TMin | 补码最小值 |
TMax | 补码最大值 |
UMax | 无符号最大值 |
目录
-
整数数据类型
-
无符号数的编码
-
补码编码
-
有符号数和无符号数之间的转换
-
C语言中有符号数与无符号数
-
扩展一个数字的位表示
-
截断数字
-
关于有符号数和无符号的建议
-
小结
整数数据类型
整数类型表示有范围的整数,关键字有char、short、int、long,它们都有两种形式,一种是非负数,也就是无符号数;另一种是有符号数,就是包括非负数和负数。
下面给出32位机器上C语言整型数据类型的典型取值范围。
C数据类型 | 最小值 | 最大值 |
[signed] char | -128 | 127 |
unsigned char | 0 | 255 |
short | -32 768 | 32 767 |
unsigned short | 0 | 65 535 |
int | -2147 483 648 | 2147 483 647 |
unsigned int | 0 | 4294 967 295 |
long | -2147 483 648 | 2147 483 647 |
unsigned long | 0 | 4294 967 295 |
int32_t | -2147 483 648 | 2147 483 647 |
uint32_t | 0 | 4294 967 295 |
int64_t | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
uint64_t | 0 | 18 446 744 073 709 551 615 |
由于机器有32位和64位,由于机器位数的不同,整数类型的字节数也会不同, 整数类型的取值范围也会有所不同,在这里我就不赘述了64位机器各整数类型的取值范围了,可以看上一节的博客中的字数据的大小,自己计算。
在这里我们发现有符号整数负数的范围好像比正数大1,这是为什么呢?我们会在后面介绍原因。
无符号数的编码
假设有一个整数数据类型有w位,我们可以将位向量写成,表示整个向量,或者写成,表示向量中的每一位,把看作是一个二进制编码,每个位的取值都为1或0,在乘以当前位置上面的值。我们用来表示无符号数的编码。
无符号数的编码
我们发现,无符号数的最小值就为位向量[000...0],就是0,而无符号数的最大值用[111...1]来表示,因此,
例如。
另外,还有一个特性,介于之间的每一个数都会对应唯一的一个w值。
补码编码
在许多应用中,我们还是希望可以表示负数值。最常见的有符号数的表示都是使用补码编码形式。在这个定义中,将字的最高位解释为负权。我们用函数来表示。
补码编码
我们来考虑一下,补码的最小值是位向量[1000]=-8,其整数值为
补码的最大值是位向量[0111]=7,其整数值为
对于一个位于范围内的任意一个数都会对应唯一一个w值。
并且我们发现存在这样一个数学关系:
并且
UMax和-1有同样的表示方式
TMin并没有一个与之相对应的正数,这是因为0也是一个非负数,才导致TMax的值要比TMin的值小1。
为了方便程序的移植,我们建议读者可以多多使用intN或uintN这类值。N可以取8,16,32,64这类值。
有符号数和无符号数之间的转换
有符号数和无符号数的转换在二进制下是没有区别的,有区别的只是在十进制下关于数的读取方式,并且有符号数和无符号数的转换在十进制下的转换是有某种关系的。
将补码转换为无符号数
例如将补码[1010]转换为无符号数,补码[1010]=-6,无符号数[1010]=10,我们发现补码转换成无符号数只要将补码值加上
在举一个例子,补码[0111]转换为无符号数,补码[0111]=7,无符号数[0111]=7,这两者是相等的。
因此,我们总结出规律
将无符号数转换为补码
还是上面的两个例子,只不过我们现在发现转换一下。
例如将补码[1010]转换为无符号数,无符号数[1010]=10,补码[1010]=-6,我们发现无符号数转换补码成只要将补码值减去
在举一个例子,补码[0111]转换为无符号数,无符号数[0111]=7,补码[0111]=7,这两者是相等的。
我们总结规律
那么,TMax的值都为多少呢,下面是列出在不同位数下TMax的十进制值,如果使用十进制表达补码时,我们能尽快判断是否应该进行转换。
数 | 字长w | |||
8 | 16 | 32 | 64 | |
UMax | 0xFF | 0xFFFF | 0xFFFF FFFF | 0xFFFF FFFF FFFF FFFF |
255 | 65 535 | 4 294 967 295 | 18 446 744 073 709 551 615 | |
TMin | 0x80 | 0x8000 | 0x8000 0000 | 0x8000 0000 0000 0000 |
-128 | -32 768 | -2 147 483 648 | -9 223 372 036 854 775 808 | |
TMax | 0x7F | 0x7FFF | 0x7FFF FFFF | 0x7FFF FFFF FFFF FFFF |
-128 | 32 767 | 2 147 483 647 | 9 223 372 036 854 775 807 |
其实,无符号数和补码的相互转换,最重要的一个分界点就是最高位值是否为1。
对于区间[0,TMax]之内的数,补码和无符号数是相等的。
C语言中有符号数与无符号数
C语言中默认数是有符号的,比如1234或者0x1234,都会被默认为是有符号的,而如果你想声明一个无符号数,你需要加上后缀“U”或者“u”。例如1234u或者0x1234U。
C语言是允许有符号数和无符号数相互转换的,遵从的规则就是我们刚刚介绍过得T2U和U2T的规则。
但是C语言中如果有符号数和无符号数进行运算时,系统会默认将有符号数转换成无符号数来进行运算。因此,有些时候有些运算的结果会和我们一般的预期不一致。例如,在32位机器上面,short类型的-1<0答案肯定是true,但是如果-1<0U,这时答案就是false了。因为会将有符号数-1转换为无符号数65535和无符号数0进行比较,结果会是false。
扩展一个数字的位表示
将一个较小的类型转变为一个较大的类型。
关于无符号数的扩展,我们直接在前面补0就可以。例如,我们在32位机子上面,将一个short类型的无符号数0x1234转变为int,我们直接进行扩展就可以,改为0x0000 1234即可。
关于有符号数的扩展,我们在前面补最高位的值即可。例如,我们在32位机子上面,将一个short类型的无符号数0xcfc7转变为int,我们直接进行扩展就可以,改为0xffff cfc7即可。
如果要把short转换为unsigned类型,我们要先改变大小,再将有符号类型转换为无符号类型。
比如在32位机子上面,有一个数-12345是short类型,我们要把它转换为unsigned类型,怎么转换呢,-12345用16进制表示0xcfc7。
short类型是2字节,unsigned类型是4字节,我们需要首先将short类型转换成4字节,也就是扩展位表示,变成0xffff cfc7,再将这些有符号的数转换成无符号的数就可以了。最后-12345的值会变为-12345+2^32=-12345+4 294 967 296=4,294,954,951
截断数字
无符号数截断数字没有什么特别的,直接截断就可以。
有符号数截断数字需要将阶段后最高位的数值,转换成负数。比如说int类型的53191将它截断成short类型,截断后,我们会发现53191转换成short类型的数,由于最高位是负数,那么,重新去看这个数应该是-12345。这也是我们再有符号数截断时应该注意的问题。
关于有符号数和无符号的建议
我们一般是不建议使用无符号数的,因为它可能会导致一些错误。
比如说2002年的getpeername的漏洞,一开始使用的是一个有符号的数,但是后来把这个有符号数又转换成一个无符号数,如果是非负数还无妨,但如果一旦有恶意的程序员输入一个负数,就会导致读到没有被授权的内核内存区域。
所以一般都不建议使用无符号数。但是无符号数也有自己的用处。比如说地址,还有模运算和多精度运算的数据包。
小结
这一节我们主要介绍了整数表示,有哪些整数,取值范围是多少,无符号编码和补码编码的原理,以及两者如何互相转换,C语言中的两者有什么需要注意的——无符号数和补码进行比较,结果可能会和平时的认知不一致,并且C语言转换的底层原理就是用我们前面介绍的B2U,B2T,T2U,U2T等。还有扩大,截断位表示,比如int和short的互相转换。最后提出的一些建议,建议少使用无符号数,只在特定的场景下使用即可。