联合CMU CSAPP课程和本人看的教材做的笔记。
数据类型大小
C的不同类型数据结构的大小。记住几个罕用的,char是1个byte,short是2个,int是4个。
C里的位运算和逻辑运算不要搞混。比方&和&&,&是按位与,&&是且运算,位运算的后果仍是一个不同的数值,逻辑运算的后果不是0就是1。
左移和右移
位左移会把高位的移除,低位的补零。
右移有逻辑右移和算术右移。
逻辑右移和左移相似,低位移除,高位补0。算术右移低位移除,高位补的是符号位。
补码(符号数示意)
正数示意的思路是,最高位作为符号位,因为当最高位取为1的时候,正好能够把这串比特示意的数字范畴拆成两半。比方我打算用4个比特示意数字,能够示意16个,1000的十进制是8,那么在1000以下的和1000以上的各能示意8个数字,就能够别离示意负数和正数了。
如下,16位示意的无符号数和有符号数的范畴。最大的正数是最高位1其余0,最大的负数是最高位0其余1,所以在负数的最大值再加个1,溢出的数值就是最大的正数。
上面的公式示意了二进制位的无符号和有符号数的公式。公式就是二进制转十进制的加权求和,留神有符号数,符号位以下的位的权重和无符号数是一样的,只有符号位的权重不一样,带了负号。
C语言的<limits.h>里的宏ULONG_MAX,LONG_MAX,LONG_MIN能够取得本机器上符号数的范畴。
常量无非凡申明的状况下默认是按有符号示意,如果前面加上U则是无符号,如0U,1234U。
类型转换
C语言中,显式类型转换就是用显式地写进去的转换操作,比方用了函数,或者(int)a这种模式。隐式类型转换就是没有显式地写进去,但为了执行命令零碎主动做的转换,常产生在赋值和逻辑比拟中。
比方int a和unsigned int b,令a=b就产生了隐式类型转换,b会被转换成有符号类型,再赋给a,或者执行a<b,也会产生隐式转换。
有符号类型和无符号类型混用,无非凡申明的状况下,总是有符号会被转换成无符号。例如,如果-1和0U做比拟,后果是-1大于0U,因为-1的位级示意是全1,对应的无符号数就最大的负数。所以,当无符号数和有符号数混用时,都是负数没有关系,要小心有符号数是正数的状况。
老师补充了编程中典型的例子。上面是个死循环,因为i是无符号的,所以永远会是负数。位级的解释就是i最初减小到0U,再减去1后的-1是全1,对应的无符号数就是最大负数。
unsigned i;for(i=n-1;i>=0;i--){ func(a[i]); //do anything}
再来个坑。如果刚开始把i申明成有符号了,然而循环条件这么写:
int i;for(i=n-1;i-sizeof(char)>=0;i--){ func(a[i]); //do anything}
结果也是死循环。因为sizeof返回的是无符号数,i和它做运算的后果始终还是无符号数。所以要分外留神隐式转换。
拓展和截断
从k位的数字拓展到k+N位的数字,放弃数值大小不变,只有对符号位拓展就行了。能够本人用之前的加权求和公式计算一下,实际上拓展出的N位符号位,和只有1位的后果正好是一样的。
因而,C对短类型转长类型,比方short转int,做的事件就在后面拓展符号位(精确地说有符号数才有符号位,意思晓得了就行)。
而从长位到短位的数据转换,如uint转ushort,会把高位的数据间接截断,在数值上的后果就是做了一次对高位代表的权重的取余。
举个例子,思考无符号数,11011是16+8+0+2+1=27,截掉最高位1,失去1011,1011=8+0+2+1=11,11就是27mod16。
而对于有符号数,截断的过程中正数可能会变正,负数可能会负,要分外留神,如int转short。
整数加减法,溢出
计算两个无符号数相加,像十进制那样做进位加法就能够,然而可能会存在最高位有进位的状况,也就是说,两个N位的数字相加,最终后果有时须要N+1位保留,然而容量只有N位,机器的做法是舍去多进去的一位,只保留N位,这就引出了溢出问题。
假如有4个bit示意无符号整数,能示意的范畴是0到15,如果计算8+12,实践上后果是20,但理论后果是4,产生了溢出,溢出的法则是:实践后果mod/舍去位的权重,即4=20mod16。
对于有符号整数的情景,如果也用4个bit示意,能示意的范畴是-8到+7,如果计算7+1,实践后果是8,理论后果是0111+0001=1000,这是最大的正数,即-8,产生了正溢出。如果计算(-8)+(-1),实践后果是-9,理论后果是1000+1111=0111,这是最大的负数,即7,产生了负溢出。不难发现溢出的法则是,从相同的最大值方向再从新轮次,刚好产生正溢出时会变成最大的正数,反之一样。
乘除的后果是相似的,产生溢出后的理论后果也是相当于做了模操作。
总结一下,产生溢出时做的操作就是高位截断,所以如果计算溢出后的理论后果的值,只有先计算出它的实践后果(二进制示意),而后依据它的数据类型的容量位数去截断高位,保留下来的就是理论后果。
幂
数据左移k位就相当于乘以2的k次幂,这是编程中常见的操作。如0011=3,左移1位就是0110,正好是6,再左移一位就是1100=12。解决2的k次幂乘积运算时,编译器就会将其优化为左移运算。
如果是做2的k次幂的除法,也只有右移即可,如果无奈整除,会向下取整。留神对于有符号数,是算术右移,然而本人计算一下理论后果不是向下取整,是向上取整了,所以零碎的做法是先对被除数加一个偏移量1,而后再做除法,这样最终的后果也是向下取整模式的,放弃了对立。
相反数
想得到一个有符号数x的相反数-x,只有对其取反再+1。其实就是补码的一种疾速计算方法。
小端&大端
指的是字节存储程序,有小端模式和大端模式,目前绝大多数机器应用小端模式,如x86的机器,ARM处理器。
假如有个4字节数0x01234567,直观上看,咱们认为这个数字是从左到右的,也就是说右边是数据的低位,左边是数据的高位,存储时内存地址从低到高也对应数据的从左到右。大端序就是这么存储,然而小端序反了过去,左边的数字会被存储到地址的低位,即67存在最低位,01存在最高位。
浮点数
浮点数的示意办法借鉴了迷信计数法的思维,迷信计数法是如下的模式:
只须要正负号,小数和指数三个局部就能够示意一个十进制数。同理,二进制数也能够示意成:
S决定正负号,称为符号位,F决定小数局部,称为“尾数”,E决定指数局部,称为“阶码”,或者指数。
如果用32位的二进制串示意,能够调配如下的格局:
单精度浮点数,阶码有8位。
十进制浮点数转二进制示意的公式,尾数不难理解,要害是了解指数局部的“偏移量”的概念。
其实想对指数局部编码,当然能够沿用补码的模式,假如指数是-1,就用1111 1111示意,-128就用1000 0000示意,+1就用0000 0001示意,+127就用0111 1111示意。只不过这样干的话,比拟两个浮点数的大小时不够直观,对于两个规格化的浮点数,符号雷同时,指数大的那个会大一个量级以上,单从数值上看,1000 0000是比0000 0001要大的,但转补码后,实际上前者比后者小。另一方面,纯0,无穷大等,十分十分靠近0的小数等非凡的数字,怎么示意进去?
于是IEEE想的方法就是扭转一下映射关系。咱们心愿用一种新的编码方式示意指数局部,叫“阶码”,使得对于指数,原来是正的a>b,转二进制后仍是a>b,负的c>d,转二进制后仍是c>d,且a>b>c>d,说白了,像无符号整数那样不扭转递增关系。因而,阶码是不像补码那样有个符号位的,它是这么示意的:
1000 0000用来示意+1,向上递增就是正整数,以下的局部就是负整数。这样正好把示意范畴拆成两半。察看这个二进制示意,和+1本来的补码0000 0001相差多少?相差127(0111 1111),即0000 0001 + 0111 1111 = 1000 0000。就是说——
阶码 = 补码 + 127
一旦建设这个新的映射关系,从负到正的大小关系就能够示意成相似无符号数的模式了。那么,计算一下,-1的阶码是1111 1111(补码) + 0111 1111(偏移量)= 0111 1110,0的阶码是紧随当前的0111 1111。那么全0用来示意什么了?
全0拿去编码非凡数字了,即浮点数纯0和非规格化数。当阶码为0000 0000,尾数也为0时,示意浮点数0,尾数不为0时,示意非规格化数。
另外,全1也拿去编码非凡数字了,即无穷大。当阶码为1111 1111,符号位为正时,就示意正无穷,符号位为负时,就示意负无穷。
所以阶码的编码方式就是:
- 非凡数字:0000 0000(0或非规格化数),1111 1111(无穷大)
- 负指数:0000 0001 ~ 0111 1110 (示意-126~-1)
- 指数为0:0111 1111
- 正指数:1000 0000 ~ 1111 1110 (示意+1~+127)
以上都是单精度的例子,对于双精度也一样。
双精度的阶码长度为11,+1的阶码示意当然仍是最高位为1,即100 0000 0000,和本来的补码示意000 0000 0001(也拓展到11位),相差的偏移就是011 1111 1111,即1023。能够看进去了,如果阶码有k位,偏移量就是2^(k-1)-1。