CS:APP Data Lab实验
本文是CS:APP学习笔记的一部分:
相关的代码都放在了GitHub下了:RayZhang13/CSAPP-Labs
久仰《深入理解计算机系统CS:APP》的大名,可惜之前自己一直是个懒狗,一听有人抱怨难就识趣的开溜了,哦那个时候编程也没开窍啊,那没事了…
原版视频在这里,如果英语好的小伙伴可以试试呢:
2015 Fall: 15-213 Introduction to Computer Systems
网课界面还是做的相当好的,幻灯片、摄像头、投影都可以按照时间轴同步播放,体验极佳:
总之这下得好好的把CS:APP这本书过下来,首先得感谢B站上的CMU课程视频和相关的翻译,链接在这里:
【精校中英字幕】2015 CMU 15-213 CSAPP 深入理解计算机系统 课程视频
字幕里面看起来还是有少许错误,这是作者的翻译项目GitHub链接:中英双语字幕精校版 CSAPP CMU 15-213 课程 2015 Fall 视频翻译计划,那是真的牛皮👍
另外关于习题课Recitation,这里也有视频,一条龙了属于是(可惜没有中文字幕,好消息是有英文字幕):
2015 CMU 15213 CSAPP 深入理解计算机系统 习题课视频
准备
对应课程
这次的Data Lab作业,如果是自学,在刚才的B站课程中,你需要完成P2~P4的课程学习掌握相关的概念。
课程文件
相关的作业可以在CMU的官网上找到:
在Data Lab一栏里面,我们可以查看相关文件,例如:
下载后并解压的文件如图所示:
关于接下来的使用可以看一下文件夹里的README,配合刚才的PDF食用更佳。
使用环境
我们主要通过btest
、dlc
、fshow
、ishow
和driver.pl
这几个文件来完成作业相关的样例测试和打分。
看起来macOS对这里头的32位代码检查工具dlc
支持有亿点点小问题,所以我们还是整一个正经的Linux环境吧~
我在这里使用了Arch Linux在VMware下的虚拟机:
使用的GCC版本为11.1.0:
我们将文件传到虚拟机里面,作业都在bits.c
文件里,我们按照要求完成代码保存即可。
(理论上)写完代码之后在文件夹下运行make
之后,例如调用driver.pl
就可以完成代码检查了。如图:
这里我们每道题都还没动,所以最后的评分都是0分,也算是挺智能的打分系统了hhh
可能的坑
先说结论,建议将Makefile中的参数稍加修改成如下形式:
64位系统
我们的Linux系统是x86_64的,如果无法成功make
可以考虑将Makefile
里面的gcc
参数中的-m32
修改为m64
。
未定义行为
根据Wikipedia中的定义,在计算机程序设计中,未定义行为(英语:undefined behavior)是指执行某种计算机代码所产生的结果,这种代码在当前程序状态下的行为在其所使用的语言标准中没有规定。常见于翻译器对源代码存在某些假设,而执行时这些假设不成立的情况。
例如,在整数运算过程中,对一个整型进行大于31位的移位就是一个未定义行为,编译器可以自由发挥进行优化,这点在Recitation习题课上16:10时刻,也被提及了。
另外,需要说明的是,在C语言中带符号的整数溢出是未定义行为,编译器会假设不会发生整数溢出这样的情况,因此编译时的可能会做出特殊的优化。我们不能理所应当的认为由于所以处理器都使用二进制补码表示法就直接对数字进行随意的溢出运算,这样会导致奇怪的结果。
而在Data Lab中,由于我们经常性的需要对一个32位的整型进行加法、与运算、或运算等,我们可能有意或者无意的会利用到相关的补码运算性质,可能会发生带符号整型溢出这样的情况,这点是我们需要警惕的。
为了让程序严格遵守正常的数学逻辑,在溢出后保证截断行为,将溢出位进行抛弃,我们需要在Makefile
中的gcc
参数下添加-fwrapv
。
在知乎上我也找到了做Data Lab时有类似同学的迷惑:为什么只是加了printf得到的结果就不同了?
我在自己的机器上也做了几个小实验,我分别运行两次这样的代码,结束后都使用GCC不带参数进行编译:
1 | //代码A |
1 | //代码B |
结果为:
按照我们的理解,0x80000000
对应的二进制是1000 0000 0000 0000 0000 0000 0000 0000
,如果我们将两个0x80000000
相加,我们就会发生溢出,理论上说去掉溢出的进位,我们会得到结果0
。
但是为什么当我们进行是否为0
的判断时,两端看起来功能完全一致的代码结果却不同呢?
我们使用GDB分别对两次生成的执行文件进行反编译,为了清晰Intel和AT&T的语法都打印了一下,结果为:
在代码A的反汇编过程中,我们看到执行时我们甚至都没有使用add
命令进行加法运算,简单的过程大概就是在<+8>
位置将0x80000000
写进内存地址rbp-0x4
的4字节空间里了(相当于完成了int x = 0x80000000;
)(PS: DWORD代表双字,也就是4字节,[]
中存储的是地址),然后在<+15>
位置将地址里面存储的0x80000000
直接和0x0
使用cmp
指令进行了比较,然后就出了结果。
大致猜测,编译器觉得如果需要确定x + x
是否是0
,编译器默认觉得不会发生溢出这种蠢事,既然我们x + x
对中间过程又没啥要求,干脆不算了,就直接优化成了x
是否为0
,而我们都知道0x80000000
不是0
,所以直接打印出了一个false
。
而在代码B的反汇编中,在<+8>
位置将0x80000000
写进了内存地址rpb-0x8
的4字节空间里,随后在<+15>
位置将rpb-0x8
里面的数据放入累加器eax
中,并在<+18>
使用add eax, eax
自己加了自己一次,随后在<+20>
将eax
中加出来的结果放在了新的4字节空间rbp-0x4
里面(相当于完成了int y = x + x;
),最后在<+23>
位置使用cmp
指令比较了rbp-0x4
里面存储的数字(加法溢出后应当是0x0
)和0x0
,那自然是true
。
综上,我们发现只有在代码B中,程序才认真的执行了加法运算操作,而在代码A中直接发生了优化,给出了不同的答案。这也警示我们需要注意带符号整型的溢出问题,这是一个未定义行为(UB),我们不能理所因当的觉得程序会对应的做出溢出后的截断等操作,事实上编译器可能会自己偷偷的做出了意想不到的优化。
使用建议
评分工具
在bits.c
文件中,给出了每道题目的相关要求,下方的函数是你需要做的作业,这里没做的情况下,老师随便填了一堆return xxx
,这显然是不可能对的。
例如:
第一题,题目名字叫做bitXor
,要求实现两数按位的异或,要求为:
- 可以使用的运算符限制在
~
和&
- 运算总步数限制在
14
次 - 整型常数最大只能用到
0xff
,不能用0xffffffff
这样的数;禁止全局变量减少总步数偷鸡(最顶上还有点条件别漏看了)
只有同时满足了答案的正确性和限制条件,这道题才会被判定为得分。
driver.pl
到时候运行的时候得分会全部显示出来,像这样(当然这是我做完后的样子了):
那么如果好巧不巧,你的做了某道题但是在对应的Rating
里面看不到得分(也就是0),说不准旁边还爆出了几个Error
,那该怎么办呢?
例如:
其实大可不必摸瞎排查问题,通不过总评分测试driver.pl
,说明我们的答案的正确性和限制条件可能没有得到满足,这个时候我们调用btest
工具,进行正确性分析:
他会把每道题目带入样例里面没通过的情况打印给你看,信息相当详细,精确到传入参数是什么,你的答案是什么,正确答案是什么。也是方便你进行排查错误。
面对偷鸡方法,我们还有dlc
进行条件检查限制,比如在第一题bitXor
中,我们直接使用return x ^ y;
,这样的话可以满足答案正确性,却没有满足限制条件。例如修改后运行:
我们看到driver.pl
中第一题仍然没有得分,但是调用btest
已经成功通过第一题了,这个时候我们调用./dlc -e bits.c
,就能看到问题:
对于满足正常限制条件的题目,dlc
会正常打印出步数相关信息,但是对于不满足限制条件的会直接警告,例如这里第一题:dlc:bits.c:146:bitXor: Illegal operator (^)
,你不应该使用^
运算符。
通过这三款工具我们就能很好的应对做题中的问题了,并能快乐的开始调试了。
当然,需要说明的是,测试样例只能证明你是错的,通过所有的测试样例却不一定能说明你是做对的。做题时一定要大胆假设小心求证。
做题小工具
此外,文件夹里还提供了两款小工具,分别是ishow
和fshow
。
ishow
功能很简单,每次你传进去一个数字,他就能给你显示这个数的16进制表示形式,作为无符号数是多少,有符号数是多少:
fshow
的功能也是类似的,你传入一个数字,他会将它转成32位二进制形式并套用在单精度浮点数的模版上,然后生成一个对应的浮点数:
题目
整型运算
首先还是贴一下做题的约束和规范,这块你在bits.c
文件的头部也能找到:
1 | INTEGER CODING RULES: |
大概意思就是:
-
按照题目要求给出的允许符号完成,并且要在规定步数内。
-
运算中带入的整型常数数字最大是
0xff
。 -
可以分行多步完成。
-
不要使用全局变量偷鸡。
bitXor按位异或
1 | /* |
这道题目要求我们通过~
和&
来完成两数的按位异或。
根据定义:
最后一步我们使用了德摩根律:
这是一位的运算,推广到按位运算上也是一样的操作,因此:
1 | int bitXor(int x, int y) { |
tmin求最小带符号整数
1 | /* |
最小带符号整数应当是,二进制补码表示为1000 0000 0000 0000 0000 0000 0000 0000
,16进制为0x80000000
。
很容易的我们得出,直接将1
左移31
位即可实现。
1 | int tmin(void) { |
isTmax判断最大带符号整数
1 | /* |
如果输入参数是最大带符号整数,就应当返回1
,否则返回0
。
要完成这道题目,我们最好还是研究一下这个数字有啥特殊之处,他的16进制表示为0x7fffffff
,二进制表示为0111 1111 1111 1111 1111 1111 1111 1111
。
我们注意到,这个数,如果我们对它进行加一,它就变成了,二进制为1000 0000 0000 0000 0000 0000 0000 0000
,16进制为0x80000000
,恰好为原数的翻转(每一位都翻过来了)。这个性质很少见,但是还有一个例外,那就是-1
也满足这个性质,他的二进制是1111 1111 1111 1111 1111 1111 1111 1111
,16进制是0xffffffff
,如果我们对-1
加一,就得0x0
,也刚好是翻转。
我们需要做的事情有两件:
- 判断参数满足性质(加一后是自身的翻转)
- 判断参数不是
-1
判断两数是否相等,我们可以使用!(x ^ y)
,只有两数每一位都相等,x ^ y
两数异或的结果才可能是0
,这个时候我们使用!
判断此数是否是0
即可,是0
即返回1
,否则返回0
。
但是判断参数是否不是-1
,我们无法使用!!(x ^ 0xffffffff)
来实现,原因是存在限制最大计算常数只能是0xff
,这个不合题意了。那我们直接判断x + 1
是否不为0
即可,也就是!!(x + 1)
。
综上答案为:
1 | int isTmax(int x) { |
此外我们还可以利用的特殊性质包括但不限于:加一后乘二(或者说加一后加自己)是0
,例外情况:0xffffffff
。也就是!(x + 1 + x + 1) & !!(x + 1)
。
P.S. 这里可能会遇到带符号整数溢出的情况,可能会发生结果和预期不符的情况,建议查看上方“准备”部分的填坑指南,添加上
-fwrapv
参数,然后再试一次。
allOddBits判定是否所有奇数位都是1
1 | /* |
题目要求我们判定是否所有奇数位都是1
,满足则返回1
,否则0
。
这里我们要做的事情可不是简单的判断x
是否和二进制1010 1010 1010 1010 1010 1010 1010 1010
,16进制0xaaaaaaaa
相等,这个数中间的0
位是可以取1
的。这里头的关系更像是判断一个包含的关系,也就是x
作为位图是否包含0xaaaaaaaa
中的所有位。
解决办法也比较简单,我们先进行x & 0xaaaaaaaa
,如果x
包含0xaaaaaaaa
,那么显然x & 0xaaaaaaaa
应当和0xaaaaaa
相等。因此答案暂时就是!(x & 0xaaaaaaaa) ^ 0xaaaaaa
。
但是按照题意,0xaaaaaaaa
我们不能直接使用(常数大小限制在0xff
),得造出来,其实这也简单,我们直接通过移位相加把0xaa
变成0xaaaa
,再同样的方法将0xaaaa
变为0xaaaaaaaa
。
代码如下:
1 | int allOddBits(int x) { |
negate取相反数
1 | /* |
这就是一个很著名的结论了,对于一个整型x
,我们知道他的翻转~x
应该会把他的每一位都翻过去,也就是0
变1
,1
变0
。
如果我们将x
和~x
相加,我们就会得到一个所有位都是1
的二进制数,也就是1111 1111 1111 1111 1111 1111 1111 1111
,16进制为0xffffffff
,我们也称这个数叫做-1
。
因此结论就是x + ~x = -1
,稍加变化我们就得出了-x = ~x + 1
。
代码如下:
1 | int negate(int x) { |
isAsciiDigit判断ASCII码是否是数字
1 | /* |
ASCII码如要表示'0'
~'9'
,那对应的数字应当在0x30
~0x39
的范围内,如果x
在范围内则返回1
,否则返回0
。
0x30
~0x39
的范围内的数在形式上大致满足0x0000003?
的形式,二进制满足0000 0000 0000 0000 0000 0000 0011 ????
的模式。
我拿到这道题目首先想到的是两个问题(假设我们从右向左开始数数,最右位为第0
位):
- 确定第
4
位到第31
位读出来的数等于3
- 确定第
0
到3
位读出来的数,落在0
~9
的范围内。
完成第一个目标还算简单,首先我们把左侧的这个数通过右移取出来,也就是x >> 4
,如果要我们判断他是否是3
,我们只需要!((x >> 4) ^ 3)
即可。
关键是完成第二个目标,也就是判断x
的右侧4
位二进制是否落在0
~9
的区间里,首先我们可以通过x & 15
(也就是和1111
取与)的方式取出最右侧4
位二进制,在这里我们假设他是b
。
这里我们将所有可能的情况列出,并绘制卡诺图。
得出的结论就是只要第3
位是0
或者第1
第2
位同时为0
即可,任选其一即可。
表示成代码就是!(b & 8) | !(b & 6)
,8
的二进制是1000
,如果b & 8
为0
,说明第3
位是0
。同理6
的二进制是0110
,如果b & 6
为0
,说明第1
第2
位同时是0
。两个条件满足其一即可。
综上代码为:
1 | int isAsciiDigit(int x) { |
这里看到网上有更加简单的做法:
主要思路可以用逻辑运算表示,(x - 0x30 >= 0) && (0x39 - x) >=0
,而-x
在刚才的题目中的结论可以表示为~x + 1
。
关于如何判断一个数t
是否大于等于0
,我们只需要检查的他的符号位就可以了,也就是看他的第31
位是不是0
,我们通过!(t & (1 << 31))
即可完成,这里就不赘述了。
所以代码如下:
1 | int isAsciiDigit(int x) { |
但是这种做法是否存在溢出的问题,也是值得讨论的。如果看看下面的“isLessOrEqual判断小于或等于关系”,希望能给你一些启发。
conditional实现三元表达式
1 | /* |
如果x
为0
我们就取z
,否则就取y
,这就是三元表达式的简单逻辑。
我们知道一个数和0xffffffff
进行与运算还是自己,和0x0
进行与运算就变成了0
,利用这一点来展开。
这里是我自己的思路:
通过这样的方式,我们确定了g(x)
的具体表达式,带入后得到代码:
1 | int conditional(int x, int y, int z) { |
当然啦,(b & y) + (a & z)
也可写作(b & y) | (a & z)
。
isLessOrEqual判断小于或等于关系
1 | /* |
如果x <= y
就返回1
,否则返回0
。
这道题目,看起来可以简单演变成判断y - x >= 0
,然后顺利的演变成y + ~x + 1 >= 0
,然后快进到判断符号位。
但是这里还是有例外,举个例子,极端情况下,我们将x
取为,将y
取为。这个时候x
为0x80000000
,y
为0x7fffffff
,如果我们尝试进行y - x
(也可以说成是y + ~x + 1
),答案会是-1
,而我们在实际运算时知道这个答案是,是个正数,换句话说y - x
发生了溢出导致了不准确,溢出的部分是。
同理,我们将x
, y
颠倒,得出的结论也是错误的,事实上,如果或者就会发生溢出错误。会发生正数变负数,负数变正数的尴尬情况。
解决方案自然就是特殊讨论,我们直接将异号的情况单独判断,如果x < 0 && y >= 0
,直接返回1
;反之x >= 0 && y < 0
直接返回1
,正负的探讨可以直接通过符号位来确定。通过同号比较,我们将y - x
的范围削减到,满足了要求。
取出一个数t
的符号位方法很简单,t >> 31 & 1
即可,&1
的原因是对于1
在最左侧开头的数,我们执行算术右移,会把1
填充进来,最后的结果是0xffffffff
。如果我们要判断当前数字是否>= 0
,直接使用!(t >> 31 & 1)
即可,但是这么一想又何必&1
呢,!(t >> 31)
也不是不能用。
我们根据刚才的逻辑形成了这样的代码:
1 | int isLessOrEqual(int x, int y) { |
根据这样的逻辑,如果x >= 0 && y < 0
,那么!(!a & b)) == 0
,后面的运算不管怎么样都是0
(因为0&x=0
必成立)。
如果不满足,则!(!a & b)) == 1
,进入下一层判断,如果x < 0 && y >= 0
,那么(a & !b) == 1
,那么答案就是1
(因为1&(1|x)=1
必成立)。
如果还不满足,则(a & !b) == 1
,最后的决定权交由y + ~x + 1 >= 0
(因为1&(0|x)=x
必成立)
logicalNeg逻辑非
1 | /* |
这里题目要我们实现一个逻辑非,如果x != 0
就返回0
,否则返回1
。
那么数字0
他有什么特殊的性质呢?
在CMU的网课里,其实在幻灯片里面也有谈及,具体位置在P3的1:14:30时刻,提出了除了0
以外的数字都满足(x | -x) >> 31 == -1
,也就是(x | (~x + 1)) >> 31 == 0xffffffff
。而只有0
计算出来是0
((0|0) >> 31
还是0
)。
判断一个数是0x0
还是0xffffffff
,我们只需要和0x1
进行与运算,把最右一位取出来取反即可。
利用这一点,我们完成如下代码:
1 | int logicalNeg(int x) { |
howManyBits表示一个数的最少位数
1 | /* howManyBits - return the minimum number of bits required to represent x in |
根据题意,例如:
5
可以表示为二进制0000 0000 0000 0000 0000 0000 0000 0101
,但是在符号为保留的情况下,我们可以直接表示为0101
,也就是4
位。
-5
可以表示为二进制1111 1111 1111 1111 1111 1111 1111 1011
,但是在符号位保留的情况下,可以直接表示为1011
,还是4
位。
我们大致总结出了如下的规律:
- 如果
x < 0
,则x
二进制开头为1
,我们需要寻找的是从左到右的第一个0
的位置,然后加一来表示一位符号位填充。 - 如果
x >= 0
,则x
二进制开头为0
,我们需要寻找的是从左到右的第一个1
的位置,然后加一来表示一位符号位填充。
换句话说,如果x < 0
,我们对x
全部翻转,变成~x
。然后所有的数字都是0
开头了,我们统一遵守:
从左到右的第一个1
的位置,然后加一来表示一位符号位填充。
关于如何实现翻转的使用的是这个,可以仔细体会一下,其实原理和上方那个“conditional实现三元表达式”类似:
1 | int sign = x >> 31; |
关于寻找最右侧的1
,我们采用二分来进行:
- 先判断
32
位的x
的左半侧有没有1
?有就先+16
,然后将数字左半边移到右对齐;否则,说明没有16
位,不移位,直接研究右半边。(接下来研究x
最右侧16
位就相当于研究原数的左半侧/右半侧) - 先判断
16
位的x
的左半侧8位有没有1
?有就先+8
,然后将数字左半边移到右对齐;否则,说明没有8
位,不移位,直接研究右半边。(接下来研究x
最右侧8
位就相当于原数某一半的左半侧/右半侧) - …以此类推
- 最后还要加上一位符号位
代码实现如下:
1 | int howManyBits(int x) { |
浮点计算
关于浮点计算题的具体规则和约束如下:
1 | FLOATING POINT CODING RULES |
在浮点计算题中,我们几乎没有特别严苛的约束。if
等语句也可放心使用,大常数也可以直接介入运算。
这里为了对照方便,我直接将维基百科上的表放这里以供实时参考。
类别 | 正负号 | 实际指数 | 有偏移指数 | 指数域 | 尾数域 | 数值 |
---|---|---|---|---|---|---|
零 | 0 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | |
负零 | 1 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | |
1 | 0 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | |
-1 | 1 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | |
最小的非规约数 | * | -126 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0001 | |
中间大小的非规约数 | * | -126 | 0 | 0000 0000 | 100 0000 0000 0000 0000 0000 | |
最大的非规约数 | * | -126 | 0 | 0000 0000 | 111 1111 1111 1111 1111 1111 | |
最小的规约数 | * | -126 | 1 | 0000 0001 | 000 0000 0000 0000 0000 0000 | |
最大的规约数 | * | 127 | 254 | 1111 1110 | 111 1111 1111 1111 1111 1111 | |
正无穷 | 0 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | |
负无穷 | 1 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | |
NaN | * | 128 | 255 | 1111 1111 | 非全0 | |
* 符号位可以为0或1 . |
floatScale2浮点数乘2
1 | /* |
题目的意思大概是传入的32
位uf
整型,我们应该视作他是单精度float
的编码方式,基于这样的编码方式,我们将它乘2
,还是以float
的编码形式输出。如果编码是NaN
,就直接返回编码。
除开NaN
,对于任意的两倍也还是无穷,所以,我们可以认为NaN
和直接返回自己就好了。两者的共同点是两者的有偏移指数exp
都是0xff
。关于偏移指数的获取,可以通过uf >> 23 & 0xff
获得。
此外如果是当前浮点数是规约数,那么要乘2
,我们只需要将exp
加一即可,表示指数加一。也就是uf + (1 << 23)
。
如果是非规约数,即exp
为0
,我们要乘2
就应该将尾数位左移一位,即使尾数位以1
开头,也是同样的操作,将1
移入exp
偏移指数中,代表变为了规约数。鉴于规约数的exp
部分全部是0
并无影响,我们其实可以考虑直接将整个数字进行左移,但是挤掉的符号位还是要补回来的。所以我们的表达式就变成了(uf << 1) + (uf & (1 << 31))
。
因此,我们得到:
1 | unsigned floatScale2(unsigned uf) { |
floatFloat2Int浮点转整数
1 | /* |
题目的意思大概是传入的32
位uf
整型,我们应该视作他是单精度float
的编码方式,基于这样的编码方式,我们将它转成整数。(NaN
和直接返回0x80000000u
)
关于精度损失问题,由于取整,小数点后的数据都不能留下,为了简单考虑,我还是偷懒了,直接向0
进行取整,也就是直接省去小数点后的数。
首先我们提取出符号位,也方便了后续的操作。即int sig = uf >> 31 & 1;
。
对于规约数来说,如果E > 31
的情况下,1.x * 2^E
位数超过32位,必然存不下,应当返回0x80000000u
。
对于E < 0
的情况,也没有啥好说的,1.x * 2^E
全是小数,化为整数就是0
。
有趣的地方在0
与31
之间,小数点具体是停留在23
位数字的中间,因此我们要掠去小数点后的数字;还是需要继续向右多画几个0
补齐32
位呢?有趣的是数据是以1.xxxxx
的方式存储的,因此数据实际上有24
位,为了得到24
位的数据,我们干脆先取出23
位,再在左侧加上一个1
,也就是uf & 0x7fffff | 0x800000
。
因此我们以23
为边界继续讨论,如果E <= 23
说明小数点还停留在23
位数字的中间(含边界)。
如果E > 23
说明,我们需要多补几个0
,干脆直接把数字左移exp - 23
位然后取出右侧31
位,补上一个符号位。
至于浮点数转整型溢出这种奇葩情况,我头实在太大,题目貌似也没说,算了别考虑了…
结果如下:
1 | int floatFloat2Int(unsigned uf) { |
floatPower2计算二次幂
1 | /* |
给了一个整数x
,要我们使用浮点编码表示2^x
。如果太小,直接返回0
,太大直接返回。
我们知道最小的非规约数只能表示到,因此如果比-149
还小直接返回0
。
在非规约数的范围内,也就是范围内,对于最小的非规约数,当x = -149
时,1
在尾数的最右侧,其他数字全部是0
,非规约数每次乘二只需要左移即可,因此返回1 << (x + 149)
。
接下来进入了规约数的范围,范围是,对于最小的规约数,有,此时偏移指数为1
,规约数的计算满足1.x * 2^E
,每次乘二只需要有偏移指数加一即可。因此返回(x + 127) << 23
。
对于超出了规约数的范围,直接返回即可,的编码为0x7f800000
。
因此我们有:
1 | unsigned floatPower2(int x) { |