计算机信息存储

2.1 信息存储

大多数的计算机使用8位,即一个字节作为最小的可寻址单元,什么叫可寻址单元呢,可以把内存看作是一个很长很长的数组,那么其中每一个元素都有一个对应的下标,我们可以通过下标找到需要的元素,这样可以被找到的最小的单位,就是一个可寻址单元,这样的下标,便称之为地址。

2.1.1 十六进制

通常,使用一长串01太麻烦了,看起来让人眼花缭乱不是么,所以我们大多数情况下都使用十六进制表示,把4位二进制数变成1位十六进制,这样不就短了很多么。十六进制的0-9和十进制一样,而10-15使用A-F进行表示:

十六进制ABCDEF
十进制101112131415
二进制101010111100110111101111

使用0x或者0X开头的被认为是十六进制数,同时,A-F的大小写不进行区分,譬如0xFa1D37b就是一个十六进制数。

二进制转化为十六进制可以这么做:把一个二进制的从低位开始每4位一划分,然后将每4个对应的十六进制数写出来即可。

例如,1101110划分为110 1110,然后对应的十六进制数为6 E,即为对应的十六进制数。

十六进制转化为二进制可以这么做:把每一个十六进制写成对应的二进制即可。

例如,3C2D,3对应0011,C对应1100,2对应0010,D对应1101,那么二进制为0011 1100 0010 1101。

2.1.2 字数据大小

前面讲了字节,一个字节是8位bit,也就是8个0或者1,那么什么是字呢,和字节有关系么?

计算机中最核心的莫过于中央处理器CPU,就像人的大脑一样,其中有一个叫做算术逻辑单元ALU的部件,ALU是负责运算的,比如要做加减乘除,都要靠ALU。但是,ALU能够处理的数据是由长度限制的,这是硬件所限,对于某一个特定的机器,它的ALU可能最多能够处理32位的数据,或者64位的数据,这就是我们说的32位机或者64位机,而这样一个机器可以处理的数据大小,就被叫做一个(word)。

一般来说,字有多长,那么地址位也就有多长,所以对于一个32位机器,可寻址的地址单元就有 2 32 2^{32} 232个。

C语言支持多种数据类型,在不同字长的机器上可能有着不同的大小,比如long型在32位机上是4个字节(32位),在64位机上是8个字节(64位)。这很容易理解,以为32位机器的ALU一次就能处理32位的整数,64位机也就是64位,后面我们也会讲到浮点型。

C Data TypeTypical 32-bitTypical 64-bitx86-64
char111
short222
int444
long488
float444
double888
long double10/16
pointer488

2.1.3 字节顺序

从上面我们可以知道,一个字包含多个字节,可能是4个,也可能是8个,那么问题来了,在内存中,一个字中的多个字节是如何放置的,举个例子,有一个32位的字0x01234567,分配给它的内存地址空间为0x100到0x103这4个字节,那么对于字节67,貌似可以放在0x100,也可以放在0x103,直觉告诉我们,不如顺序放吧,像下面这样:

字节01234567
地址0x1000x1010x1020x103

那难道下面这种不可以么,虽然它似乎有点反人类:

字节01234567
地址0x1030x1020x1010x100

事实上,这就是两种不同的字节组织形式,上面那种被称之为大端,下面的则是小端,书上给的区分方式是这样的:最低有效字节在最前面,称为小端,而最高有效字节在前面,称为大端。

看的是低位(字节),对于上面的例子而言,低位是67,如果放在低地址,即0x100,则为小端,如果放在高地址0x103,则是大端。

image-20220112115034337

事实上,两种方式都有厂家在使用,比如大端的典型有Sun(现在被Oracle收购了)的机器以及网络的传输(所以如果你用的是小端机器,使用网络传输时可能需要一些操作),而小端的典型有x86、ARM等等,现在的机器大多数都是小端的,所以你可能见不到大端机器。

大端和小端这两个名词的来源是格列佛游记,讲的是吃鸡蛋应该从大的一端打呢,还是小的一端打。感兴趣的话可以看看教材中的描述。

我们来看看大端机器和小端机器的区别,比如有一个整形变量A,那么它在大端机器Sun和小端机器x86上的区别如下:

int A = 15213;	// 十六进制为3B6D

image-20220112120407778

注:上面是低地址,下面是高地址。

如果我们想要程序来让我们看看从一个地址开始的一些字节是什么样子的,那么可以使用下面的例程:

typedef unsigned char *pointer;

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

注:在C++中,设计size_t就是为了适应多个平台的。size_t的引入增强了程序在不同平台上的可移植性。size_t是针对系统定制的一种数据类型,一般是整型,因为C/C++标准只定义一最低的位数,而不是必需的固定位数。而且在内存里,对数的高位对齐存储还是低位对齐存储各系统都不一样。为了提高代码的可移植性,就有必要定义这样的数据类型。

使用char型的指针是因为char正好是一个字节的。

比如,打印出一个整型变量在内存中的存储:

int a = 15213;
printf("int a = 15213;\n");
show_bytes((pointer) &a, sizeof(int));

在x86、windows系统的机器上跑出来的结果如下:

int a = 15213;
000000000062FE1C        0x6d
000000000062FE1D        0x3b
000000000062FE1E        0x00
000000000062FE1F        0x00

可以很明显地看出,这是一个小端的机器。

另外,如果看一下同一个值,在不同机器和操作系统上的字节表示形式,可以发现一个很奇妙的事情,对人而言,12345和12345.0应该是相等的,但是在int和float上的表示形式确实截然不同的,如下图,一个是00 00 30 39,而另一个则是46 40 E4 00,展开为二进制的话,分别是0000 0000 0000 0000 0011 0000 00110100 0110 0100 0000 1110 0100 0000 0000,对比一下会发现其中有13位是匹配的,这并不是巧合,在后面学习浮点数的时候我们会有对应的解释。

image-20220112134429987

打印出的指针值和操作系统、编译器有着密切的关系,甚至每一次运行都有可能出现不同的结果
Different compilers & machines assign different locations to objects

2.1.4 字符串表示

字符串比数字要简单许多,对于不同的机器,打印出的字符串在虚拟内存中的表示形式是相同的,因为都是采用ASCII码的表示,而且与上面说的字节顺序没有任何关系,因此具有较高的兼容性。在下图中可以看到,对于这一串字符,在大端和小端机器上都没有任何区别。

image-20220112135435919

image-20220112135442738

2.1.5 表示代码

那么代码又如何在机器中表示呢,比如有下面这一段C程序代码:

int sum(int x, int y){
	return x + y;
}

在不同的机器上编译,会发现机器代码都是不同的,这是因为不同的机器用的指令是不同的,因此二进制代码是不兼容的,很难在不同机器或者操作系统之间进行移值。

2.1.6 布尔代数

布尔代数是19世纪的时候被乔治·布尔发明的,后来香农首先建立了布尔代数和数字逻辑之间的联系。对于布尔代数而言,把1当成是Ture,把0当成是False。

有这样几种基本的运算:

  • And与:两个都为1的时候结果为1
  • Or或:两个中只要有一个为1,结果为1
  • Not非:1变成0,0变成1
  • Xor异或(Exclusive-Or),两个不同的时候则为1

image-20220112140621962

位运算:对位向量进行上面的这些操作,位向量就是一串01,比如01101001。下面给一些例子:

image-20220112140839066

很多时候,我们会用位向量,也就是一串01来表示一个有限集合,比如01101001,实际上,如果我们把所有的1所在的位置(最右边一位的位置记为0)写出来,也就是一个与其等价的集合 { 0 , 3 , 5 , 6 } \{0,3,5,6\} {0,3,5,6}。这种表示方法很有用,几个简单的例子,比如我们现在有10个信号,也就是一个信号的集合,我们需要屏蔽其中的一些,那么我们可以记被屏蔽的为0,反之为1,于是我们就可以用10位的位向量进行表示。

2.1.7 C语言的位运算

C语言也是支持按位运算的,并且和上面所用的符号是相同的,比如下面这几个例子:

image-20220112142346726

在对十六进制进行位运算的时候,先转化为二进制,因为只有二进制才能看到具体的某一位是0还是1,最后再转化为十六进制。

2.1.8 C语言的逻辑运算

逻辑运算和位运算经常被人搞混,因为他们长得太像了,逻辑运算也有三种,分别用&&||!来表示。

那么这个逻辑运算和位运算的有什么区别呢?

在逻辑运算中,只关心True和False,正如它的名字一样,只关心这些逻辑,比如对于逻辑与,两个都为True则结果为True,对于逻辑或,只要一个为True,则结果为True,非则是True变False,False变True。

那什么是True,什么又是False呢?

在C语言中,非零即真,也就是说,无论数是多少,位表示是怎么样的,只要不是0,就是真。举个例子,0x69是True,0x55也是True,那么他们的与结果就是True,而C语言中的True就是1,那么就有如下的运算式:
0 x 69   & &   0 x 51 = 0 x 01 0x69 \space \&\& \space 0x51 =0x01 0x69 && 0x51=0x01
同样的也就有如下的式子:

image-20220112143300609

2.1.9 移位操作

什么是移位呢,对于一个w位的位向量 [ x w − 1 , x w − 2 , . . . , x 0 ] [x_{w-1},x_{w-2},...,x_0] [xw1,xw2,...,x0],你可以把每一位都向左或者向右移动若干位,但是这样会有一个很显然的问题,就是向左移的时候,必然会把 x w − 1 x_{w-1} xw1移出去,同时本来 x 0 x_{0} x0的位置处也会被空出来,因为位数是固定的,而右移的时候,也必然会把 x 0 x_0 x0移出去,同时本来 x w − 1 x_{w-1} xw1的位置处也会被空出来。

对于左移而言,很简单,移出去的就移出去了,而补的时候补0即可,这就是左移,符号是<<

比如现在有一个数 x = 01100011 x=0110 0011 x=01100011,那么有:

x << 4 = 0011 0000

其中右边的4个0都是补的,而左边的4位是原来右边的四位。

而对于右移而言,却分成了两种,一种称之为逻辑右移,一种称之为算术右移,逻辑右移的操作和上面一样,空出来的补0即可,而算数右移则不是,算数右移为了保证有符号数的正负不改变(因为最高位是符号位),会在空出来的高位上补符号位,而不是所有情况下都补0。

比如数 x = 10010101 x=1001 0101 x=10010101,那么有:

x >>(逻辑右移) 4 = 0000 1001 
x >>(算数右移) 4 = 1111 1001 

对有符号数而言,默认是使用算数右移,而对于无符号数而言,只能是逻辑右移。

Java与C不同,使用>>表示算术右移,而>>>表示逻辑右移。

可能看到这里有细心的同学会问:如果移的位数超过了本身的位数呢?比如我只有8位,但是你要我移12位,如果是逻辑的移位,那么结果会是全0么?

答案是,当移动的位数 k k k 大于等于自身的位数 w w w ,实际上移动的位数是 k   m o d   w k \space mod \space w k mod w 位。比如上面的例子,8位左移12位,实际上只会移动4位(12 mod 8)。

最后,提一句移位操作的优先级,移位操作的优先级是比加减运算低的,所以如果你想优先移位,请加上括号,吃不准的时候,都加上吧。

本篇文章内容均来自CSAPP导读第2章,希望更加深入了解计算机系统的朋友可以点击看一看。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值