2.1 信息存储

 
    大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space)。虚拟内存概念参考: https://www.jianshu.com/p/415618863d52  
 

2.1.1 十六进制表示法

    一个字节由8位组成。在二进制表示法中,它的值域是00000000 ₂~ 11111111₂。如果看成十进制整数,它的值域为 010~25510。这两种符号表示法对于描述位模式来说都不是非常方便。常用的是以16为基数,或者叫做十六进制数,来表示位模式。十六进制(简写hex)使用数字 0 ~ 9 以及字符 A~F来表示16个可能的值。用十六进制书写,一个字节的值域 0016~FF16
 
练习题2.1 完成下面的数字转换:
 
 A.  将0x39A7F8转换为二进制    
十六进制         3           9          A            7              F              8
二进制          0011    1001    1010        0111        1111        1000
 
B.  将二进制1100100101111011 转化为十六进制
二进制       1100    1001     0111     1011
十六进制       C       9        7        B
 
C.  将0xD5E4C转化为二进制
十六进制        D        5        E        4        C
二进制        1101      0101     1110    0100      1100
 
D.  将二进制10011011110011110110101
二进制      100     1101     1110     0111     1011     0101
十六进制     4        D        E        7        B        5
 

2.1.2 字数据大小

    每台计算机都有一个字长(word size),指明指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为0~2W-1,程序最多访问2W个字节。
    常用的32位字长限制虚拟地址空间为4千兆字节(4GB),刚刚超过4*109字节。扩展到64位字长使得虚拟地址空间为16EB,大约1.84*1019字节。
    大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。我们将程序称为“32位程序”或“64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。
    计算机和编译器支持多种不同方式编码的数字格式,如不同长度的整数和浮点数。C语言支持整数和浮点数的多种数据格式。如下图展示了C语言各种数据类型分配的字节数。有些数据类型的确切字节数依赖于程序是如何被编译的。数据类型short、int、long可以提供各种数据大小,即使是为64位系统编译,数据类型int通常也只有4个字节。数据类型long一般在32位程序中为4字节,在64位程序中则为8字节。
    下图还展示了指针(例如一个被声明为类型为“char *”的变量)使用程序的全字长。大多数机器还支持两种不同的浮点数格式:单精度(float)和双精度(double),分别使用4字节和8字节。
 
C声明 字节数
有符号 无符号 32位 64位
[signed] char unsigned char 1 1
short unsigned short 2 2
int  unsigned 4 4
long unsigned long 4 8
int32_t uint32_t 4 4
int64_t uint64_t 8 8
char *   4 8
float   4 4
double   8 8

2.1.3 寻址和字节顺序

    对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中的最小地址。例如,假设一个类型为int的变量x的地址为0x100,也就是说,地址表达式&x的值为0x100。那么,int(32位,4个字节表示)将被存储在内存的0x100、0x101、0x102和0x103位置。
    排列表示一个对象的字节有两种通用规则。一个w位的整数,其位表示为[xw-1,xw-2,''',x1,x0],其中xw-1是最高有效位,x0是最低有效位。假设w是8的倍数,这些位就能被分组称为字节,其中最高有效字节包含位[xw-1,xw-2,''',xw-8],而最低有效字节包含位[x8,''',x1,x0],其他字节包含中间的位。某些机器选择在内存中按照从低有效字节到最高有效字节的顺序存储对象,而另一些机器按照从最高有效字节到最低有效字节的顺序存储。其中,最低有效字节在最前面的方式,称为小端法。最高有效字节在最前面的方式,称为大端法。
    假设变量x的类型为int,位于地址0x100处,它的十六进制值为0x01234567。地址范围0x100~0x103的字节顺序依赖于机器的类型:
大端法                    
                                              0x100        0x101            0x102            0x103
 
··· 01 23 45 67 ···
小端法
                                              0x100        0x101            0x102            0x103
··· 67 45 23 01 ···
    注意,在字0x01234567中,高位字节的十六进制值为0x01,而低位字节值位0x67。
    大多数Intel兼容机都只用小端模式。
 
练习题2.5 思考对show_bytes的三次调用:
   
typedef unsigned char *byte_pointer;
 
    show_bytes(byte_pointer start, size_t len){
        size_t i;
        for(i = 0; i < len; i++)
            printf("%.2f",start[i]);
}
 
int val = 0x87654321;
byte_pointer valp = (byte_pointer) &val;
 
show_bytes(valp,1);    A:小端法: 21   大端法:87
show_bytes(valp,2);    A:小端法: 2143   大端法:8765
show_bytes(valp,3);    A:小端法: 214365   大端法:876543
 

2.1.4 表示字符串

    C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。因此,我们以参数"12345"和6(包括终止符)来运行例show_bytes,得到结果 31 32 33 34 35 00。请注意,十进制数字x的ASCII码正好是0x3x,而终止字节的十六进制表示为0x00。在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
 
练习题2.7 对show_bytes调用
const char *s = "abcdef";
show_bytes((byte_pointer) s,5);
 
输出  61 62 63 64 65 66
其中字母 'a'~'z' 的ASCII码为 0x61~0x7A。
 

2.1.5 表示代码

    在示例机器上编译时,生成不同字节表示的机器代码。不同的机器类型使用不同且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。
    计算机系统的一个基本概念就是,从机器的角度来看,程序进行只是字节序列。
    

2.1.6 布尔代数简介

    二进制值是计算机编码、存储和操作信息的核心。布尔注意到通过逻辑值TRUE(真)和FALSE(假)编码成二进制1和0,能够设计出一种代数,以研究逻辑推理的基本原则。
    最简单的布尔代数是在二元集合{0,1}基础上定义。布尔代数的运算,二进制值1和0表示逻辑值TRUE或者FALSE,而运算符~、&、|和^ 分别表示逻辑运算NOT、AND、OR、EXCLUSIVE-OR。
    后来创立信息论领域的Claude Shannon 首先建立了布尔代数和数字逻辑之间的联系。他表明了布尔代数可以用来设计和分析机电继电器网络。尽量那时计算机技术已经取得到了相当的发展,但是布尔代数仍在数字系统的设计和分析中扮演者重要的角色。
    我们可以将上述4个布尔运算扩展到位向量的运算,位向量就是固定长度为W、由0和1组成的串。位向量的运算可以定义成参数的每个对应元素之间的运算。假设a和b分别表示位向量[aw-1,aw-2,...,a0]和[bw-1,bw-2,...,b0]。我们将a&b也定义为一个长度为w的向量,其中第i个元素等于ai&bi,0<i<w。可以用类似的方式将运算|、^和~扩展到位向量上。
 
练习题 2.8 填写下表,给出位向量的布尔运算的求值结果
运算 结果
a [0110 1001]
b [0101 0101]
~a [1001 0110]
~b [1010 1010]
a&b [0100 0001]
a|b [0111 1101]
a^b [0011 1100]
     位向量一个很有用的应用就是表示有限集合。我们可以用位向量[aw-1,aw-2,...,a0]编码任何子集A ⊆{0,1,...,w-1},其中ai = 1当且仅当i∈A。例如位向量 a = [01101001]表示集合A={0,3,5,6},而b=[01010101]表示集合B = {0,2,4,6}。使用这种编码集合的方法,布尔运算|和&分别对应于集合的并和交,而~对应于集合的补。运算a&b得到位向量[01000001],而A∩B = {0,6}。
    在大量实际应用中,我们都能看到用位向量来对集合编码。例如,我们能够通过指定一个位向量掩码,有选择地使能或是屏蔽一些信号,其中某一位位置上位1时,表明信号i是有效的(使能),而0表明该信号是被屏蔽的。因而,这个掩码表示的就是设置为有效信号的集合。
 

2.1.7 C语言中的位级运算

    C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:|就是OR(或),&就是AND(与),~就是NOT(取反),而^就是EXCLUSIVE-OR(异或)。这些运算能运用到任何“整数”的数据类型上。以下是一些对char数据类型表达式求值的例子:
C的表达式 二进制表达式 二进制结果 十六进制结果
~0x41 ~[0100 0001] [1011 1110] 0xBE
~0x00 ~[0000 0000] [1111 1111] 0xFF
0x69 & 0x55 [0110 1001]&[0101 0101] [0100 0001] 0x41
0x69 | 0x55 [0110 1001] |[0101 0101] [0111 1101] 0x7D
    如示例所示,确定一个位级表达式的最好的方法,就是将十六进制的参数扩展成二进制表达并执行二进制运算,然后再转换回十六进制。
练习题 2.10  对于任一位向量a,有 a^a = 0。应用这一属性,考虑下面的程序:
void inplace_swap(int *x, int *y){
    *y = *x ^ *y;    /*Step1*/
    *x = *x ^ *y;    /*Step2*/
    *y = *x ^ *y;    /*Step3*/
}
填写每一步运行结果:
步骤 *x *y
初始 a b
Step1 a a ^ b
Step2 a ^ a ^ b = b a ^ b
Step3 a ^ a ^ b = b a ^ a ^ b ^ a ^ b = a
    位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x = 0x89ABCDEF,其表达式将得到0x000000EF。表达式~0 将生成一个全1的掩码,不管机器的字大小是多少。
 

2.1.8 C语言中的逻辑运算

  C语言还提供了一组逻辑运算符 || 、&&和!,分别对应命题逻辑中的OR、AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。它们返回1或者0,分别表示结果为TRUE或者FALSE。表达式求值示例如下:

表达式 结果
! 0x41 0x00
! 0x00 0x01
!! 0x41 0x01
0x69 && 0x55 0x01
0x69 || 0x55 0x01
   可以观察到,按位运算只有在特殊情况下,也就是参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。逻辑运算符&&和||与它们对应的位级运算&和|之间的第二个重要区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此,例如,表达式a&&5/a就不会造成被零除,而表达式p&&*p也不会导致间接引用空指针。
练习题 2.14 假设x和y的字节值分别为0x66和0x39。填写下表,指明各个C表达式的字节值
 
表达式 表达式
x & y 0x20 x && y 0x01
x | y 0x7F x || y 0x01
~x | ~y 0xDF !x || !y 0x00
x | !y 0x00 x && ~y 0x01
 
 练习题 2.15 只使用位级和逻辑运算,编写一个C表达式,它等价于 x==y。换句话说,当x和y相等时它将返回1,否则就返回0
  表达式:!(x ^ y)。当且仅当x的每一位和y对应的每一位匹配时,x^y等于零。然后我们通过!来判断一个字中是否包含任何非零位。
 

2.1.9 C语言中的移位运算

  C语言还提供了一组移位运算,向左或者向右移动位模式。对于一个位表示为[xw-1,xw-2,...,x0]的操作数x,其位表示为[xwk-1,xw-k-2,...,x0,0...,0]。也就是说,x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个0~w-1之间的值。移位运算是从左至右可以结合的,所以x<<j<<k等价于(x<<j)<<k。

  有一个相应的右移运算x>>k,但是它的行为有点微妙。一般而言,机器支持两种形式的右移:逻辑右移和算术右移。逻辑右移在左端补k个0,得到的结果是[0,...,0,xw-1,xw-2,..,xk]。算术右移是在左端补k个最高有效位的值,得到的结果是[xw-1,...,xw-1,xw-2,...xk]。这种做法对有符号整数的运算非常有效。

操作
参数x [01100011] [10010101]
x << 4 [00110000] [01010000]
x >> 4(逻辑右移) [00000110] [00001001]
x >> 4(算术右移) [00000110] [11111001]

  斜体的数字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一个外,其他的都填充0。唯一的例外是算术右移[10010101]的情况。因为操作数的最高位是1,填充的值就是1。

  C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移,然而,实际上几乎所有的编译器/机器都对有符号数使用算术右移。对于无符号数,右移必须是逻辑的。

 练习题 2.16 填写下表,展示不同移位运算对单字节数的影响。

x x<<3 x>>2(逻辑) x>>(算术)
十六进制 二进制 二进制 十六进制 二进制 十六进制 二进制 十六进制
0xC3 1100 0011 0001 1000 0x18 0011 0000 0x30 11110000 0xF0
0x87 1000 0111 00111000 0x38 0010 0001 0x21 1110 0001 0xE1

 

  

posted @ 2020-04-15 21:37  风墓  阅读(687)  评论(0编辑  收藏  举报