资讯动态

计算机组成原理:数据的表示与运算核心要点解析

发布时间:2026/10/15 3:21:06 来源:尧图企业网站定制
1. 为什么数据的表示与运算是绕不开的基本功很多人学计算机组成原理或者准备相关考试时看到数据的表示与运算这一章第一反应就是——这不就是进制转换吗初中信息技术课就学过了。我当初也是这么想的直到后来真正动手写底层代码、做嵌入式开发、处理浮点数精度问题的时候才发现这一章的内容几乎无处不在。你写的每一行代码底层都在做数据的表示和运算只是平时被高级语言封装得太好你感觉不到而已。这一章要解决的问题其实很朴素计算机只认识0和1那它怎么表示负数怎么表示小数怎么表示文字两个数相加的时候硬件层面到底发生了什么这些问题看起来基础但如果你不理解后面学CPU、学汇编、学操作系统的时候就会处处卡壳。我见过太多人学到浮点数那一节直接放弃就是因为前面定点数的表示没搞明白后面自然听不懂。这篇文章适合谁看如果你是计算机相关专业的学生正在学这门课那这篇内容可以帮你把课本上那些干巴巴的定义串起来如果你是已经工作但想补底层的开发者这里面的很多细节能解释你平时遇到的一些玄学问题比如为什么0.1加0.2不等于0.3为什么两个正数相加会变成负数。我会尽量用大白话把每个概念讲清楚同时把那些考试常考、工作中常踩的坑都标出来。整章的核心逻辑其实就三条线第一数据怎么表示定点数、浮点数、字符编码第二数据怎么做运算加减乘除、移位第三运算过程中会出现什么问题溢出、精度丢失。把这三条线抓住这一章就不会乱。2. 定点数的表示从原码到补码的进化逻辑2.1 为什么需要这么多编码方式先说一个很多人没想明白的问题为什么一个负数要有原码、反码、补码三种表示方法直接用一个符号位加绝对值不就完了吗答案是为了让减法变成加法。计算机硬件做加法电路是最简单的做减法电路就复杂了。如果能让减法也用加法来实现硬件设计就能大大简化。原码的符号位加绝对值这种表示方式做加法的时候符号位没法直接参与运算还得单独判断符号、比较绝对值大小非常麻烦。所以前辈们就琢磨能不能找到一种编码方式让正数和负数直接相加就能得到正确结果。反码是第一步尝试但反码有个问题0有两种表示0和-0而且运算时还需要处理循环进位。补码才是终极方案它把0的表示统一了而且符号位可以直接参与运算减法直接变成加负数。这就是为什么现在所有计算机都用补码表示整数。我个人的理解方式是补码本质上就是把数轴卷成了一个圆。就像时钟一样从12点往前走3个小时和往后走9个小时结果是一样的。在模2^n的意义下-x就等于2^n - x这就是补码的数学本质。2.2 原码、反码、补码的转换实操具体怎么转换我用一个8位整数的例子来说清楚。假设我们要表示-5。原码最高位是符号位正数为0负数为1其余位是绝对值。所以-5的原码是10000101。反码正数的反码和原码一样负数的反码是符号位不变其余位按位取反。所以-5的反码是11111010。补码正数的补码和原码一样负数的补码是反码加1。所以-5的补码是11111011。这里有个快速技巧从右往左看遇到第一个1之前包括这个1保持不变之后的所有位取反就能直接从原码得到补码。比如10000101从右往左第一个1是最低位那个1所以最低位1不变前面全部取反得到11111011。这个技巧在考试的时候能省不少时间。反过来从补码求原码操作是一样的对补码再求一次补码就得到原码。因为补码的补码就是原码这个性质很对称。注意补码表示的范围是不对称的。8位补码能表示-128到127负数比正数多一个。这是因为0只有一种表示省出来的那个编码给了-128。这个点在考试里经常考很多人会忘记。2.3 定点小数的表示与常见误区定点小数就是小数点位置固定的数。通常我们把小数点固定在符号位之后这种叫纯小数也可以固定在最后叫纯整数。定点小数的原码、反码、补码规则和整数完全一样只是小数点的位置不同。这里有个容易混淆的地方定点小数的补码最小负数是多少以8位为例符号位1位数值位7位补码范围是-1到(1-2^-7)。注意-1是能表示出来的因为补码10000000对应的是-1而不是-0。这个和整数补码里-128的道理是一样的。我在实际教学中发现很多人对定点数的理解停留在会算题的层面但一问到为什么补码能表示-1而原码不能就答不上来了。原因就在于原码里10000000表示的是-0而补码里0只有一种表示10000000就被用来表示-1了。理解这一点后面学浮点数的规格化会轻松很多。3. 浮点数的表示精度与范围的博弈3.1 浮点数的基本结构浮点数说白了就是科学计数法的二进制版本。一个浮点数由符号位、阶码和尾数三部分组成。符号位决定正负阶码决定小数点的位置也就是数的范围尾数决定有效数字也就是数的精度。这里的关键在于阶码和尾数各占多少位直接决定了这个浮点数的表示范围和精度。阶码位数多能表示的范围就大尾数位数多精度就高。但总位数是固定的所以这是一个此消彼长的关系。IEEE 754标准就是在这个博弈中找了一个平衡点。以32位单精度浮点数为例1位符号位8位阶码23位尾数。8位阶码用移码表示偏移量是127。23位尾数用原码表示而且隐藏了最高位的1因为规格化后最高位一定是1不用存。所以实际精度是24位。3.2 IEEE 754的规格化与特殊值规格化是浮点数表示的核心概念。规格化的目的就是让尾数的最高位始终是1这样就能多出一位精度。规格化的规则是尾数左移阶码相应减小直到尾数最高位为1。IEEE 754规定了几种特殊情况这些在考试和实际编程中都很重要阶码尾数表示的值全0全0±0全0非0非规格化数全1全0±无穷大全1非0NaN非数其他任意规格化数非规格化数是为了表示比最小规格化数还小的数用来实现渐进下溢。这个设计很巧妙让浮点数在接近0的时候不会突然变成0而是逐渐损失精度。实操心得在写代码的时候判断一个浮点数是不是NaN千万不要用比较因为NaN不等于任何数包括它自己。要用专门的函数比如C语言里的isnan()。这个坑我踩过调试了半天才发现问题。3.3 浮点数运算的精度问题现在来解释那个经典问题为什么0.1加0.2不等于0.3原因很简单0.1和0.2在二进制下都是无限循环小数无法用有限位数的浮点数精确表示。存储的时候就已经有误差了相加之后误差累积结果自然不是精确的0.3。这不是计算机算错了而是二进制浮点数的固有局限。那怎么解决如果对精度要求高比如金融计算绝对不能用浮点数要用定点数或者专门的十进制库。如果只是比较两个浮点数是否相等不要用而是判断它们的差值是否小于一个很小的阈值epsilon。# 错误的做法 if a b: print(相等) # 正确的做法 epsilon 1e-9 if abs(a - b) epsilon: print(相等)这个epsilon取多少合适取决于你的具体场景。一般来说如果数据量级在1左右1e-9是个不错的默认值。但如果数据量级很大比如1e10那epsilon也要相应放大。4. 字符与字符串的表示编码的演进4.1 从ASCII到Unicode字符在计算机里也是用二进制表示的这就需要编码。最早的ASCII码用7位表示128个字符包括英文字母、数字和一些控制字符。后来扩展到8位变成扩展ASCII能表示256个字符。但ASCII只能表示英文其他语言的字符怎么办于是各国搞了自己的编码比如中文的GB2312、日文的Shift_JIS。这些编码互不兼容同一个二进制数在不同编码下显示不同的字符这就是乱码的来源。Unicode的出现就是为了统一所有字符的编码。它给每个字符分配一个唯一的码点比如A是U0041中是U4E2D。但Unicode只是规定了码点具体怎么存储又衍生出了UTF-8、UTF-16、UTF-32等编码方案。UTF-8是现在最流行的因为它兼容ASCII而且变长设计节省空间。英文字符占1字节中文字符通常占3字节。UTF-16用2字节或4字节表示Java和Windows内部就用这个。UTF-32固定4字节简单但浪费空间。4.2 大小端模式与字节序多字节数据在内存里怎么排列这就涉及大小端模式。大端模式是高位字节存在低地址小端模式是低位字节存在低地址。举个例子0x12345678这个数大端模式下内存里是12 34 56 78小端模式下是78 56 34 12。两种方式没有优劣之分只是约定不同。x86架构用小端网络协议用大端。注意在做网络编程或者跨平台数据交换的时候字节序问题一定要处理好否则数据会完全错乱。我见过一个项目就是因为发送方和接收方字节序不一致调试了两天才找到原因。判断自己机器的字节序可以用一段简单的C代码int x 1; if (*(char *)x 1) { printf(小端\n); } else { printf(大端\n); }4.3 校验码检错与纠错数据在传输和存储过程中可能会出错所以需要校验码来检测甚至纠正错误。常见的校验码有奇偶校验、海明校验和CRC校验。奇偶校验最简单增加一位校验位使1的个数为奇数奇校验或偶数偶校验。但它只能检测奇数个错误不能纠错。海明校验能检错也能纠错原理是增加多个校验位每个校验位负责校验一组数据位。通过校验位的组合可以定位出错的位置。海明码的校验位数量k满足2^k n k 1其中n是数据位数量。CRC校验在通信和存储中用得最多它用多项式除法产生校验码检错能力强实现也简单。以太网帧、压缩文件、磁盘扇区都用CRC。校验方式检错能力纠错能力典型应用奇偶校验奇数个错误无简单通信海明校验多位错误1位错误内存ECCCRC校验强无网络、存储5. 定点数运算加减乘除与溢出判断5.1 补码加减法补码加减法的规则很简单减法变加法符号位一起参与运算最高位的进位丢弃。这就是补码设计的精妙之处。比如计算5 - 3变成5 (-3)。5的补码是00000101-3的补码是11111101相加得到00000010也就是2。最高位有进位但丢弃了结果正确。那怎么判断溢出溢出的本质是结果超出了表示范围。对于补码加法有两种判断方法第一种是看符号位两个正数相加得到负数或者两个负数相加得到正数就是溢出。一正一负相加永远不会溢出。第二种是看进位最高位进位和次高位进位不同就是溢出。这个可以用异或门实现硬件上很高效。我个人的经验是考试的时候用第一种方法更快但做硬件设计的时候用第二种更直接。5.2 移位运算移位运算分为算术移位、逻辑移位和循环移位。算术移位用于有符号数左移补0右移补符号位。逻辑移位用于无符号数左右移都补0。循环移位是把移出的位补到另一端。左移一位相当于乘2右移一位相当于除2。这个性质在优化代码的时候很有用比如乘2的幂次可以用左移代替编译器通常会自动做这个优化。但要注意左移可能会导致溢出。比如8位补码127左移一位变成-2因为符号位被移进去了。这个坑在写底层代码的时候要特别小心。5.3 乘除法运算定点数的乘法可以用移位和加法实现。原码乘法是符号位异或数值位相乘。补码乘法复杂一些有Booth算法等优化方法。除法更复杂有恢复余数法和不恢复余数法加减交替法。恢复余数法思路简单但效率低不恢复余数法效率高但逻辑稍复杂。在实际的CPU设计中乘除法通常由专门的硬件单元完成不需要软件模拟。但理解这些算法的原理对理解CPU的工作方式很有帮助。实操心得如果你在做嵌入式开发发现乘除法特别慢可能是因为你的芯片没有硬件乘法器编译器在用软件模拟。这时候可以考虑用移位和加法来优化关键代码。6. 浮点数运算对阶、尾数运算与规格化6.1 浮点数加减法的步骤浮点数加减法比定点数复杂得多因为要处理阶码和尾数两部分。完整步骤是对阶、尾数加减、规格化、舍入、溢出判断。对阶是让两个数的阶码相同小阶向大阶看齐。为什么是小阶向大阶因为如果大阶向小阶看齐尾数左移会导致高位丢失误差更大。小阶向大阶看齐尾数右移丢失的是低位误差更小。尾数运算就是定点数加减法因为阶码已经对齐了。运算完之后可能不是规格化的需要规格化。左规是尾数左移阶码减1右规是尾数右移阶码加1。舍入有几种方式就近舍入、朝零舍入、朝正无穷舍入、朝负无穷舍入。IEEE 754默认是就近舍入也就是四舍五入。6.2 浮点数乘除法浮点数乘法简单一些阶码相加尾数相乘然后规格化。除法是阶码相减尾数相除然后规格化。乘除法不需要对阶因为阶码的运算直接就是加减。但要注意阶码的溢出问题阶码相加可能上溢相减可能下溢。6.3 浮点数运算的精度损失浮点数运算的每一步都可能损失精度。对阶时尾数右移会丢失低位规格化时左移可能丢失高位舍入时也会引入误差。这些误差累积起来可能导致最终结果和理论值相差很大。有一个经典例子计算1到100000000的倒数和正序加和倒序加的结果不一样。因为正序加的时候大数加小数小数被吃掉了倒序加的时候小数先累积成大数再和大数相加精度损失更小。这个现象在数值计算中很常见叫做大数吃小数。解决办法是排序后再加或者用Kahan求和算法。# Kahan求和算法 def kahan_sum(numbers): total 0.0 compensation 0.0 for num in numbers: y num - compensation t total y compensation (t - total) - y total t return total这个算法用一个补偿变量来记录每次加法丢失的低位下次加法时补回来。虽然不能完全消除误差但能显著提高精度。7. 常见问题与排查技巧实录7.1 整数溢出问题排查整数溢出是实际开发中最常见的问题之一。比如两个大整数相加结果变成负数数组下标计算溢出导致越界访问。排查方法首先确认数据类型的范围然后检查运算过程中是否可能超出这个范围。对于加法可以检查两个操作数的符号和结果的符号是否一致对于乘法可以检查结果除以一个操作数是否等于另一个操作数。避坑技巧在做金融计算或者涉及大数的场景不要用int或long直接用BigInteger或者类似的任意精度类型。虽然性能差一点但安全。7.2 浮点数比较问题前面说过浮点数不能用比较。但具体怎么写很多人还是搞不清楚。正确的做法是判断差值是否小于epsilon但epsilon的选取要根据场景来定。如果比较的是经过大量运算的浮点数epsilon要适当放大。如果比较的是从文件读取的浮点数epsilon可以小一些。没有万能的值需要根据实际情况调整。7.3 字符编码乱码问题乱码的本质是编码和解码用的字符集不一致。比如文件是UTF-8编码的你用GBK去读就会乱码。排查方法首先确认文件的真实编码可以用十六进制编辑器查看字节内容。然后确认读取时用的编码是否匹配。在Web开发中还要注意HTTP头里的Content-Type和HTML里的meta标签是否一致。问题现象可能原因解决方法中文显示为问号编码不支持中文改用UTF-8中文显示为方块字体不支持安装中文字体部分中文乱码混合编码统一编码所有字符乱码编码完全不匹配确认源编码7.4 字节序问题排查字节序问题通常出现在网络通信和跨平台数据交换中。排查方法是打印出原始字节看看排列顺序是否符合预期。如果是网络通信发送方和接收方要约定好字节序通常用网络字节序大端。如果是文件存储要在文件头里标明字节序或者统一用一种。实操心得在做协议设计的时候一定要在文档里明确字节序否则后期联调会非常痛苦。我见过一个团队因为没约定字节序两边各自按自己的习惯实现结果数据完全对不上排查了一周才发现问题。7.5 校验码失效问题校验码失效通常是因为校验范围不对或者校验算法实现有误。比如CRC校验多项式选错了或者初始值、异或值设置不对都会导致校验失败。排查方法用已知正确的数据测试校验算法确认算法本身没问题。然后检查校验范围是否覆盖了所有需要校验的数据。最后检查发送方和接收方的校验参数是否一致。这一章的内容看起来基础但真正吃透需要反复琢磨。我在实际工作中遇到底层问题的时候经常要回来翻这一章的内容。数据怎么表示、怎么运算这些看似简单的问题背后都有精妙的设计。理解这些设计不仅能帮你应付考试更能让你在遇到实际问题的时候知道问题出在哪里为什么会出现怎么去解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑