【CSAPP笔记】第二章 信息的表示和处理

319 阅读9分钟

2 信息的表示和处理

2.1 信息存储

  • 字节(byte:计算机使用8位的块,作为最小的可寻址内存单位(00000000211111111200000000_2 \sim 11111111_2
  • 虚拟内存(virtual memory:程序将内存视为非常大的字节数组
  • 地址(address:内存的每个字节由唯一的数字标识
  • 虚拟地址空间(virtual address space:所有地址的集合

2.1.1 十六进制表示法

为了方便表示一个字节,通常用16进制表示。可以很容易地互相转换二进制和十六进制数(逐位转换):

【例】16进制数字0x173A4C,转为二进制:0001 0111 0011 1010 0100 1100

十六进制173A4C
二进制000101110011101001001100

2.1.2 字数据大小

  • 字长(word size:指明虚拟地址空间的范围。如:计算机字长为64位,则表示虚拟地址范围是026410 \sim 2^{64}-1,也就是程序最多访问2642^{64}个字节
  • C语言中,不同数据类型在32位机器与64位机器所占字节数的大小不同
有符号类型无符号类型32位字节数64位字节数
signed charunsigned char11
shortunsigned short22
intunsigned int44
longunsigned long48
int32_tuint32_t44
int64_tuint64_t88
char48
float44
double88

2.1.3 寻址和字节顺序

多字节对象的存储方式分为两种:

  • 小端法(little endian):最低有效字节在最前面(大多数Intel的机器只使用小端)
  • 大端法(big endian):最高有效字节在最前面(大多数IBM和Sun的机器采用大端)

【例】数字0x01234567,在内存中存放的方式

image.png

【例】打印字节序(Linux64位机器)

#include <stdio.h>

typedef unsigned char* byte_pointer;

void show_bytes(byte_pointer start, size_t len)
{
    size_t i;
    for (i = 0; i < len; i++) {
        printf("%.2x ", start[i]);
    }
    printf("\n");
}

int main(int argc, char* argv[])
{
    int num = 0x01234567;
    show_bytes((byte_pointer) &num, sizeof(int));
    return 0;
}

【输出】小端序

67 45 23 01

2.1.4 表示字符串

C语言字符串:以'0x0'字符结尾的字符串数组

【例】"abcd"在内存中记录为:61 62 63 64 00

2.1.5 表示代码

从机器角度看,程序仅仅是字节序列。不同的操作系统上将程序编译为不兼容的机器代码:

image.png

2.1.6 布尔代数、位运算

  • 逻辑值:真(TRUE)、假(FALSE)
  • 运算:非、与、或、异或

image.png

2.1.7 C语言中的位级运算

使用|代表OR&代表AND~代表NOT^代表XOR

image.png

【例】两数交换

#include <stdio.h>

void swap(int *x, int *y)
{
    *y = *x ^ *y;
    *x = *x ^ *y;
    *y = *x ^ *y;
}

int main(int argc, char* argv[])
{
    int x = 2, y = 3;
    swap(&x, &y);
    printf("x=%d, y=%d\n", x, y);
    return 0;
}

2.1.8 C语言中的逻辑运算

逻辑运算符:或(||)、与(&&)、非(!),结果返回TRUEFALSE

image.png

【注】C语言逻辑运算有短路效应

/* 这里a=0时,不会执行1/a */
if (a && 1/a)  printf("%d\n", a);

2.1.9 C语言中的移位运算

移位运算符:<<>>。其中左移都是末尾补0。右移注意区分逻辑右移算数右移

  • 逻辑右移:首位补0
  • 算数右移:如果首位是0补0;如果首位是1补1

image.png

【注】C语言中,有符号数是算数右移,无符号数是逻辑右移

#include <stdio.h>

int main(int argc, char* argv[])
{
    unsigned int x = 2, y = -2;
    printf("x=%d, y=%d\n", x>>1, y>>1);
    return 0;
}

【输出】

x=1, y=2147483647

【注】Java中使用>>代表算数右移,>>>代表逻辑右移

2.2 整数表示

2.2.1 整型数据类型

  • 32位机器上整型的表示:

image.png

  • 64位机器上整型的表示:

image.png

2.2.2 无符号数的编码

无符号数范围:02w10\sim2^w-1。这里书中解释的严谨但晦涩,用例子说明:

【例】以字长w=4为例,无符号数11,编码为1011:

[1011]2=123+022+121+120=11[1011]_2=1*2^3+0*2^2+1*2^1+1*2^0=11

2.2.3 有符号数的编码

有符号数范围:2w12w11-2^{w-1}\sim2^{w-1}-1,几乎所有现代机器使用补码方式编码。

  • 补码:编码最高位为1时,表示2w1-2^{w-1}。比如当字长为4,编码最高位为1,代表-8

【例】以字长w=4为例,有符号数5,编码为0101;有符号数-5,编码为1011

[0101]2=023+122+021+120=5[0101]_2=0*2^3+1*2^2+0*2^1+1*2^0=5

[1011]2=123+022+121+120=5[1011]_2=-1*2^3+0*2^2+1*2^1+1*2^0=-5

[1111]2=123+122+121+120=1[1111]_2=-1*2^3+1*2^2+1*2^1+1*2^0=-1

【补充】有符号数的其它表示

  • 反码:最高有效位的权是2w11-2^{w-1}-1。好处是比较直观:5[1101]2-5\to[1101]_2;缺点:数值0有两种表示方法
    • +0[0000]2+0\to[0000]_20[1111]2-0\to[1111]_2
  • 原码:最高有效位是符号位,剩下的位确认数值大小。优缺点类似反码
    • +0[0000]2+0\to[0000]_20[1000]2-0\to[1000]_2

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

C语言中可以将无符号数和有符号数互相转换,转换规则:位不变,解释方式改变

  • 有符号数转无符号数
T2Uw(x)={x+2w,x<0x,x0T2U_w(x)=\left\{ \begin{array}{ll} x+2^w, & x<0\\ x, & x\ge0 \end{array} \right.
  • 无符号数转有符号数
U2Tw(x)={u,xTMaxwu2w,u>TMaxwU2T_w(x)=\left\{ \begin{array}{ll} u, & x\le TMax_w\\ u-2^w, & u>TMax_w \end{array} \right.

【例】有符号数-12345转换为无符号数53191

12345[1100 1111 1100 0111]253191-12345\to[1100\ 1111\ 1100\ 0111]_2 \to 53191,即:12345+65536=53191-12345+65536=53191

2.2.5 C语言中的有符号数和无符号数

C语言中,默认使用有符号数。创建无符号数时,需加上后缀'U'

有符号数和无符号数在以下情况发生转换:

  • 显示强制转换:
int tx, ty;
unsigned ux, uy;

tx = (int) ux;
uy = (unsigned) ty;
  • printf输出,隐式转换
int x = -1;
printf("x = %u\n", x);
  • 如果是 有符号数 <运算> 无符号数,则会将有符号数隐式转换为无符号数
    • 有符号+无符号\to无符号
    • 关系运算符

image.png

【注】由于C存在无符号到有符号的隐式转换,容易出错,需特别小心。有些语言如Java语言没有无符号数,规避了这个问题

2.2.6 扩展数据类型

  • 较小的数据类型\to较大的数据类型
    • 无符号数:补零即可(也称零扩展,zero extension)
    • 有符号数:如果首位是0补0;如果首位是1补1

【例】有符号数的扩展

5=[0101]2[0000 0101]25=[0101]_2 \to [0000\ 0101]_2

3=[1101]2[1111 1101]2-3=[1101]_2 \to [1111\ 1101]_2

2.2.7 截断数据类型

  • 较大的数据类型=>较小的数据类型
    • 无符号数:截断为k位的数字,相当于2k2^k取模:x=x mod 2kx'=x\ mod\ 2^k
    • 有符号数:无符号数截断,然后将最高位转换为符号位:x=U2Tk(x mod 2k)x'=U2T_k(x\ mod\ 2^k)

【例】有符号数的截断

unsigned int x = 53191;
printf("x=%d\n", (short)x);

【输出】-12345

【说明】short范围:-32768~32767,所以必丢精度

53191=[0000 0000 0000 0000 1100 1111 1100 0111]2[1100 1111 1100 0111]2=1234553191=[0000\ 0000\ 0000\ 0000\ 1100\ 1111\ 1100\ 0111]_2 \to [1100\ 1111\ 1100\ 0111]_2=-12345

53191=[1111 1111 1111 1111 0011 0000 0011 1001]2[0011 0000 0011 1001]2=12345-53191=[1111\ 1111\ 1111\ 1111\ 0011\ 0000\ 0011\ 1001]_2 \to [0011\ 0000\ 0011\ 1001]_2=12345

2.3 整数的运算

2.3.1 无符号加法

  • 原理:正常相加,溢出时取截断,丢弃最高位
x+y={x+y,x+y<2w正常x+y2w,2wx+y<2w+1溢出x+y=\left\{ \begin{array}{lll} x+y, & x+y<2^w & 正常\\ x+y-2^w, & 2^w\le x+y<2^{w+1} & 溢出 \end{array} \right.

【例】255+1=[1111 1110]2+[0000 0001]2=[0000 0000]2=0255+1=[1111\ 1110]_2+[0000\ 0001]_2=[0000\ 0000]_2=0

2.3.2 有符号加法

  • 原理:类似无符号加法,包含正溢出和负溢出。溢出时取截断
x+y={x+y2w,2w1x+y正溢出x+y,2w1x+y<2w1正常x+y+2w,x+y<2w1负溢出x+y=\left\{ \begin{array}{lll} x+y-2^w, & 2^{w-1} \le x+y & 正溢出\\ x+y, & -2^{w-1} \le x+y<2^{w-1} & 正常\\ x+y+2^w, & x+y<-2^{w-1} & 负溢出 \end{array} \right.

【例】127+1=[0111 1110]2+[0000 0001]2=[1000 0000]2=0127+1=[0111\ 1110]_2+[0000\ 0001]_2=[1000\ 0000]_2=0

2.3.3 有符号减法和无符号减法

  • 减法:x-y等价于x+y',其中y'是y的加法逆元,即y+y'=0

  • 无符号数的加法逆元:

x={x,x=02wx,x0-x=\left\{ \begin{array}{lr} x, & x=0\\ 2^w-x, & x\ge0 \end{array} \right.

【例】12345+x=0x=21612345=5319112345+x=0 \to x=2^{16}-12345=53191

  • 有符号数的加法逆元:
x={TMinw,x=TMinwx,x>TMinw-x=\left\{ \begin{array}{ll} TMin_w, & x=TMin_w\\ -x, & x>TMin_w \end{array} \right.

【例】有符号数的加法逆元

12345+x=0x=1234512345+x=0 \to x=-12345

32768+x=0x=32768(若x=32768溢出,截断后值为32768-32768+x=0 \to x=-32768(若x=32768溢出,截断后值为-32768)

【例】有符号数的减法:123345=123+(345)=222123-345=123+(-345)=-222

2.3.4 无符号乘法

  • 有溢出情况,截断处理。原理:xy=(xy) mod 2wx*y=(x*y)\ mod\ 2^w

【例】无符号乘法:12345*12345

unsigned short x = 12345, y = 12345;
printf("x=%d\n", (unsigned short)(x*y));    //结果:x=27825

【解释】1234512345=152,399,025 mod 65536=2782512345*12345=152,399,025\ mod\ 65536=27825

2.3.5 有符号乘法(补码乘法)

  • 有溢出情况,截断处理。无符号乘法和有符号乘法的乘积的位级表示都相同(证明略)

image.png

2.3.6 乘以常数

  • 乘以2k2^k:转化为左移k位。x4=(x<<2)x*4=(x<<2)
  • 乘以常数:转化为左移和加法。x14=(x<<3)+(x<<2)+(x<<1)=(x<<4)(x<<1)x*14=(x<<3)+(x<<2)+(x<<1)=(x<<4)-(x<<1)

2.3.7 除以2的幂

  • 无符号数除以2k2^k:使用逻辑右移实现

image.png

  • 有符号数除以2k2^k:使用算数右移实现。如果除不尽,向零取整
    • 为了实现向零取整,当x<0时,有符号数需要加上偏量(biasing)
x2k={(x+2k1)>>k,x<0x>>k,x0\frac{x}{2^k}=\left\{ \begin{array}{ll} (x+2^k-1)>>k, & x<0\\ x>>k, & x\ge 0 \end{array} \right.

【例】书中例子,12340/2k-12340 / 2^k。为了保证向零取整,需要加上偏量

  • 不加偏量的情况,可以看到12340>>4=772-12340>>4=-772结果不正确。实际我们要的是771.25771-771.25\approx-771

image.png

  • 加上偏量biasing=241=15biasing=2^4-1=15,此时(12340+15)>>4=771(-12340+15)>>4=-771

image.png

【注意】上述方法不能推广到除以任意常数

2.4 浮点数

2.4.1 二进制小数

二进制小数:即将二进制整数扩展到小数位:

b=i=nm2i×bib=\sum_{i=-n}^m 2^i\times b_i

【例】101.112=1×22+0×21+1×20+1×21+1×22=5.7510101.11_2=1\times2^2+0\times2^1+1\times2^0+1\times2^{-1}+1\times2^{-2}=5.75_{10}

2.4.2 IEEE浮点表示

IEEE标准V=(1)s×M×2EV=(-1)^s \times M \times 2^E。类似科学计数法

  • 符号s(sign):s=0表示正数;s=1表示负数
  • 小数字段M(significand):表示二进制小数的值
  • 阶码E(exponent):对浮点数加权,权重是2E2^E

C语言的浮点数:分单精度浮点(float)和双精度浮点(double):

  • 单精度(32位):s、exp、frac分别为1位、8位、23位
  • 双精度(64位):s、exp、frac分别为1位、11位、52位

image.png

float格式编码类型:根据exp的值,编码类型分为三大类:

image.png

  • 规格化值(exp位不全为0且不全为1):最普遍的情况
    • 阶码E=eBiasE=e-Bias:单精度是126 +127-126~+127、双精度是1022 +1023-1022~+1023
    • 小数字段M:看做是 1.fn1fn2f01.f_{n-1}f_{n-2} \cdots f_0 的数字。由于都是以1开头,不需要显示表示
  • 非规格化值(exp位全为0):表示数值0、表示非常接近0.0的数
    • 阶码E=1BiasE=1-Bias:单精度是126 +127-126~+127、双精度是1022 +1023-1022~+1023
    • 小数字段M:看做是 0.fn1fn2f00.f_{n-1}f_{n-2} \cdots f_0 的数字
    • 数值0:有两种表示方法:+0.0和-0.0
  • 特殊值(exp位全为1)
    • ++\infty:s=0、exp全1、frac全0
    • -\infty:s=1、exp全1、frac全0
    • NaNNaN:exp全1、frac不全为0

2.4.3 数字示例

  • ee:假定阶码字段是一个无符号整数所表示的值
  • EE:偏置之后的阶码值
  • 2E2^E:阶码的权重
  • ff:小数值
  • MM:尾数的值
  • 2E×M2^E \times M:该数(未规约的)小数值
  • VV:该数规约后的小数值(约分后的值)
  • 十进制:该数的十进制表示

image.png

【例】整数12345和小数12345.0的关系

  • 整数12345具有二进制表示[11 0000 0011 1001]2[11\ 0000\ 0011\ 1001]_2

  • 12345.0=1.10000001110012×21312345.0=1.1000000111001_2 \times 2^{13},因此:

    • s=0s=0
    • e=13+127=140=[10001100]2e=13+127=140=[10001100]_2
    • M=[1000001110010000000000]2M=[1000001110010000000000]_2(丢弃第一个1,后面补0)
    • 得到:12345.0=[01000110010000001110010000000000]212345.0=[01000110010000001110010000000000]_2
  • 有意思的是:12345和12345.0两者有部分数字是重叠的:

image.png

2.4.4 舍入

IEEE定义了四种舍入方式

image.png

向偶数舍入的理解:可防止结果的统计偏差,一半情况向上舍入,一半情况向下舍入

  • 1.2349991.231.234999 \to 1.23
  • 1.2350011.241.235001 \to 1.24
  • 1.2351.241.235 \to 1.24
  • 1.2251.221.225 \to 1.22

二进制数向偶数舍入:1看做奇数,0看做偶数

  • 10.11100211.00210.11100_2 \to 11.00_2

2.4.5 浮点运算

由于浮点数有溢出的存在,有些运算带来影响(需特别小心!

  • 加法交换律:具备
  • 加法结合律:不具备。例:(3.14+1e10)1e103.14+(1e101e10)(3.14+1e10)-1e10 \neq 3.14+(1e10-1e10)
  • 乘法交换律:具备
  • 乘法结合律:不具备。例:(1e201e20)1e201e20(1e201e20)(1e20*1e20)*1e-20 \neq 1e20*(1e20*1e-20)
  • 乘法分配律:不具备。例:1e20(1e201e20)1e201e201e201e201e20*(1e20-1e20) \neq 1e20*1e20-1e20*1e20

2.4.6 C语言中的浮点数

GCC在库中定义了一些特殊值:如INFINITY(代表++\infty)、NAN(代表NaNNaN

#ifndef _MATH_H
#define _MATH_H 1
......

#ifdef __USE_ISOC99
/* IEEE positive infinity.  */
# if __GNUC_PREREQ (3, 3)
#  define INFINITY (__builtin_inff ())
# else
#  define INFINITY HUGE_VALF
# endif

/* IEEE Not A Number.  */
# if __GNUC_PREREQ (3, 3)
#  define NAN (__builtin_nanf (""))
# else
/* This will raise an "invalid" exception outside static initializers,
   but is the best that can be done in ISO C while remaining a
   constant expression.  */
#  define NAN (0.0f / 0.0f)
# endif
#endif /* __USE_ISOC99 */

C语言定义了int、float、double三种类型。其强制类型转换规则:

  • int转换成float:数字不会溢出,但是可能舍入
  • int、float转换成double:保留精确值
  • double转换成float:可能溢出、舍入
  • float、double转换成int:向零舍入、有可能溢出
#include <stdio.h>

int main(int argc, char *argv[])
{
    float num = 1e10;
    printf("%d\n", (int)num);
    return 0;
}

【输出】-2147483648