【CSAPP】整数表示


使用 位编码整数有两种不同的方式:

  1. 只能表示非负数(零、正数),即无符号数的编码
  2. 既能表示非负数,也能表示负数,即有符号数的编码补码编码

下表是一些数学术语,用于精确定义和描述计算机如何编码操作整数。

符号类型含义
B 2 T w B2T_w B2Tw函数二进制转补码
B 2 U w B2U_w B2Uw函数二进制转无符号数
U 2 B w U2B_w U2Bw函数无符号数转二进制
U 2 T w U2T_w U2Tw函数无符号数转补码
T 2 B w T2B_w T2Bw函数补码转二进制
T 2 U w T2U_w T2Uw函数补码转无符号数
T M i n w TMin_w TMinw常数最小补码值
T M a x w TMax_w TMaxw常数最大补码值
U M a x w UMax_w UMaxw常数最大无符号数
+ w t +^{t}_{w} +wt操作补码加法
+ w u +^{u}_{w} +wu操作无符号数加法
∗ w t *^{t}_{w} wt操作补码乘法
∗ w u *^{u}_{w} wu操作无符号数乘法
− w t -^{t}_{w} wt操作补码取反
− w u -^{u}_{w} wu操作无符号数取反

下标w表示整数的位数。

整型数据类型

C语言支持多种整型数据类型——表示有限范围的整数。每种类型都可以用关键字指定大小,如charshortlong,同时还可以用unsigned表示是无符号的。
无符号数永远是>=0的。有符号数既能表示负数,又能表示非负数,在“典型”实现中,负数的表示范围比非负数大1(不对称)。

C语言标准定义了每种数据类型必须能够表示的最小取值范围。对有些数据类型则要求正数和负数的取值范围是对称的。
C/C++支持无符号数和有符号数,Java只支持有符号数。

无符号数的编码

假设一个整数有w位,其编码为 [ x w − 1 , x w − 2 , . . . , x 0 ] [x_{w-1}, x_{w-2}, ... , x_0] [xw1,xw2,...,x0],写作位向量 x ⃗ \vec x x 。在这个编码中,每个位 x i x_i xi取值为01

原理:无符号数的编码定义。
对向量 x ⃗ = [ x w − 1 , x w − 2 , . . . , x 0 ] \vec x = [x_{w-1}, x_{w-2}, ... , x_0] x =[xw1,xw2,...,x0],有
B 2 U w ( x ⃗ ) = ˙ ∑ i = 0 w − 1 x i 2 i \begin{align} B2U_w(\vec x) \.=\displaystyle\sum_{i=0}^{w-1}x_i2^i \end{align} B2Uw(x )=˙i=0w1xi2i

= ˙ \.= =˙表示左边被定义为等于右边。

函数 B 2 U w ( x ⃗ ) B2U_w(\vec x) B2Uw(x )将一个长度为w0、1串映射为非负整数,函数的返回值就是该编码的无符号数值。

x ⃗ \vec x x 所能表示的最小值为[0, 0, ..., 0]w0,该编码对应的无符号数值为0;最大值是[1, 1, ..., 1]w1,该编码对应的无符号数值为 U M a x w = ˙ B 2 U w ( [ 1 , 1 , . . . , 1 ] ) = ∑ i = 0 i = w − 1 2 i = 2 w − 1 UMax_w \.= B2U_w([1, 1, ..., 1]) = \displaystyle\sum_{i=0}^{i=w-1}2^{i} = 2^w - 1 UMaxw=˙B2Uw([1,1,...,1])=i=0i=w12i=2w1
因此,函数 B 2 U w B2U_w B2Uw能够被定义为一个映射: { 0 , 1 } w → { 0 , 1 , 2 , . . . , 2 w − 1 } \{0, 1\}^w \to \{0, 1, 2, ..., 2^{w}-1\} {0,1}w{0,1,2,...,2w1}

原理:无符号数编码是唯一的。
函数 B 2 U w B2U_w B2Uw是一个双射。
对函数 B 2 U w ( x ⃗ ) B2U_w(\vec x) B2Uw(x )而言,给定一个位向量 x ⃗ \vec x x ,有唯一对应的无符号数值。
对函数 U 2 B w ( y ) U2B_w(y) U2Bw(y)而言,给定一个无符号数值 y y y,有唯一对应的位向量 x ⃗ \vec x x

补码编码

最常见的有符号数的计算机表示方法是补码编码。在这个定义中,将最高有效位解释为权。
原理:补码编码定义。
对向量 x ⃗ = [ x w − 1 , x w − 2 , . . . , x 0 ] \vec x = [x_{w-1}, x_{w-2}, ... , x_0] x =[xw1,xw2,...,x0],有
B 2 T w ( x ⃗ ) = ˙ − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i \begin{align} B2T_w(\vec x) \.=-x_{w-1}2^{w-1} + \displaystyle\sum_{i=0}^{w-2}x_i2^i \end{align} B2Tw(x )=˙xw12w1+i=0w2xi2i
最高有效位 x w − 1 x_{w-1} xw1也称为符号位,它的权重为 − 2 w − 1 -2^{w-1} 2w1 x w − 1 x_{w-1} xw11时,该值是负数;为0时,该值是非负数。

x ⃗ \vec x x 所能表示的最小值为[1, 0, ..., 0],值为 T M i n w = ˙ − 2 w − 1 TMin_w\.=-2^{w-1} TMinw=˙2w1;最大值为[0, 1, ..., 1],值为 T M a x w = 2 w − 1 − 1 TMax_w=2^{w-1}-1 TMaxw=2w11
因此,函数 B 2 T w B2T_w B2Tw能够被定义为一个映射: { 0 , 1 } w → { − 2 w − 1 , − 2 w − 1 + 1 , . . . , 2 w − 1 − 2 , 2 w − 1 − 1 } \{0, 1\}^w \to \{-2^{w-1}, -2^{w-1} + 1, ..., 2^{w-1}-2, 2^{w-1}-1\} {0,1}w{2w1,2w1+1,...,2w12,2w11}

原理:补码编码是唯一的。
函数 B 2 T w B2T_w B2Tw是一个双射。
对函数 B 2 T w ( x ⃗ ) B2T_w(\vec x) B2Tw(x )而言,给定一个位向量 x ⃗ \vec x x ,有唯一对应的有符号数值。
对函数 T 2 B w ( y ) T2B_w(y) T2Bw(y)而言,给定一个有符号数值 y y y,有唯一对应的位向量 x ⃗ \vec x x

相同的位向量 x ⃗ \vec x x 在不同的语境下有不同的含义。
如果 x ⃗ \vec x x [1, 1, ..., 1]的串,无符号数值语境下表示UMax,有符号数值语境下表示-1
如果 x ⃗ \vec x x [1, 0, ..., 0]的串,无符号数值下表示正数 2 w − 1 2^{w-1} 2w1,有符号数值下表示负数 − 2 w − 1 -2^{w-1} 2w1
最高位为0的情况下,无符号数值和有符号数值相等。

C语言标准并没有要求采用补码形式表示有符号数,但几乎所有的机器都是这么做的。如果程序员希望代码具有最大的可移植性,那么就该假定有符号数是采用补码编码的

补码:对于非负数x,用 2 w − x 2^w - x 2wx的值表示-x
反码:对于非负数x,用 2 w − 1 − x 2^w - 1 - x 2w1x的值表示-x

确定大小的整数类型

ISO C99标准定义了确定大小的整数类型。如int32_tuint64_t。无论在哪台机器上,它们都分别表示32位的有符号数和64位的无符号数。标准也定义了诸如INT32_MAXINT32_MINUINT64_MAX这样的宏,表示对应整数类型值的范围。标准也定义了诸如PRId32PRIu64这样的宏,表示格式化打印相应的整型变量时的宽度。

相比于C/C++Java标准定义更明确一些:仅支持有符号数、整型的表示范围确定、采用补码编码。

练习1

位向量 x ⃗ \vec x x 的长度w = 4,填写下表。

位向量 xB2UB2T
十六进制二进制
0xE[1110]14-2
0x0[0000]00
0x5[0101]55
0x8[1000]8-8
0xD[1101]13-3
0xF[1111]15-1

练习2

将下列使用32补码表示的16进制数转换成等价的10进制数。

  1. 0x2e0
    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

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

相同位宽的有符号数和无符号数之间强制类型转换的结果是底层位值不变,但改变了解释这些位的方式。比如把short类型的-12345转换成unsigned short会得到53191,事实上这两个数的底层位值是一模一样的。再比如把UINT_MAX强转为有符号数会得到-1UINT_MAX-1的底层位值都是[1, 1, ..., 1]
因此,我们可以通过分析二进制表示,完成有符号数和无符号数之间的转换。

原理:从符号数转符号数。
对于位宽为w的有符号数 x x x,二进制表示为位向量 x ⃗ \vec x x ,满足 T M i n w < = x < = T M a x w TMin_w <=x<= TMax_w TMinw<=x<=TMaxw,有:
T 2 U w ( x ) = ˙ B 2 U w ( T 2 B w ( x ) ) = x + x w − 1 2 w = { x + 2 w , x < 0 x , x > = 0 \begin{align} T2U_w(x)\.=B2U_w(T2B_w(x))= x+x_{w-1}2^w= \begin{cases} x+2^w, \quad &x<0 \\ x, \quad &x>=0 \end{cases} \end{align} T2Uw(x)=˙B2Uw(T2Bw(x))=x+xw12w={x+2w,x,x<0x>=0

有符号数的最高位权值是 − 2 w − 1 -2^{w-1} 2w1,无符号数的最高位权值是 2 w − 1 2^{w-1} 2w1,两者相差 2 w 2^{w} 2w。所以当最高位为1 x < 0 x<0 x<0)时,无符号数值比有符号数值大 2 w 2^{w} 2w

原理:从符号数转符号数。
对于位宽为w的无符号数 x x x,二进制表示为位向量 x ⃗ \vec x x ,满足 0 < = x < = U M a x w 0<=x<= UMax_w 0<=x<=UMaxw,有:
U 2 T w ( x ) = ˙ B 2 T w ( U 2 B w ( x ) ) = x − x w − 1 2 w = { x , x < = T M a x w x − 2 w , x > T M a x w \begin{align} U2T_w(x)\.=B2T_w(U2B_w(x))=x-x_{w-1}2^w= \begin{cases} x, \quad &x<=TMax_w \\ x-2^w, \quad &x>TMax_w \end{cases} \end{align} U2Tw(x)=˙B2Tw(U2Bw(x))=xxw12w={x,x2w,x<=TMaxwx>TMaxw

当最高位为1 x > T M a x w x>TMax_w x>TMaxw)时,有符号数比无符号数小 2 w 2^w 2w

练习

补充下面的表格。

x x x T 2 U 4 ( x ) T2U_4(x) T2U4(x)
-88
-313
-214
-115
00
55

C语言中的有符号数与无符号数

C语言中,声明一个常量时,默认是有符号的,如1230xAB。要创建无符号常量,需要加上后缀字符uU,如123u0xABu

可以显示转换,如:

int i;
unsigned int u;

i = (int)u;
u = (unsigned int)i;

可以隐式转换,如:

int i;
unsigned int u;

i = u;
u = i;

int i;
unsigned int u;

printf("%u\n", i);
printf("%d\n", u);

当执行一个运算时,如果它的一个运算数是有符号的,另一个运算数是无符号的,那么C语言会隐式地将有符号参数转换为无符号数,再进行无符号数之间的运算。

练习

假设在采取补码运算的32位机器上执行以下运算,填写运算的类型和结果。

表达式类型求值
-2147483647-1 == 2147483648U无符号1
-2147483647-1 < 2147483647有符号1
-2147483647-1U < 2147483647无符号0
-2147483647-1 < -2147483647有符号1
-2147483647-1U < -2147483647无符号1

扩展一个数字的位表示

要将一个无符号整数转换为一个更大的数据类型,只需要简单地在高位加0,这种运算被称为无符号数的零扩展
要将一个有符号整数转换为一个更大地数据类型,需要在高位加符号位,这种运算被称为补码的符号扩展

当数据扩展和有无符号转换同时出现时,C标准要求先扩展、后进行有无符号转换

short sx = -12345;
unsigned int uy = sx;  // 等价于unsigned int uy = (unsigned int)(int)sx;
// uy的十六进制表示为0xFFFFCFC7

如果先做符号转换、再扩展,那uy的十六进制表示为0x0000CFC7,就无法正确表示原值sx的符号位。

练习1

下面各个补码对应的有符号数值是多少?

  1. [1011]
    − 2 3 + 2 1 + 2 0 = − 5 -2^3+2^1+2^0=-5 23+21+20=5
  2. [11011]
    − 2 4 + 2 3 + 2 1 + 2 0 = − 5 -2^4+2^3+2^1+2^0=-5 24+23+21+20=5
  3. [111011]
    − 2 5 + 2 4 + 2 3 + 2 1 + 2 0 = − 5 -2^5+2^4+2^3+2^1+2^0=-5 25+24+23+21+20=5

后两个有符号数分别是第一个有符号数符号扩展1位和2位的结果。符号扩展不会改变补码表示的数值大小。

练习2

考虑下面的C函数:

int fun1(unsigned word)
{
	return (int)((word << 24) >> 24);
}

int fun2(unsigned word)
{
	return ((int)word << 24) >> 24;
}

填写下表。

w w w f u n 1 ( w ) fun1(w) fun1(w) f u n 2 ( w ) fun2(w) fun2(w)
0x000000760x000000760x00000076
0x876543210x000000210x00000021
0x000000C90x000000C90xFFFFFFC9
0xEDCBA9870x000000870xFFFFFF87

截断数字

当将一个w位的数截断为一个k位的数时,会丢弃高w-k位,并对低k位做新的解释。截断一个数可能会改变它的值(即溢出)。

练习

将一个4位数值截断为一个3位数值,填写下表。

十六进制无符号(十进制)补码(十进制)
原始值截断值原始值截断值原始值截断值
0x00x00000
0x20x22222
0x90x191-71
0xB0x3113-53
0xF0x7157-1-1

关于有符号数与无符号数的建议

整数的扩展和截断、有符号数和无符号数之间的隐式转换,会导致程序错误或漏洞。而这些不符合预期的行为有时很难被程序员发现。避免这些错误的方法有:

  1. 整数运算时,统一符号。
  2. 如果涉及到整数类型的变化,使用强制转换代替隐式转换,方便程序员发现问题。

练习1

考虑下面的代码,当length等于0时,预期结果是0.0。但实际上,运行时会造成一个内存错误。为什么?该如何修改代码?

float sum_elements(float a[], unsigned length)
{
	float result = 0;
	for (int i = 0; i <= length - 1; ++i) {
		result += a[i];
	}
	return result;
}

length是无符号数,i是有符号数,程序执行i <= length - 1时,程序会按无符号数执行该运算。
length = 0时,在无符号数的语境下,length - 1等于UINT_MAXi的值永远小于UINT_MAX,所以程序会访问未知内存,导致运行时错误。

解决办法是:

  1. length的类型改为int。当length = 0时,在有符号数的语境下,length - 1等于-1i的值是0,程序立即返回0.0
  2. i <= length - 1改为i < length

练习2

下面的代码用来判断一个字符串是否比另一个更长。在头文件stdio.h中,size_t无符号整型

// 库函数
size_t strlen(const char *s);

// 用户代码
int strlonger(char *s, char *t)
{
	return strlen(s) - strlen(t) > 0;
}
  1. 在什么情况下,这个函数会产生不正确的结果?为什么?
    如果strlen(s)小于strlen(t)strlen(s) - strlen(t)的预期结果应该是个负数, strlen(s) - strlen(t) > 0的预期结果应该是0
    strlen(s)strlen(t)的返回值都是无符号数strlen(s) - strlen(t)的结果也是无符号数,无符号数一定是>=0的。因此程序会返回1,而不是0,和预期不符。
  2. 如何修改代码,使其正确。
    return strlen(s) - strlen(t) > 0;改为return strlen(s) > strlen(t);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值