- 计算机组成原理(基于x86-64架构)
- (美)罗伯特·G.普兰茨
- 3168字
- 2025-05-19 16:07:05
2.3 在存储器中存储数据
我们现在有了必要的背景知识来讨论数据是如何在计算机存储器中存储的。首先,需要了解存储器(memory)[3]的组织形式。计算机通常配备了以下两种用于存储程序指令和数据的存储器。
[3] 在本书中,memory一词会根据具体上下文,翻译为“存储器”或“内存”。
随机存取存储器(Random Access Memory,RAM)
一旦某个位(开关)被设置为0或1,该状态将一直保持不变,直到控制单元主动去改变或者断电。控制单元可以读取和改变位的状态。
“随机存取存储器”这个名字容易引起误解。这里的“随机存取”意思是说访问存储器中任意字节所花费的时间都是一样的,并不是说读取字节时的随意不定。不妨将RAM与顺序访问存储器(Sequential Access Memory,SAM)作一个对比,对于后者,访问字节所花费的时间取决于该字节在访问顺序中所处的位置。你可以将SAM看作磁带:访问字节需要的时间取决于该字节相对于磁盘当前位置的物理距离。
只读存储器(Read-Only Memory,ROM)
ROM也称为非易失性存储器(NonVolatile Memory,NVM)。控制单元可以读取但无法更改位的状态。通过专门的硬件能够对某些类型的ROM重新编程,即便是断电,位也能保持原先的状态。
2.3.1 内存地址的表示方式
内存中的每个字节都有一个位置或地址,这很像办公楼的房间号。特定字节的地址永远不会改变。也就是说,从内存开始的第957字节永远都是第957字节。但是,特定字节的各个位的状态(内容)(0或1)都是可以改变的。
计算机科学工作者通常使用十六进制表示内存中每个字节的地址,从0开始。因此,我们可以说第957字节的地址为0x3bc(十进制的956)。
内存的前16字节的地址分别为0、1、2、3、4、5、6、7、8、9、a、b、c、d、e、f。采用以下记法:
<address>: <content>
我们可以像表2-3那样显示内存的前16字节的内容(其中的内容是随意的)。
表2-3 内存前16字节的内容

每个字节的内容由2个十六进制数码表示,指定了该字节8个位的具体状态。
但是字节的状态又能告诉我们什么呢?在内存中存储数据时,程序员需要考虑以下两个问题。
● 存储数据需要多少位?要回答这个问题,我们需要知道特定数据项允许多少个不同的值。看看我们在表2-1(4位)和表2-2(3位)中可以表示的不同值的数量。可以看出,n位能够表示多达2n个不同的值。还要注意,我们未必会在已分配的存储空间内用完所有可能的位模式。
● 数据的编码是什么?我们日常处理的大部分数据并不是以0和1的形式表示的。要想将这类数据存入内存,程序员必须决定如何用0和1编码数据。
在本章余下的部分中,我们将看到如何使用一个或多个字节的位状态在内存中存储字符和无符号整数。
2.3.2 字符
在编程的时候,几乎少不了操作文本字符串,所谓的字符串就是一组字符。你编写的第一个程序可能就是“Hello, World!”程序。如果你使用C语言编写,就应该是下面这句:
printf("Hello, World!\n");
如果使用的是C++,就应该是这句:
cout << "Hello, World!" << endl;
将这些语句转换为机器码之前,编译器必须做两件事。
● 将每个字符保存在控制单元能够访问的内存位置。
● 生成机器指令,将字符写到屏幕上。
我们先来考虑如何将单个字符存储到内存中。
字符编码
最常见的字符编码标准是Unicode UTF-8。它使用1~4字节存储代表某个字符的数字,我们将这个数字称为码点(code point)。Unicode码点写作U+h,其中的h是4~6个十六进制数码。操作系统和显示硬件将一个或多个码点与字形(glyph)关联起来,字形是我们在屏幕或纸张上看到的字符外观。例如,U+0041是拉丁大写字母A的码点,在本书所用的字体中,其字形为A。
UTF-8向后兼容旧标准ASCII(American Standard Code for Information Interchange,美国信息交换标准码,读作“ask-ee”)。ASCII使用7位指定大小为128的字符集合中每个字符的码点,这个字符集合包含英文字母(大写字母和小写字母)、数字、特殊字符、控制字符。在本书中,我们在编程时只使用UTF-8的ASCII子集(U+0000至U+007F)。
表2-4展示了用于表示十六进制数的字符所对应的Unicode码点及其在内存中的8位模式。在随后学习将字符的整数表示形式转换为二进制形式的时候,你还会用到这张表。目前,要注意尽管数字字符是按照连续的位模式序列组织的,但其与字母字符之间存在间隔。
表2-4 十六进制字符的UTF-8码点

尽管十六进制的数字部分与代码点U+0000到U+007F的位模式相同,但这未必适用于其他字符。例如,U+00B5是“微”词头符号(micro sign)[4]的码点,它以16位模式0xc2b5存储在内存中,在本书所采用的字体中呈现为字形µ。
[4] “微”是国际单位制词头,指10−6(一百万分之一)。其语源为希腊语μικρός,符号是希腊字母µ。
UTF-8使用一字节存储码点在U+0000至U+007F范围内的字符。字节中的位6和位5(位从右到左编号,以0为起始)指定了该字符属于四个分组中的哪一个,如表2-5所示。这些特殊字符主要是标点符号。例如,空格字符是U+0020,而;字符是U+003B。
表2-5 码点U+0000至U+007F之间的字符分组

你可以在Linux终端窗口中输入命令man ascii来生成与ASCII字符一致的码点表(可能需要在计算机上安装ascii程序)。这张表内容非常多,不会是你想要记住的那种东西,但粗略地了解其组织形式还是有用的。
Unicode的更多信息可参见其网站。对于Unicode前世今生的详细讨论,推荐阅读Joel Spolsky的文章“The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)”。
动手实践
1.很多人使用大写字母书写十六进制字符。我知道的所有编程语言都接受大小写字母的形式。重制表2-4,列出大写十六进制字符的位模式。
2.创建小写字母字符的ASCII表。
3.创建大写字母字符的ASCII表。
4.创建标点符号的ASCII表。
存储字符串
回到“Hello, World!\n”,编译器将此字符串存储为常量字符数组。为了指明数组大小,C风格的字符串在末尾使用码点U+0000(ASCII NUL)作为哨兵值(sentinel value),这个独特的值表示字符序列的结束。因此,编译器必须为这个字符串分配 15 字节:其中 13 字节用于“Hello, World!”,1 字节用于换行符\n,1 字节用于 NUL。例如,表 2-6 展示了该字符串是如何在起始地址为0x4004a1的内存处存储的。
表2-6 内存中存储的“Hello, World!”

C语言使用U+000A(ASCII LF)作为单个换行符(在本例中位于地址0x4004ae处),即便C语法需要程序员书写两个字符\n。字符串结尾的NUL字符位于地址0x4004af处。
在Pascal中(另一种编程语言),字符串的长度由该字符串的第一字节指定,其中的值被视为8位无符号整数(这正是Pascal的字符串最长只能有256个字符的原因)。C++的字符串类提供了额外的特性,不过实际的字符串还是按照C风格字符串保存在C++字符串实例内。
2.3.3 无符号整数
因为无符号整数可以用任意基数表示,所以最显而易见的存储方式就是使用二进制数字系统。如果我们按从右到左的顺序对字节中的各个位进行编号,那么最低位为0,下一位为1,以此类推。例如,整数12310 = 7b16,所以存储该整数的字节状态为011110112。
如果只使用一字节,那么无符号整数的取值范围就被限制在了0~25510(因为ff16=25510)。在我们的编程环境中,无符号整数的默认大小是4字节,允许的取值范围为0~4 294 967 29510。
二进制系统的局限之一是在执行算术运算之前,你需要将字符串的十进制数转换为二进制数。例如,十进制数123以字符串格式存储为4字节(0x31、0x32、0x33、0x00),而按照无符号整数格式,则存储为0x0000007b。相反,为了满足现实世界的大部分显示需要,二进制数需要转换为十进制字符表示形式。
BCD(Binary Coded Decimal,二进制编码的十进制)是用于存储整数的另一种编码。每个十进制数码占用4位,如表2-7所示。
表2-7 BCD编码

例如,如果大小为16位,十进制数1234以BCD编码存储为0001 0010 0011 0100(在二进制数字系统中,则为0000 0100 1101 0010)。
16种可能的组合形式只用到了其中10种,浪费了6种。这意味着如果使用BCD,16位的取值范围为0~9 999,相较于二进制的取值范围0~65 535,BCD的内存利用率不足。使用BCD在字符格式和整数格式之间进行转换比较简单,参见第16章。
在主要处理商业数值数据的专有系统中,BCD非常重要,这类系统更多的是打印数据,而非执行数学运算。COBOL就是一门商业应用编程语言,支持压缩BCD格式(packed BCD format),其中使用8位字节保存2个数码(以BCD编码形式)。最后一个(4位)数码用于存储数字符号,如表2-8所示。具体的编码取决于实现。
表2-8 COBOL压缩BCD格式的符号编码示例

例如,0001 0010 0011 1010表示+123,0001 0010 0011 1011表示−123,0001 0010 0011 1111表示123。