第二章 信息的表示与处理------2.1信息存储

信息的表示和处理

二进制在机器上相比于十进制工作的更好,更加容易被表示、存储和传输

三种重要的数字表示:

  1. 无符号编码(unsigned):大于或等于零的数字
  2. 补码:可以为正或负的数字
  3. 浮点数:表示实数的科学技术法,以2位基数的版本

溢出(overflow):用有限数量的位对一个数字编码,因此当结果太大导致不能表示时,某些运算就会溢出

对于GNU编译套装(GCC)依照不同版本的C语言规则来编译程序编译prog.c文件

  1. C90:gcc -std=c89 prog.c
  2. C99:gcc -std=c99 prog.c
  3. C11:gcc -std=c11 prog.c

信息存储

  1. 大多数计算机使用8位的块,或者字节(byte),作为最小的可以寻址的内存单位,而不是访问内存中单独的位
  2. 虚拟内存(\(virtual\ memory\):指的是机器程序将内存视为一个非常大的字节数组
  3. 地址(\(address\):内存的每个字节都由一个唯一的数字来表示。(地址就是一个数字)
  4. 虚拟地址空间\(virtual\ address\ space\)):所有可能地址的集合。(一堆数字的集合)

编译器运行时,将系统划分为可管理的单元,用来存放程序对象(程序数据、指令和控制信息)

每个程序对象可以简单的视为一个字节快,而程序本身就是一个字节序列


区分字、字节、字长、位(50条消息) 字、字长、字节、位_loverszhaokai的博客-CSDN博客

  1. \((Bit)\):表示一个二进制数码0或者1,是计算机存储处理信息的最基本的单位
  2. 字节\((Byte)\)一个字节由8个位组成。表示作为一个完整处理单位的8个二进制数码。目前计算机多使用ASCII编码
  3. \((Word)\)16个位为一个字(即两个字节是一个字)。代表计算机处理指令或数据的二进制数位数,是计算机进行数据存储和数据处理运算单位。通常称16位是一个字,32位是一个双字,64位是两个双字。
  4. 字长:字的位数叫字长。不同单词的机器有不同的字长。例如一个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的字节顺序依赖于机器的类型

大端法:最高有效字节在最前面的方式。

image-20230211164841268

小端法:最低有效字节在最前面的方式

image-20230211164918126

注意:高位字节的十六进制为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

  1. a[k]

  2. 当最后一次循环,两个指针指向了相同的地址,由a^a = 0可知,最后会等于0;

  3. 改成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位。

posted @ 2023-02-12 16:35  jay_j  阅读(126)  评论(0)    收藏  举报