一些微小的细节,虽然90%的时间你用不上:
1.溢出
2.舍掉小很多的数
本门课虽然用到汇编,但并不是为了写汇编,而是要读懂c编译后的汇编
本门课的核心是直到怎样利用好分层存储系统,从程序角度了解性能
c没有任何内存保护,因此可能导致不好的bug,需要明白怎样运作
32位机器意味着地址也是32位,64位同理
!、||、&&的特点:
1.将0视为false,任何非0视为true
2.总是返回0或1
3.调用会提前终止
4.这三个都和真正的位计算(~,|,&)不一样
左移<<,右移>>:
算术右移:首位是0,右移后高位填充的就是0,如果是1那就填充1
逻辑右移:高位填充的永远是0
大部分机器做的是算术移位
对于C,无符号数做的是逻辑移位
整数
补码与原码:
对于4、3、2、1、0位,原码(无符号数)对应的是16、8、4、2、1,补码对应的是-16、8、4、2、1(即最高位取相反数),利用这个技巧进行十进制和二进制的转换。同时,据此可以知道n位能表示的十进制数的从负(Tmin)到正(Tmax)的范围,也可以据此知道为什么一个很大的正数突然变成一个负数
该点在java和python不会用到,而c中unsigned是一个明确的数据类型,因此可以在c中声明unsigned long之类的数据类型
这一点用在for循环中极其重要。如果for循环中使用到了unsigned(例如使用到了unsigned int i、sizeof()、与无符号数进行大小比较或运算),并且是倒序(也就是for(i=n-1;i>=0;i--)
)那么程序会陷入死循环而崩溃,因为此时系统会将所有的符号数转化成unsigned无符号数,而unsigned始终大于0,就算i=0的时候减去1也依然看作大于0.
例如:-1与0,如果进行比较,在unsigned的情况下,-1>0,因为-1=1111…1,而0=0000…0。
以6位为例,31与32u,在signed的情况下,31>32u,因为32u=10000=(转换为符号数)-32
因此在进行比较时,需要判断各自是否为符号数,并且转换为二进制位数,再在给定情况下进行比较
一些等式:
|Tmax|=|Tmin|-1
|Tmax|=2*|Umax|+1
符号扩展
如何把原有的一定位数的有符号的二进制数扩展为更多位数的二进制数(前提是不改变数字本身的大小和符号)?
复制符号位到最左侧,无论符号位是0还是1
例如:01100–>001100,11100–>111100.箭头两边在有符号的情况下相等。并且可以一直复制下去
对于无符号数,只需要最左侧不停添0即可
常用于位模式
加减乘除
如果两个N位的二进制数相加,理论结果是N+1位的话,实际结果将会无条件舍弃掉最高位,只留下N位作为运算结果。
对于无符号数,这N位正好等于理论结果模2N(或者可以理解成:实际结果 = 理论结果 - 2N)
对于符号数,这N位正好等于补码运算
例如,1101+0101=10010=(丢弃最高位)0010=2
以无符号二进制表示,相当于13+5=18,(18)mod16=2,而16=24
以符号二进制表示,相当于-3+5=2
注意:
第二种情况,
对于两个正数相加,如果理论结果大于Rmax,会导致实际结果编程负数,称为正溢出(例如5+7=0101+0111=1100=-4,(12)mod16=-4)
对于两个负数相加,如果理论结果小于Tmin,会导致实际结果变成正数,称为负溢出(例如-3+(-6)=1101+1010=10111=(丢弃最高位)0111=7,(-9)mod16=7)。
将上述结果用c语言表示:
int s,t,u,v;
s = (int) ((unsigned) u + (unsigned) v);
t = u+v;
/*s==t*/
1.十进制数乘2,结果相当于对应的二进制数左移一位(当然,前提是不溢出)。将乘数变成与2的幂接近的数字才有利于计算二进制乘法。
例如5 * 4=20,4=22(补码)对应:0101–>(左移2位)010100=(保留后4位)0100=4,20mod16=4
例如5 * 5=25,5=22+1(补码)对应:0101–>(左移2位)010100–>(加上5)011001=(保留后4位)1001=-7,25mod16=-7
2.对于N位二进制数与N位二进制数的乘法,得到的结果同样只保留后N位,同样等于理论结果mod2N,无论是否为符号数。几乎所有情况下,你也只需要后N位就能得知乘法结果是什么,而不用管丢掉的前N位。
乘法除法是最消耗计算机资源的运算,利用第一点将乘法/除法解释为左移位/右移位,就能很大程度降低消耗
将X变为-X:每一位都取反,再+1
如果没有准确的知道哪个变量是否为符号数,就不要用for倒计时,否则很容易出现下面的错误:
/*错误示范*/
for(unsigned i=n;i>=0;i--)
第一种避免无符号数可能带来错误的方法是完全不用无符号数,确保所有变量都是符号数
第二种方法是:
对于刚刚的for倒计时,可以这样改写:
/*正确示范*/
for(unsigned i=n-2;i<n;i--)
更为安全的改写:
/*更为正确的示范*/
/*size_t的大小等于该机器的字长,同时它也是unsigned类型*/
for(size_t i=n-2;i<n;i--)
而反过来,什么时候应该要用无符号数呢?
1.模运算(加密算法中常用到模运算)
2.用来表示集合
内存、指针、字符中的数字表示
内存
程序按地址引用数据
在计算机上运行程序时,从编程角度来看,内存就是个巨大的字节数组,从0到某个最大数字的编号。
因此,地址就像是内存,这个字节数组,的索引(index),一个指针变量存储着一个地址。然而,程序运行时,程序实际用到的只是内存中的某一部分区域,而不是整个内存那么多那么夸张。如果尝试不听话、访问其他区域,系统会报错,称为分段错误。
系统提供专用地址空间来保护每个进程。
进程相当于一个正在被执行的程序
一个程序可以破坏自己的数据,但不能破坏其他程序的数据
实际操作中程序会在内存的不同区域之间移动,但我们仍可以按这个思路去进行后续的思考
机器字(machine words):
字长(word size):无论最大数是什么,或者指针表示的范围在这种语言中有多大,或硬件上最大的数据块是多少,总有一个标准确定如何存储值和算术运算。例如64位计算机,意思是它通常都是处理64位值及其算术运算,并且地址的值是64位(即使实际可用地址只有47位)
如果用GCC来编译,可以指定字长是64还是32,并据此生成两种不同类型的代码。实际上是硬件和编译器一起决定在某个程序中使用的字长是多少
地址
无论如何,内存本身就是字节的数组。但是对于不同字长的机器,一个字块(word)所包含的字节(byte)数目却是不一样的。32位机器的一个字块包含连续的4个字节,64位则包含8个字节。编译器努力保持字节对齐,因为这样地址索引更快(一个字块的字节数就等于对应字长机器的指针字节数),运行效率更高。字块中数据的存储方式有两种:大端法和小端法。x86、ARM都是小端法。互联网都是大端法。
小端法:低位写在低地址
大端法:高位写在低地址,看起来会更加符合我们的日常直觉
例如15213–>0011 1011 0110 1101–>3 B 6 D
小端(32位):6D 3B 00 00
小端(64位):6D 3B 00 00 00 00 00 00
大端:00 00 3B 6D
/*将pointer强制转换为unsigned char *,使得可以将其作为字节数组来处理*/
typedef unsigned char *pointer;
/*将一系列的字节当作一个数组,并打印出该数组中的每个字节*/
void show_bytes(pointer start,size_t len){
size_t i;
for(i = 0;i < len;i++)
printf("%p \t 0x%.2x \n",start+i,start[i]);
printf("\n");
}
int a = 15213;
show_bytes((pointer) &a,sizeof(int));
字符串
C语言中,字符串char本质上还是数组,它将每个字符以ASCII码的形式转换为对应的一个字节存储起来。字符串的最后一个字节一定是00,也就是NULL终止符
小知识:
1.符号数x小于0,x*2不一定小于0,例如1000
2.无符号数恒大于等于0
3.无符号数一定不大于-1(-1被转换为unsigned,也就是0xffffffff)
4.如果x大于y,-x不一定小于-y(-x相当于x取反+1)。例如y是Tmin,x是其他数,那么Tmin取反+1还是Tmin
举例子时,要最先想到Tmin,0
浮点数
例如101.11表示4+1+1/2+1/4=5+3/4
浮点数同样满足【乘2/除2对应左移一位/右移1位】
局限性:
1.由于在这里,小数本质上是用1/2,1/4…来表示的,因此有时候只能逼近一个数而不能完全准确表示
2.浮点主要通过移动二进制小数点来表示尽可能大的取值范围及尽可能高的精度的平衡,因此取值范围有限
浮点数不好用,因为不同机器对同一浮点数的行为、四舍五入也不一样。因此有一个通用的IEEE浮点数的标准,来规范使用浮点数。以下都是关于该标准的学习。
表示方式:(-1)S* M *2E
存储形式:【S 】【exp】【frac】
S:符号位
M:尾数,介乎于1和2
E:幂次数。这里可以理解成M的小数点左移/右移的位数。E大于0就是左移,小于0就是右移
S,exp,M区域有点复杂
E = exp - 偏置(bias)
偏置值:bias=2k-1-1.其中k是exp区总共的位数。
exp:exp字段对应的无符号数值
例如:exp区有8位,那么对应的exp取值范围是1254**(**规范取值时,exp既不全0也不全1,因此取值只能是0000000111111110),bias=127,E的取值范围是-126 ~ 127
frac区域存储的是1.xxx…x的小数区域,而xxx…x的取值范围是000…0~111…1,对应着1≤M<2
实例:
1521310 = 111011011011012 = 1.11011011011012×213.
对应 (-1)S* M *2E的话,S=0,M=1.11011011011012,E=13
所以frac=11011011011010000…02
如果是单精度8位,那么exp=E+bias=13+127=140=100011002
上述都是规范化的表示浮点数的方法,下面要介绍非规范化的方法
非规范化E=1-bias
好处是能够表示0、正无穷和负无穷
想要弄明白为什么需要规范化和非规范化,
浮点数运算:
浮点数加法关键要舍入
规则是向最近的偶数(或者是整数)舍入
如果是对二进制浮点数进行舍入,应遵循:
如果最小的要舍入的位之后的位数超过一半就向上入,小于一半就向下舍,刚好一半的话就看要舍入的位,如果舍了能变为偶数(也就是那一位变为0),那就舍,否则就入
(例如10.000112–>10.002,10.001012–>10.012,10.101002–>10.102,10.011002–>10102)
浮点数加法遵循交换律,但不遵循结合律,因为会有舍入
浮点数乘法:
(-1)S1 M1 2E1 ×(-1)S2 M2 2E2 = (-1)S* M *2E
S=S1异或S2
M=M1×M2,如果M≥2,需要将M右移,增加E,使M的值在1到2之间
E=E1+E2
类型转换会对值有影响,例如double/float–>int,会向零舍入
小知识
int x=…;float f = …;double d = …;
1.x 不等于 (int)(float) x,转换过程中会丢失一些位
2.x 等于(int)(double) x
3.f 等于(float)(double) f
4.d不等于(double)(float)