第二章 信息的表示与处理------2.1信息存储
信息的表示和处理
二进制在机器上相比于十进制工作的更好,更加容易被表示、存储和传输
三种重要的数字表示:
- 无符号编码(unsigned):大于或等于零的数字
- 补码:可以为正或负的数字
- 浮点数:表示实数的科学技术法,以2位基数的版本
溢出(overflow):用有限数量的位对一个数字编码,因此当结果太大导致不能表示时,某些运算就会溢出
对于GNU编译套装(GCC)依照不同版本的C语言规则来编译程序编译
prog.c文件
- C90:
gcc -std=c89 prog.c- C99:
gcc -std=c99 prog.c- C11:
gcc -std=c11 prog.c
信息存储
- 大多数计算机使用8位的块,或者字节(
byte),作为最小的可以寻址的内存单位,而不是访问内存中单独的位 - 虚拟内存(\(virtual\ memory\)):指的是机器程序将内存视为一个非常大的字节数组
- 地址(\(address\)):内存的每个字节都由一个唯一的数字来表示。(地址就是一个数字)
- 虚拟地址空间(\(virtual\ address\ space\)):所有可能地址的集合。(一堆数字的集合)
编译器运行时,将系统划分为可管理的单元,用来存放程序对象(程序数据、指令和控制信息)
每个程序对象可以简单的视为一个字节快,而程序本身就是一个字节序列
区分字、字节、字长、位((50条消息) 字、字长、字节、位_loverszhaokai的博客-CSDN博客)
- 位\((Bit)\):表示一个二进制数码0或者1,是计算机存储处理信息的最基本的单位。
- 字节\((Byte)\):一个字节由8个位组成。表示作为一个完整处理单位的8个二进制数码。目前计算机多使用ASCII编码
- 字\((Word)\):16个位为一个字(即两个字节是一个字)。代表计算机处理指令或数据的二进制数位数,是计算机进行数据存储和数据处理的运算单位。通常称16位是一个字,32位是一个双字,64位是两个双字。
- 字长:字的位数叫字长。不同单词的机器有不同的字长。例如一个8位机,他的一个字就等于一个字节,字长为8位;一个16位机,他的一个字等于两个字节,字长为16;一个32位机,他的一个字等4个字节,字长为32
对于不同CPU,字长的长度也不一样。8位的CPU一次只能处理一个字节,32位CPU一次就能处理4个字节
十六进制
| 表示方法 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 十进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 二进制 | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
| 十六进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
在C语言中,以0x或者0X开头的数字常量被认为是十六进制的值,字符\(A-F\)可以是大写或者小写
二进制与十六进制相互转换
给定一个二进制数字1111001010110110110011,可以首先把它分为每4位一组来转换为十六进制。如果位数 不是4的倍数,最左边一组可以少于4位,前面用0补足,然后将每个4位组转换为对应的十六进制数字
| 二进制 | 11 | 1100 | 1010 | 1101 | 1011 | 0011 |
|---|---|---|---|---|---|---|
| 十六进制 | 3 | c | A | D | B | 3 |
当\(x = 2^n\)时,可以很容易将\(x\)写成十六进制,将\(n = i +4j\),其中\(0 <=\ i\ <=3\),可以把x写成开头的十六进制数字为\(1(i=0)、2(i=1)、4(i = 2) 、8(i=3)\),后面跟着\(j\)个十六进制的0.比如\(x=2048=2^{11}\),则有\(n=11=3+4*2\),所以十六进制表示为0x800。
十进制与十六进制
两者转换需要使用乘法或者除法来处理一般情况。将一个十进制的数字转换成十六进制,可以反复用16除以x。然后通过余数的排列得到转换后十六进制的结果。
反过来,将一个十六进制的数字转换成十进制数字,可以用16乘以每个十六进制的数字。比如0x7AF,计算对应的十进制为\(7*16^2 + 10*16+15 = 1967\)。
字数据大小
字长\((word\ size)\),指明指针数据的标称大小。其决定着最重要的系统参数就是虚拟地址空间的最大大小。
对于一个字长为\(w\)位的机器而言,虚拟地址的范围为\(0-{2^w-1}\),程序最多访问\(2^w\)个字节
不同位数机器限制了虚拟地址空间的大小。
32位字长限制虚拟地址空间为4千兆字节(4GB)
64位字长使得虚拟地址空间为16EB,大约是\(1.84×10^{19}\)字节
32位程序与64位程序区别在于该程序是如何编译的,而不是其运行的机器类型。
| C声明 | C声明 | 字节数 | 字节数 |
|---|---|---|---|
| 有符号 | 无符号 | 32位 | 64位置 |
[signed]char |
unsigned char |
1 | 1 |
short |
unsigned short |
2 | 2 |
int |
unsigned |
4 | 4 |
long |
unsigned long |
4 | 8 |
int 32_t |
uint 32_t |
4 | 4 |
int 64_t |
uint64_t |
8 | 8 |
char * |
4 | 8 | |
float |
4 | 4 | |
double |
8 | 8 |
所谓的可移植性就是使程序对不同数据类型的确切大小不敏感
寻址和字节顺序
对象的地址是什么?
在内存中如何排列这些字节?
排列一个对象的字节由两个通用的规则。考虑一个\(w\)位的整数,其位表示为\([x_{w-1}, x_{w-2},…, x_1, x_2]\),其中\(x_{w-1}\)是最高有效位,而\(x_0\)是最低有效位。假设\(w\)是8的倍数,这些为就能被分组成为字节,其中最高有效字节包含位\([x_{w-1}, x_{w-2}, ……, x_{w-8}]\),而最低有效字节包含位\([x_7, x_6,……, x_0]\)。
假设变量x的类型位int,位于地址0x100处,他的十六进制值为0x01234567,地址范围0x100--0x103的字节顺序依赖于机器的类型
大端法:最高有效字节在最前面的方式。
小端法:最低有效字节在最前面的方式
注意:高位字节的十六进制为0x01,而低位字节值为0x67
现在比较新的微处理器是双端法\((bi-endian)\):可以把它们配置成作为大端或者小端的机器运行。
实际情况是:一旦选择了特定的操作系统,那么字节顺序也就固定下来。
\(ARM\)微处理器,其硬件可以按小端或大端两种模式操作
但是这些芯片上最常见的两种操作系统---\(Android(from\ Google)\)和\(IOS(from\ Apple)\)却只能运行小端模式。
选择何种字节顺序没有技术上的理由。
字节顺序也会引起一定的问题:
第一种情况:
当网络应用程序的代码从小端法机器传输到大端法机器时(或反过来),接收程序时会发现,字里的字节成了反序。
第二种情况:
阅读表示正数数据的字节序列。通常发生在检查机器级程序时。
例如:
下面是一行由反汇编器(\(disassembler\))生成的,这条指令是把一个字长的数据加到一个值上,该值的存储地址由
0x200b43加上当前程序计数器的值得到,当前程序的值即为下一条将要执行指令的地址。4004d3: 01 05 43 0b 20 00 add %eax,0x200b43(%rip)如果取出这个序列的最后4个字节:
43 0b 20 00,并且按照相反的顺序写出。我们得到00 20 0b 43,去掉开头的0,得到值:
0x00200b43就是右边的数值。
当阅读此类小端法机器生成的机器级程序表示时,经常会将字节按照相反的顺序显示。
书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,这正好和通常的书写数字时最高有效位在左边,最低有效位在右边的方式相反。
第三种情况
typedef的使用
typedef就是给一个数据类型进行新的命名比如
typedef unsigned char* byte_pointer;//将unsigned char*类型定义新名字 byte_pointer typdef int* int_pointer;//将int* 定义为int_pointer //下面两者等价 int *ip; int_pointer ip;在C语言中可以通过强制类型转换\((cast)\)或者联合\((union)\)来允许一种数据类型引用一个对象。
通过在不同的机器上运行如下代码
#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"); } void show_int(int x) { show_bytes((byte_pointer) &x, sizeof(int)); } void show_float(float x) { show_bytes((byte_pointer) &x, sizeof(float)); } void show_pointer(void *x) { show_bytes((byte_pointer) &x, sizeof(void *)); } void test_show_bytea(int val) { int ival = val; float fval = (float)ival; int* pval = &ival; show_int(ival); show_float(fval); show_pointer(pval); } int main() { test_show_bytea(12345); }最终可以得到如下图:
12345的十六进制表示为
0x00003039,对于int数据类型数据,除了字节顺序意外,所有机器上都得到了相同的结果。Linux32、Windows和Linux64上,最低有效数字39最先输出,所以可以判定他们是小端法机器;
而Sun上39最后输出,可以判定为大端法。
float数据的字节也是一样.对于指针,不同的机器/操作系统配置使用不同的存储分配规则。Linux32、Windows、Sun的机器使用的是4字节
关于字节(十六进制的理解)
给定一个字节序列(十六进制),例如39 30 00 00
一直无法理解这里的每个数字的意义。
解析:
上述是4个字节,每两个数字代表一个字节,按照十六进制。
每个字节是有8个二进制组成的。所以一个字节的范围是:0~255,也就是1111 1111
按照二进制与十六进制的转换,1111 1111转换为十六进制为:FF
。
表示字符串
C语言中字符串编码是一个以null(其值为0)字符结尾的字符数组。每个字符都由标准编码\((ASCII)\)来表示。如果以"12345"和6来运行show_bytes我们得到结果31 32 33 34 35 00
数字a的ASCII码是0x3a
表示代码
在不同机器上编译如下代码
int sum(int x, int y){
return x + y;
}
在机器上编译,会生成如下字节表示机器代码

不同的机器使用的指令和编码方式是不同的。即便是完全一样的进程,在不同的操作系统上也会有不同的编码规则。
因此,二进制编码是不兼容的。二进制代码很少能在不同机器和操作系统只见那移植。
计算机系统的一个基本概念就是,在机器的角度来看,程序仅仅是字节序列。
布尔代数简介
| 逻辑运算 | 布尔运算 | 符号 | 命题 | 表达式 |
|---|---|---|---|---|
| NOT | ~ |
¬ |
当P等于0时 | ¬P = 1 |
| AND | & |
^ |
当p=1且q=1时 | p&q=1 |
| OR | ` | ` | V |
当P=1或q=1时 |
| 异或 | ^ |
⊕ |
(1)p=1且q=0; (2)p=0且q=1 |
p^q=1 |
上述4个布尔运算扩展到位向量的运算。
假设 \(a\)和\(b\)分表表示位向量:
\([a_{w-1},\ a_{w-2},\ ……,\ a_0]\)
\([b_{w-1},\ b_{w-2},\ ……,\ b_0]\)
也定义a & b为一个长度\(w\)的位向量,其中第\(i\)个元素等于\(a_i\ \&\ b_i\)
表示有限集合
比如有位向量\([a_{w-1},\ a_{w-2},\ ……,\ a_0]\)
假设\(w=8\),那么可以通过\([a_7,\ a_6,\ ……,\ a_0]\)编码表示\([0,7]\):
位向量\(a=[01101001]\)表示第0、3、5、6位置为1,即\(A=\){\(0,\ 3,\ 5,\ 6\)}
位向量\(b=[01010101]\)表示第0、2、4、6位置为1,即\(B=\){\(0,\ 2,\ 4,\ 6\)}
对两者取
AND得到位向量\([01000001]\),即\(A\)\(n\)\(B\)={\(0,\ 6\)}
C语言中的位级运算
确定一个位级表达式结果的最好的方法,就是将十六进制的参数扩展成二进制表示,然后执行二进制运算,然后再转换回十六进制。
十六进制----->二进制-------->二进制运算---------->十六进制
| C的表达式 | 二进制表达式 | 二进制结果 | 十六进制结果 |
|---|---|---|---|
~x41 |
~[0100 0001] |
[1011 1110] |
0xBE |
练习题:
2.11

-
a[k] -
当最后一次循环,两个指针指向了相同的地址,由a^a = 0可知,最后会等于0;
-
改成
first<last最中心位置的数字不需要进行对调。
void inplace_swap(int* x, int* y) {
*y = *x ^ *y;
*x = *x ^ *y;
*y = *x ^ *y;
}
void reverse_array(int a[], int cnt) {
int first, last;
for (first = 0, last = cnt - 1; first < last;
first++, last--)
inplace_swap(&a[first], &a[last]);
}
int main() {
int a[] = { 1, 2, 3, 4, 5 };
int cnt = 5;
reverse_array(a, cnt);
for (int i = 0; i < cnt; i++)
{
std::cout << a[i] << std::endl;
}
}
位级运算的常见用法:掩码运算
掩码0xFF(最低的8位为1)表示一个字的低位字节。
位级运算x & 0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0
C语言中的逻辑运算
逻辑运算很容易和位级运算混淆,但两者是完全不同的。
第一个重要区别:逻辑运算认为所有非零参数都表示True,而参数0则表示FALSE
个人理解:逻辑运算的结果只有1或者0
逻辑运算&& ||与位级运算& |的第二重要区别是:如果第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值
练习题2.15
bool prac2_15(int x, int y) {
return (bool)!(x ^ y);
}//!(x ^ y)
利用异或,如果两个相同的值进行异或则等于0;
C语言中的移位运算
有两种移位运算,向左或者向右移动位模式
对于一个位表示\([x_{w-1},\ x_{w-2},\ ……,\ x_0]\)
左移运算
x<<k,其位表示为\([x_{w-k-1},\ x_{w-k-2},\ ……,\ x_0,\ 0,\ ……,\ 0]\),x向左移动\(k\)位,丢弃最靠的\(k\)位,并在右端补\(k\)个0。
右移运算
x>>k,机器支持两种形式的右移:逻辑右移和算术右移
逻辑右移在左端补\(k\)个0,得到结果是\([0,\ ……,\ 0,\ x_{w-1},\ x_{w-2,\ ……,\ x_k}]\)
算术右移在左端补\(k\)个最高有效位的值,得到的结果是\([x_{w-1},\ ……,\ x_{w-1},\ x_{w-1},\ x_{w-2},\ ……,\ x_k]\)
几乎所有的编译器/机器组合都对有符号数使用算术右移。
对于无符号数,右移必须是逻辑的
移动\(k\)位(很大),实际上位移量是通过计算
k mod w得到的假定需要移动32位,那么就是32/32=1……0,移动0位
移动36位,那么就是36/32 = 1 ……4,移动4位。


浙公网安备 33010602011771号