信息存储
在计算机中,都是使用8位的块或者字节来作为最小的可寻址内存空间,简单地说,就是不能只从内存中取一个比特,而是一下子取一个块或者一个字节。程序把内存看作一个巨大的数组,这个数组称为虚假内存,内存中每个字节都可以由唯一的数字区分,这个数字叫做内存地址,内存地址的集合称为虚假内存空间。
这一章是讲数据到底是怎么存储在计算机中的,更细致的,是指各种数字是怎么存的,会先从简单开始介绍。
十六进制表示法
十六进制就是以0x或0X开头的数字,它是计算机数字表示的一种进制,可以让二进制位看得更简洁一些。
对于十进制的数字,用0-9表示;但对于十六进制的数字,因为它是逢十六进一,所以在9之后,紧接的是字母A-F,它们各自代表10-15。
十进制 | 0 | ... | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|
二进制 | 0000 | ... | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
十六进制 | 0 | ... | 9 | A | B | C | D | E | F |
在这里也很明显地发现,二进制转十六进制的时候,可以从低到高四位四位地转,剩下的若不足四位,则高位补0。
十六进制转二进制
下面展示十六进制的0x173A4C
转换为二进制数的过程。简单地说就是一个十六进制顶四个二进制。
十六进制 | 1 | 7 | 3 | A | 4 | C |
---|---|---|---|---|---|---|
二进制 | 0001 | 0111 | 0011 | 1010 | 0100 | 1100 |
2^n转十六进制
对于一些特殊点的情况,比如一些2^n
的二进制数,转十六进制数则非常快,看看n由几个4组成,每有一个4,后面即跟一个0,剩下的余数则是正常转。
比如,对十进制的2^10,转成二进制的话,即1后面跟10个0,10000000000
。
因为10=2+4*2
,即2^10=2^2*2^4*2^4
因此,转成十六进制为0x400
十六进制转十进制
其实和二进制转十进制一样,只是每一位权重不同。比如最低位是16^0,倒数第一位是16^1,倒数第二位是16^2,依此类推……
只要这一位有数,就乘就行。
十进制转十六进制
手动算的话,需要一步一步地除,下面举个例子,比如把十进制188转换成十六进制。
188 = 11 * 16 + 12
11 = 0 * 16 + 11
结果从下往上看,即为0xBC
我们再拿它转回去,即C+B*16=12+11*16=188,正确。
字数据相关
这部分了解得不深,难以分类,便简单地介绍一下。
现在想想,它们把这些写成书真牛逼,我连自己总结都憋不出来几个字。
一些介绍
每台计算机都有其特定的字长,通常为32位或64位。那对于一台字长为32位的机子来说,它的程序的虚拟内存空间最大能取2^32-1,程序最多能访问2^32个字节。这个计算是拿32位全1来看的,因为这个数是无符号的数。
书本上提到,32位机子内存空间是4GB,64位机子内存空间是4EB。(GB-->TB-->PB-->EB,非常大!)
它这里讲到一些不同字长的机子上运行同一个程序,它们要怎么兼容,在C中,在32位机子中,long为4字节,而64位机子中,它占8字节,就会很不同。
即,一段在32位机子上能正常运行的代码,到64位机子上却出异常了,这是为什么呢。。。就有这种疑惑。
寻址和字节序
对于跨越多个字节的程序对象,要思考两个问题。
- 它的地址是什么
- 内存中如何排布这些字节
对于几乎所有的机器,这种对象的存储一般都是连续的内存空间,并且它的地址是使用字节的最低地址。假如一个整数x的地址是0x100,那么它就会从0X100开始,存储这个整数,总共占4个字节,即存放在0x100、0x101、0x102、0x103。
对于一个w位的整数,它的位可以表示为[x~w-1~,x~w-2~,x~w-3~...x~2~, x~1~,x~0~],其中,最高有效位是x~w-1~,最低有效位是x~0~。
-
大端和小端
在内存的低位存放的是最高有效位,这就是大端法。
在内存的高位存放的是最低有效位,这就是小端法。总觉得容易搞混,真是。
那我们从人的角度来说,数据总是从低地址往高地址存放,在存放的过程中,如果是按我们正常的做法去放,那应该把整数
正序
地存放,即最高有效位在前
,最低有效位在后,因此,这就是内存的低地址存放最高有效位,也就是常说的大端法。
我发现我的机子好像使用的是小端,是通过练习题的结果知道的。
/**
* @brief 结果是小端??
* 结果:
* 21
* 21 43
* 21 43 65
*/
void think()
{
int val = 0x87654321;
byte_pointer valp = (byte_pointer)&val;
// 这个函数是打印,按char输出(即两个字节,8位)
show_bytes(valp, 1);
show_bytes(valp, 2);
show_bytes(valp, 3);
}
这个大端小端,似乎与操作系统有关系,这种没有好坏之分,只要协商好规则即可。
字节序可能引起的问题
-
不同类型的计算机传输数据时。
若是不同的字节序,就会收到反序的数据,没有得到正确的结果。
-
阅读表示整型数据的字节序列时。
这里展示的在需要阅读的机器级程序的场景,代码是一个反汇编的语句,里面有一行说,需要把一个字长的数据怎样地添加,但是,这行数据的存储方式是大端或小端会影响最终的结果,因为会加错,因此需要注意。
-
编写规避正常的类型系统的程序时。
就是上面那段代码了,再加一段。
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 *)); }
把数据转成char*指针,然后打印。
打印的结果,你会发现,跟大端/小端有关,如果是小端,打印出来的结果是数字的低位;如果是大端,打印出来的结果就是我们正常阅读的位数。
这块实在太高深,总结不出来什么,不过可以记录一下这段话:
指令编码是不同的,不同的机器类型使用不同的且不兼容的指令和编码方式,即使完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的,二进制代码很少能够在不同机器和操作系统组合之间移植。
计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调度的辅助表以外。
布尔代数
把逻辑值true和false编码为二进制的1和0,能够创建一种新的代数。
-
AND:&
同为1才是1,否则为0
& 0 1 0 0 0 1 0 1 -
OR:|
只要有1就是1
0 1 0 0 1 1 1 1 -
NOT:~
取反
~ 0 1 1 0 -
EOR:^
异或,相异为1,相同为0
^ 0 1 0 0 1 1 1 0
C语言中的位级运算
同布尔代数的符号,&、|、~、^。
这些位运算,非常精妙,就是对于整数,它是一位一位地算的,没什么技巧。
下面展示一些精妙的东西,比如这个交换元素,真是炫技:
void 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--)
{
swap(&a[first], &a[last]);
}
}
在偶数个元素时,最后一次会交换中间的元素。swap很正常,但是它传过去的是同一个地址,要swap的第3行时,就直接异或成0了,因此中间的元素总是会为0。
解决方法是,把=去掉。
C语言中的逻辑运算
逻辑运算,就是结果只有1或0的运算,而不是像位运算一样,都有结果。
运算的符号有:&&、||、!
而且,这个操作还短路,即它的左边判断出来了,右边就不会继续执行!
-
做一道题,总结一下位运行和逻辑运行
对于x=0x66 01100110,y=0x39 00111001
x=0x66 01100110 y=0x39 00111001 x & y 0x20 x && y 0x01 x | y 0x7f x || y 0x01 ~x | ~y 0xdf !x || !y 0x00 x & !y 0x00 x && ~y 0x01
C语言中的位移运算
左移:<<
左移x位,抛弃溢出的最高位,右端补0。
逻辑右移:>>
同左移,右移x位,抛弃溢出的最低位,左端补0 。
算术右移:>>
几乎所有的编译器和处理器对于>>都是采用算术右移,即右移x位,抛弃溢出的最低位,左端补符号位。
但是对于Java这种,它有>>>区分。
下面展示一个例子
x | x << 3 | x >> 2 (逻辑右移) | x >> 2 (算术右移) |
---|---|---|---|
11000011 | 00011000 | 00110000 | 11110000 |