共计 3771 个字符,预计需要花费 10 分钟才能阅读完成。
联合 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。