袁春风的书讲的稀里糊涂。确实直接看CSAPP就很好。

整数的大类有两种,一种只能表示非负数,一种可以表示负数,零和正数。虽然通常叫无符号数,但是CSAPP的这个讲法显然更容易懂。

无符号数的编码

无符号数的编码,等于每位二进制数乘以总权重,最右边一位是2的0次方,然后是2的1次方。依次类推。

无符号数是一个双向映射,即每个无符号数可以映射到唯一一个二进制数,反过来也成立。

补码编码

CSAPP这个讲的真是棒。没有讲什么取反+1,而是用了最高位的权重为负数来解释。

1101如果按4位字长来解释,无符号数的话,就是1*8+1*4+0*2+1 = 13,而如果按照补码表示,第一位如果是0,就正常计算,如果是1,要乘以-1。

1101按补码解释,就是-1*8+4*1+0*2+1 = -3。最高位的权重8,就是模。

补码也是双向映射,是唯一的。

2.17 4位字长的无符号和补码表示

十六进制 二进制 无符号数 补码表示
0xE 1110 14 -2
0x0 0000 0 0
0x5 0101 5 5
0x8 1000 8 -8
D 1101 13 -3
0xF 1111 15 -1

有一些特别字长的数字要记住,这些可以在C语言的<limits.h>里找到。

2.18 十六进制转补码

  1. 0x2e0 = 2*16*16+14*16 = 736
  2. -0x58 = -88
  3. 0x28 = 40
  4. -0x30 = -48
  5. 0x78 = 120
  6. 0x88 = 136
  7. 0x1f8 = 504
  8. 0xc0 = 192
  9. -0x48 = -72

有符号数和无符号数的转换

C语言里,强制转换有符号和无符号数,底层表示不变,只是解释变了。

int main()
{

    short int v = -12345;
    unsigned short uv = (unsigned short) v;

    printf("u = %d, uv=%u\n", v, uv);

}

大多数C语言的实现里,同样字长的无符号和有符号数转换,显示数值会改变,但二进制位不会改变。

补码和无符号数的关系是1+无符号w字长的最大值 = 2的w次方

2.19 将补码转换成无符号数之后的实际大小

按照前边的转换规则,二进制数相同,只是解释不同。

x T2U4(x) 补码转无符号数
-8 8
-3 13
-2 14
-1 15
0 0
5 5

补码的转换成无符号数,实际上最高位的权重会发生变化,从-2的n-1次方变成2的n-1方,实际就是加上2的n次方,所以又验证了模是2的n次方。

C语言中有符号数和无符号数字转换的时候,底层二进制位不会发生变动,仅是解释相同。这发生在单独使用的情况下。

如果同时使用有符号数和无符号数,C会隐式的将有符号数转换成无符号数(即解释成无符号数),然后再参与运算,这在进行比较运算的时候要注意,很可能得到意外的结果:

2.21 无符号数与有符号数的比较

表达式 类型 求值
-2147483647-1 == 2147483648U 无符号 转换成无符号的时候,左边实际上是1000….右边也是100….,按照无符号数比较会得到相等,所以值是1
-2147483647-1 < 2147483647 有符号 左边是10000,右边是正常int范围内的正数,所以值是1
-2147483647-1U < 2147483647 无符号 左边的是无符号数 1000….,右边的是FFFFFFFF,左边大于右边,所以值是0
-2147483647-1 < -2147483647 有符号 左右两边都可以被正常的int表示,所以表达式值是1
-2147483647-1U < -2147483647 无符号 左边的是0x80000000,右边的int表示是0x80000001,按照无符号数比较就是小于,所以值是1

数的扩展

数的扩展分为两部分,一是无符号数扩展,二是补码扩展

无符号数的扩展在左边补0。

有符号数的扩展在左边补符号位。

补符号位的原理是,补的最高位权重是负数,而原来的次高位变成正数,二者的差始终是原来的最高位的权重,所以补符号位不会影响按照新字长对数的解释和运算。

int main()
{
    short int v = -12345;

    show_bytes((byte_pointer) &v, sizeof(short));

    unsigned short uv = v;
    show_bytes((byte_pointer) &uv, sizeof(unsigned short));

    int intv = (int) v;

    show_bytes((byte_pointer) &intv, sizeof(int));

    unsigned int uintv = (unsigned int) v;

    show_bytes((byte_pointer) &uintv, sizeof(unsigned int));

    unsigned int uintv2 = (unsigned int) uv;

    show_bytes((byte_pointer) &uintv2, sizeof(unsigned int));
}

结果依次是:

c7 cf
c7 cf
c7 cf ff ff
c7 cf ff ff
c7 cf 00 00

这里尤其要注意第四个结果和第五个结果。第四个结果并不是直接将Cfc7当做无符号数前边补0,而是先将其转成了正常的int,即前边补符号位,再转换成unsigned解释。

而对于原来就是unsigned的类型,则不再转换成正常的int,直接在高位补0。

即无符号数扩展成无符号数的过程中才会补0。如果有符号和无符号之间的扩展,会先进行类型转换。

2.22 扩展有符号数的转换

  1. 1011,4位字长补码来解释,就是-1*8+2+1 = = -5
  2. 11011,5位字长补码来解释,相比4位字长,等于-1*16+8+剩余3位,其实还是等于-8加上剩余3位,所以还是-5
  3. 111011,6位字长补码来解释,相比5位字长,等于-1*32+16+8,其实还是等于-8加上剩余3位,所以还是-5

可以看到从1011开始,按照字长解释往高位添加任意多的1,只要字长也同步变动,则对数字的解释不变,依然是原来的补码数字。

2.23 移位运算

有符号算术右移,无符号算术左移。

//第一个函数是先左移24位也就是6个十六进制数,然后在右移24位,最后转成int。
int fun1(unsigned word){
    return (int) ((word << 24) >> 24);
}

//第二个函数是先左移24位,转成int,再右移24位
int fun2(unsigned word){
    return ((int) word << 24) >> 24;
}

x fun1(x) fun2(x)
0x00000076 左移24位也就是6个十六进制的0,得到0x76000000,再右移回去,由于还是无符号,高位补0,结果依然是0x00000076 左移得到0x76000000,转换成int,由于最高位不是1,所以是一个正数,再移回去,最高位补0,结果还是0x00000076
0x87654321 左移得到0x21000000,右移得到0x00000021 左移得到0x21000000,转换成int是正数,右移得到0x00000021
0x000000C9 左移得到0xC9000000,右移得到0x000000C9 左移得到0xC9000000,此时最高位是1,即11001001…所以移位要补符号位,结果是0xFFFFFFC9
0xEDCBA987 左移再右移之后只剩2位,0x00000087 左移之后是0x87000000,转换成int最高位1,补符号,结果是0xFFFFFF87.

用语言来描述函数1的作用:将除了最低两位以外的位置0,或者说提取这个数最低字节组成的无符号数。函数2的作用:将除了最低两位以外的位置1,或者说提取一个数的最低字节组成的补码数字。

截断数字

截断的原理是:直接将位数截断,不考虑符号位对于截断以后数字的影响。

之后再按照新的数据类型重新解释被截断之后的数据。

int main()
{
    // 0000CFC7
    int x = 53191;

    show_bytes((byte_pointer) &x, sizeof(int));

    short sx = (short) x;
    //截断之后是CFC7,按照补码是一个负数
    show_bytes((byte_pointer) &sx, sizeof(short));

    printf("sx is %d\n", sx);

    //再扩回int并不是原来的数字,因为是有符号数,最高位要补符号位
    int is_x = (int) sx;

    printf("new x is %d\n", is_x);
}

2.24 无符号截断和有符号截断的转换

无符号 补码
原始值(十进制) 截断值(十进制) 原始值(十进制) 截断值(十进制)
0 0 0 0
2 2 2 2
9 1 -7 1
11 3 -5 3
15 7 -1 -1

-1截断的时候注意,字长此时为3,结果还是-1。

2.25 找程序中的错误

float sum_elements(float a[], unsigned length){
    int i;
    float result = 0;

    for (i = 0; i <= length - 1;i++) {
        result += a[i];
    }
    return result;
}

这段程序的核心问题在于 i<= length -1

由于length是无符号数,所有的都要变成无符号数来比较。-1的无符号数是0xFFFFFFFF,而i无论如何也不会比-1大,所以程序会一直往后不断读内存,直到越界出现错误。

2.26 判断字符串长度函数中的问题

int strlonger(char *s, char *t){
    return strlen(s) - strlen(t) > 0;
}

strlen的返回值是size_t类型,在我的MingGW编译器里,是typedef unsigned __int64 size_t,即一个64位的无符号整数。

两个无符号数做过运算之后,再与int进行比较,会将0作为无符号数进行比较,则所有的有符号数都会比大于等于0。所以这个函数在两个字符串长度不相等的时候,都返回1。

int main()
{
    char *a = "fdsafdsaf";
    char *b = "fdsfdsa";
    char *c = "dskjlso";

    printf("%d\n", strlonger(a, b));
    printf("%d\n", strlonger(b, a));
    printf("%d\n", strlonger(b, c));

}

第一行和第二行都返回1,所以这个函数实际上无法判断字符串长度不相等的两个字符串的长度。修改方法也很简单,换成两个无符号数比较就可以了:

int strlonger(char *s, char *t){
    return strlen(s) > strlen(t);
}