【第19节 C语言语法进阶】
【19.1 条件运算符与逗号运算符】
1 条件运算符
条件运算符是C语言中唯一的一种三亩运算符。三目运算符代表有三个操作数;双目运算符代表有两个操作数,如逻辑运算符就是双目运算符;弹幕运算符代表有一个操作数,如逻辑非就是单目运算符。运算符也称操作符。三目运算符通过判断问号之前的表示的真假,来确定整体表达式的值,如下例所示,如果a>b为真,那么三目表达式整体的值为a,所以max的值等于a,如果a>b为假,那么三目表达式整体的值为b,所以max的值等于b。
#include <stdio.h>int main(){int a,b,max;while(scanf("%d%d",&a,&b)){max = a > b ? a : b;printf("max = %d\n",max);}return 0;
}
2 逗号运算符
逗号运算符的优先级最低,逗号表达式的整体值是最后一个表达式的值。
#include <stdio.h>
int main(){int i,j;i = 10;j = 1;if(i,--j){ // 不会进入if,逗号表达式整体的值是最后一个表达式的值, j是-1printf("condition value is:j = %d,if excute\n",j);}if(i,++j){ // 进入if,逗号表达式整体的值是最后一个表达式的值, j是0printf("condition value is:j = %d,if excute\n",j);}for (i = 0,j = 1; i < 10; i++) { // 逗号表达式的常见场景,for的表达式1初始化多个变量用的较多}return 0;
}
【19.1 题】
1、条件运算符是C语言唯一的三目运算符,优先级高于赋值运算符。
2、max=a>b?a:b;通过这种方式max可以获取a和b中较大的那个数。
3、逗号运算符的优先级低于赋值运算符,逗号表达式的整体值是最后一个表达式的值。
【19.2 自增自减运算符】
1 自增自减运算符
自增、自减运算符和其他运算符有很大区别,因为其他运算符除赋值运算可以改变变量本身的值外,不会有这种效果。自增、自减就是对变量自身进行加1、减1操作,那么有了加法和减法运算符为啥还要发明这种运算符呢?原因是自增自减来源于B语言,当时ken thompson和dennis M.ritchie(C语言的发明者)为了不改变程序员的编写习惯,在C语言中保留了B语言的自增和自减。因为自增、自减会改变变量的值,所以自增和自减不能用于常量。
下例中的j=i++>-1,对于后++或者后--,需要去掉++或--运算符,也就是首先计算j=j>-1,因为i本身等于-1,所以得到j的值为0,接着单独计算i++,也就是对i加1,所以i从-1加1得到0,因此printf("i=%d,j=%d",i,j);语句的执行结果是0和0。
#include <stdio.h>
int main(){int i,j;i = -1;
// 5++; // 编译不通,自增自减不能用于常量j = i++ > 1; // j = i > 1;i++;printf("i = %d,j = %d",i,j); // i = 0,j = 0return 0;
}
2 自增自减运算符与取值运算符
只有比后增优先级高的操作,才会走位一个整体,如()、[]
#include <stdio.h>
int main(){int a[3] = {2,7,8};int *p;int j;p = a;j = *p++; // 先把*p的值赋给j,然后对p加1,j = a[0],p = a[1]printf("a[0] = %d,j = %d,*p = %d\n",a[0],j,*p); // 2,2,7j = p[0]++; // 先把p[0]的值赋给j,然后对p[0]加1,j = a[1],p = a[2]printf("a[0] = %d,j = %d,*p = %d\n",a[0],j,*p); // 2,7,8return 0;
}
【19.2 题】
1、a=10,执行b=a++;后,a的值是11,b的值是10。
2、a=10,执行b=a--;后,a的值是9,b的值是10.
3、int a[3] = {2,7,8};int *p = a;执行了j = *p++;后,p指向元素a[1].整型指针变量加1,偏移4个字节,因此指向a[1]。
【19.3 位运算符】
1 位运算符
位运算符<<、>>、~、|、^、&依次是左移、右移、按位取反、按位或、按位异或、按位与。
位运算符只能用于对整型数据进行操作。
左移:高位丢弃,低位补0,相当于乘以2,一半申请内存时会用左移,例如要申请1GB大小的空间,可以使用malloc(1<<30)。
右移:低位丢弃,整数的高位补0(无符号数认为是正数),负数的高位补1,相当于除以2,移位比乘法和除法的效率要高,负数右移,对偶数来说是除以2,但对奇数来说是先减1后除以2。例如,-8>>1,得到的是-4,但-7>>1得到的并不是-3而是-4。另外,对于-1来说,无论右移多少位,值永远为-1.
C语言的左移和右移相当于是算数左移与算数右移。考研中的逻辑左移与右移,左移和右移空位都补0.
异或:相同的数进行异或时,结果为0,任何数和0异或的结果是其本身。
按位取反:数位上的数是1变为0,0变为1.
按位与和按位或:用两个数的每一位进行与和或。
// bit operator
#include <stdio.h>
int main(){short i = 5;short j;j = i << 1; // j = i*2 = 10,i = 5printf("j = %d\n",j); // j = 10j = i >> 1; // j = i/2 = 2,i = 5printf("j = %d\n",j); // j = 2i = 0x8011; // 1000 0000 0001 0001unsigned short s = 0x8011; // 1000 0000 0001 0001unsigned short r = 0;j = i >> 1; // i >> 1 = 11000 0000 0001 000 = 1100 0000 0000 1000 = A008r = s >> 1; // s >> 1 = 01000 0000 0001 000 = 0100 0000 0000 1000 = 4008printf("j = %d,r = %d\n",j,r);i = 5,j = 7; // 5 = 0000 0101,7 = 0000 0111printf("i & j = %d\n",i & j); // 0000 0101 = 5printf("i | j = %d\n",i | j); // 0000 0111 = 7printf("i ^ j = %d\n",i ^ j); // 0000 0010 = 2printf("~i = %d\n",~i); // 按位取反的结果是负数,该负数是原数的补码,需要将补码转换成原数,原数是按位取反加1,再加上负号。return 0;
}
2 异或运算符示例解析
异或运算符有两个特性,一是任何数和零异或得到的是自身,两个相等的数异或得到的是零,通过这两个特性,可以在一堆数中找出出现1次的那个数,其他数是出现2次。异或满足交换律。
#include <stdio.h>// XOR Exclusive OR
int main(){int arr[5] = {8,5,3,5,8};int result = 0; // 任何数和0异或都是自身for (int i = 0; i < 5; ++i) {result ^= arr[i]; // 两个相等的数异或得到的是零,异或满足交换律}printf("result = %d\n",result);return 0;
}
【19.3 题】
1、5&7得到的值是5. // 0000 0101 & 0000 0111 = 0000 0101 = 5
2、5<<1得到的值是10,-7>>1得到的值是-4. // 5左移1位=5*2=10,-7右移1位=(-7-1)/2=-4
3、5^5得到的值是0,7^0得到的值是7. // 数和本身异或结果都为0,相同的数异或结果为0;数和0异或结果都为数本身。
【19.4 switch do-while】
1 switch选择语句讲解
判断一个变量可以等于几个值或几十个值时,使用if和else if语句会导致else if分支非常多,这时可以考虑使用switch语句,switch语句的语法格式如下:
switch(表达式)
{case 常量表达式1:语句1case 常量表达式2:语句2...case 常量表达式n:语句ndefault:语句n+1
}
使用switch语句的例子。输入一个年份和月份,然后打印队医你给月份的天数,如输入一个闰年和2月,则输出为29天。具体代码如下所示,switch语句中case后面的常量表达式的值不是按照1到12的顺序排列的。switch语句匹配并不需要常量表达式的值有序排列,输入值等于哪个常量表达式的值,就执行其后的语句,每条语句后面需要加上break语句,代表匹配成功一个常量表达式时就不再匹配并跳出switch语句。
【例1】switch语句的使用。
#include <stdio.h>// switch
int main(){int year,month;while(scanf("%d%d",&year,&month)){switch (month) {case 1:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 2:printf("year = %d,month = %d,have %d days\n",year,month,28+(year%400==0 || year%100!=0 && year%4==0));break;case 3:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 4:printf("year = %d,month = %d,have 30 days\n",year,month);break;case 5:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 6:printf("year = %d,month = %d,have 30 days\n",year,month);break;case 7:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 8:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 9:printf("year = %d,month = %d,have 30 days\n",year,month);break;case 10:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 11:printf("year = %d,month = %d,have 30 days\n",year,month);break;case 12:printf("year = %d,month = %d,have 31 days\n",year,month);break;default:printf("invalid month\n");}}return 0;
}
如果一个case语句后面没有break语句,那么程序会继续匹配下面的case常量表达式。代码优化。原理是只要匹配到1、3、5、7、8、10、12中的任何一个,就不再拿month与case后的常量表达式的值进行比较,而执行语句printf,完毕后执行break语句跳出switch语句,switch语句最后加入default的目的是,在所有case后的常量表达式的值都未匹配时,打印输出错误标志或着一些提醒。
【例2】日期示例改进。
#include <stdio.h>// switch
int main(){int year,month;while(scanf("%d%d",&year,&month)){switch (month) {case 1:case 3:case 5:case 7:case 8:case 10:case 12:printf("year = %d,month = %d,have 31 days\n",year,month);break;case 4:case 6:case 9:case 11:printf("year = %d,month = %d,have 30 days\n",year,month);break;case 2:printf("year = %d,month = %d,have %d days\n",year,month,28+(year%400==0 || year%100!=0 && year%4==0));break;default:printf("invalid month\n");}}return 0;
}
2 do while循环讲解
do while语句的特点是:先执行循环体,后判断循环条件是否成立。其一般形式为
do{循环体语句;
}while(表达式);
执行过程如下:首先执行一次指定的循环语句,然后判断表达式,当表达式的值为非零(真)时,返回重新执行循环体语句,如此反复,直到表达式的值等于0为止。例3是使用do while语句计算1到100之间所有整数之和的例子,do while语句与while语句的差别是,do while语句第一次执行循环体内语句之前不会判断表达式的值,也就是如果i的初值为101,那么依然会进入循环体。实际情况中do while语句应用较少。
【例3】do while语句计算1到100之间的所有整数之和
#include <stdio.h>// do while
int main(){int i = 0,sum=0;do{sum += i;} while (++i <=100);printf("%d\n",sum);i = 0,sum=0;do{sum = sum + i;i++;} while (i <=100);printf("%d\n",sum);i = 101,sum=0;do{sum = sum + i;i++;} while (i <=100); // 循环体总是会先执行一次printf("%d\n",sum);return 0;
}
【19.4 题】
1、switch的case后只能放常量表达式。常量表达式的值是整型、字符,因为字符也可以是一个整型值。
2、case后如果没有break,那么不会匹配后面的case的表达式,直接执行表达式后的语句。
3、do while循环,如果while()括号内的表达式为假,第一次仍然会进入循环。
【19.5 二维数组、二级指针】
1 二维数组讲解
二维数组定义的一般形式如下:
类型说明符 数组名[常量表达式][常量表达式];
例如,定义a为3*4(3行4列)的数组,b为5*10(5行10列)的数组:
float a[3][4],b[5][10];
可以将二位数组视为一种特殊的一维数组:一个数组中的元素类型是一维数组的一维数组。
例如,可以把二维数组a[3][4]视为一个一维数组,他有3个元素a[0] a[1] a[2],每个元素又是一个包含4个元素的一维数组,如图1所示。
二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储在第一行的元素,后顺序存储第二行的元素,数组元素的获取依次是从a[0][0]到a[0][1],直到最后一个元素a[2][3]。
图2中显示了存储二维数组a[3][4]中每个元素时的顺序。
#include <stdio.h>// Two-dimensional array
int main(){int arr[3][4] = {1,2,3,4,5,6,7,8,9};printf("sizeof(arr) = %d\n",sizeof (arr)); // 3*4*sizeof(int) = 3*4*4printf("arr[2][3] = %d\n",arr[2][3]); // 二维数组的最后一个元素return 0;
}
2 二级指针讲解
二级指针是指针的指针,二级指针的作用是服务于一级指针变量,对一级指针变量实现间接访问。
#include <stdio.h>// Two-level pointer
int main(){int i = 10;int *p = &i; // p指向i的地址int **q = &p; // q指向p的地址 如果需要把一个一级指针变量的地址存起来,就需要二级指针类型printf("sizeof(i) = %d,i = %d,&i = %d\n",sizeof (i),i,&i);printf("sizeof(p) = %d,p = %d,*p = %d\n",sizeof (p),p,*p);printf("sizeof(q) = %d,q = %d,*q = %d,**q = %d\n",sizeof (q),q,*q,**q); // 通过2次取值可以拿到i的值return 0;
}
假设如下是内存:
【19.5 题】
1、二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储第一行的元素,后顺序存储第二行的元素,直到最后一行。
2、定义int a[3][4];那么可以访问数组的最后一个元素是a[2][3]。
3、二级指针是存储一级指针变量的地址值,也是用于做间接访问。
【19 代码题】
1、读取一个有符号数,对其进行左移,输出结果,对其进行右移,输出结果。例如,输入数值5,左移得到的结果是10,右移得到的结果是2.(不考虑左移后正值变为负值,负值变为正值的情况),每个输出占用2个字符位置,采用(%2d)。
#include <stdio.h>// Two-dimensional array
int main(){int a;scanf("%d",&a);int i = a << 1;int j = a >> 1;printf("%2d\n",i);printf("%2d\n",j);return 0;
}
2、输入5个数,其中2个数出现2次,1个数是出现1次,找出出现1次的那个数,例如输入的是8 5 3 5 8,输出的值为3.
#include <stdio.h>// XOR Exclusive OR
int main(){int num,result = 0;for (int i = 0; i < 5; ++i) {scanf("%d",&num);result = result ^ num;}printf("%d\n",result);return 0;
}
【第20节 数据的机器级表示】
【20.2 与408关联】
2012年
13.假定编译器规定int和short型长度分别为32位和16位,执行下列C语言语句:
unsigned short x = 65530;
unsigned int y = x;
得到y的机器数为:?。
14.float 类型(即IEEE754单精度浮点数格式)能表示的最大整数是?。
2013年
14.某字长为8位的计算机中,已知整型变量x、y的机器数分别为[x]补=1 1110100,[y]补=10110000.若整型变量z=2*x+y/2,则z的机器数为:?。
2016年
13.有如下C语言程序段
short si = -32767;
unsigned short usi = si;
执行上述两条语句后,usi的值为:?。
14.某计算机字长为32位,按字节编址,采用小端(Little Endian)方式存放数据。假定有一个double型变量,其机器表示位1122 3344 5566 7788H,存放在0000 8040H开始的连续存储单元中,则存储单元0000 8046中存放的是:?。
2018年
13.假定带符号整数采用补码表示,若int型变量x和y的机器数分别是FFFF FFDFH和0000 0041H,则x,y的值以及x-y的机器数分别是:?。
14.IEEE754单精度浮点格式表示的数中,最小的规格化正数是:?。
15.某32位计算机按字节编址,采用小端(Little Endian)方式,若语令"int i = 0;"对应指令的机器代码为"C7 45 FC 00 00 00 00",则语句"int i = -64;"对应指令的机器代码是:?。
16.整数x的机器数为1101 1000,分别对x进行逻辑右移1位和算数右移1位操作,得到的机器数各是:?。
2019年
13.考虑一下C语言代码:
unsigned short usi = 65535;
short si = usi;
执行上述程序段后,si的值是:?。
2 本节内容介绍
本大节课分为20.3小节到20.7小节,包含补码解析,整型不同类型、溢出解析,浮点数IEEE754标准解析,真题实战
20.3小节是补码讲解及内存实战演示
20.4小节是整型不同类型解析-溢出解析
20.5小节是浮点数IEEE754标准解析及实战计算演示
20.6小节是浮点数精度丢失实战演示
20.7小节是选择题真题讲解
【20.3 补码讲解及内存实战演示】
计算机的CPU无法做减法操作(硬件上没有减法器),只能做加法操作。CPU中有一个逻辑单元叫加法器。计算机所做的减法,都是通过加法器将其变化为加法实现的。实现2-5的方法是2+(-5)。由于计算机只能存储0和1,因此编写程序来查看计算机如何存储-5,5的二进制数为101,称为原码。计算机用补码表示-5,补码是对原码取反后加1的结果,即计算机表示-5时会对5的二进制数(101)取反后加1,如图1所示。-5在内存中存储为0xffff fffb,因为5取反后得0xffff fffa,加1后得0xffff fffb(由于是X86架构是小端存储,小端存储是低字节在前,即低字节在低地址,高字节在后,即高字节在高地址,fb对于0xffff fffb是最低的字节,因此fb在最前面,大端和小端相反),对其加2后得0xffff fffd,见图2,他就是k的值。当最高位为1(代表负数)时,要得到原码才能知道0xffff fffd的值,即对其取反后加1(也可以减1后取反,结果是一样的)得到3,所以其值为-3.
求-5的补码,原码5=0000 0000 0000 0000 0000 0000 0000 0101,
取反1111 1111 1111 1111 1111 1111 1111 1010 = ffff fffa
加1:1111 1111 1111 1111 1111 1111 1111 1011 = =ffff fffb
-5的补码是:ffff fffb。(电脑是X86架构,X86架构是小端存储。电脑内存显示方式为小端存储,即低字节在前,高字节在后。-5的补码内存视图显示为:fb ff ff ff)
对负数加2,即对负数的补码加2,得0xffff fffd。(原码为减1后取反,或取反后加1)
-5的补码加2:1111 1111 1111 1111 1111 1111 1111 1101 = 0xffff fffd(内存视图显示为:fd ff ff ff)
-5的补码加2后取反:1000 0000 0000 0000 0000 0000 0000 0010
-5的补码加2后取反加1:1000 0000 0000 0000 0000 0000 0000 0011(-5+2的原码)
#include <stdio.h>// complement
int main(){int i = -5;int j = -5 + 2;printf("%d\n",j);return 0;
}
考研注意:假设变量A的值为-5,通过8位表示,那么A[补]值为1111 1011,A[原]的值为1000 0101,符号位是不动的,只有值的部分是5,通过符号位不动,其他值取反加1。正数的补码和原码一致。
反码是一种在计算机中数的机器码表示。对于单个数值(二进制的0和1)而言,对其进行取反操作就是将0变为1,1变为0.正数的反码和原码一致,负数的反码就是在原码的基础上符号保持不变,其他位取反。
6是正数,补码与原码一致,-3的补码是:1111 1101.
-3的原码:0000 0011,取反:1111 1100,加1:1111 1101.(补码)。原码:先减1:1111 1100,后取反:1000 0011,负数的原码为:-3.考研的原码记住负数的符号位要保留。
【20.3 题】
1、负数是以补码形式在内存中进行存储的。
2、小端是低位在低地址,高位在高地址(小端:低位在前,高位在后),而大端刚好相反,低位在高地址,高位在低地址(大端:低位在后,高位在前)。
3、对于int a = -6;小端存储,那么a在内存中从低地址到高地址的存储的效果是fa ff ff ff.
原码:0000 0110.取反:1111 1001.加1:1111 1010. fa
4、假设有2个8位的补码数A和B,A[补]表示为1101 1010,B[补]表示为0011 0101,那么A[原]为1010 0110,B[原]为0011 0101
A:1101 1010 -> 1101 1001 -> 0010 0110 ->考研初试负数的符号位不变->1010 0110
B:0011 0101 -> 0011 0101
【20.4 整型不同类型解析-溢出解析】
1 整型不同类型解析
整型变量包括6种类型。其中有符号短整型与无符号短整型的最高位所代表的意义不同。不同整型变量表示的整型数的范围如表,超出范围会发生溢出现象,导致计算出错。
有符号基本整型 (signed)int
有符号短整型 (signed)short
有符号长整型 (signed)long
无符号基本整型 (unsigned)int
无符号短整型 (unsigned)short
无符号短整型 (unsigned)short
有符号短整型short(2个字节,16位)的最大值为:2^15-1 = 32767
无符号短整型short(2个字节,16位)的最大值位:2^16-1 = 65535
短整型最小数是:-32768(-2^15).补码=1000 0000,原码=1000 0000
考研补充说明:
考研会考8位的,也就是1个字节的整型数的大小,对于1个字节的有符号类型的数值范围是-2^7~(2^7-1),也就是-128到127,对于8位的无符号(unsigned)类型的数值范围是0~(2^8-1),也就是0-255.
2 溢出解析
如下例,有符号短整型数可以表示的最大值位32767,当对其加1时,b的值会变为多少?实际运行打印得到的是-32768。因为32767对应的十六进制数为0x7fff,加1后变为0x8000,其首位为1,因此变成了一个负数。取这个负数的原码后,就是其本身,值为32768,所以0x8000是最小的负数,即-32768。这时就发生了溢出,对32767加1,希望得到的值是32768,但结果却是-32768,因此导致计算结果错误。在使用整型变量时,一定要注意数值的大小,数值不能超过对应整型数的表示范围。在编写的程序中数值大于2^64-1时,可以自行实现大整数加法。
2^15 = 32768
2^16 = 65536
2^31 = 2147483648
2^32 = 4294967296
#include <stdio.h>// overflow
int main(){int i;short a = 32767;short b;long c; // 32位的程序是4个字节,64位的是8个字节b = a + 1; // 发生了溢出,解决溢出的办法是用更大的空间来存i = a + 1; // 用更大的空间来存printf("short b = a(32767) + 1 = %d\n",b); // short b = -32768printf("int i = a(32767) + 1 = %d\n",i); // int i = 32768i = 2147483647;c = i + 1;printf("int i = %d\n",i);printf("long c = i(2147483647) + 1 = %d\n",c);printf("sizeof(long) = %d\n",sizeof (long));// 无符号数unsigned int m = 3;unsigned short n = 0x8056; // 无符号类型,最高位不认为是符号位 原码=补码:1000 0000 0101 0110unsigned long k = 5;b = 0x8056; // 有符号短整型 1000 0000 0101 0110 (负数的补码) 原码:0111 1111 1010 1010 考研(保留符号位) :1111 1111 1010 1010printf("b = %d\n",b); // b是有符号类型,所以输出是负值printf("n = %u\n",n); // 无符号类型要用%u,用%d是不规范的return 0;
}
【20.4 题】
1、一个C语言程序在一台32位机器上运行,程序中定义了三个变量x,y,z,其中x和z为int型,y为short型。当x=127,y=-9时,执行赋值语句z=x+y后,x、y和z的值分别时x=0000 007FH,y=FFF7H,z=0000 0076H.
int为32位,short位16位;又C语言的数据在内存中为补码形式,故x、y的机器数写为0000 007FH,FFF7H;执行z=x+y时,由于x是int型,y是short型,需将y的类型强制转换为int,在机器中通过符号位扩展实现,由于y的符号位为1,故在y的前面添加16个1,即可将y强制转换为int型,其十六进制形式为FFFF FFF7H;然后执行加法,即0000 007FH + FFFF FFF7H = 0000 0076H,其中最高位的进位1自然丢弃,因此答案是x=0000 007FH,y=FFF7H,z=00000 0076H.
y(原9)= 0000 1001
y(补-9)= 1111 0111
y = 1111 1111 1111 1111 1111 1111 1111 0111
x = 0000 0000 0000 0000 0000 0000 0111 1111
x+y=0000 0000 0000 0000 0000 0000 0111 0110 = ...76H
2、假定有4个整数用8位补码分别表示r1=FEH,r2=F2H,r3=90H,r4=F8H,若将运算结果存放在一个8位寄存器中,则下列4个运算均不会发生溢出:r1*r2(28). r2*r3(1568>127,溢出). r1*r4(16). r2*r4(112).
用补码表示时8位寄存器所能表示的整数范围为-128~-127,由于r1=-2,r2=-14,r3=-112,r4=-8,因此r2*r3=1568,大于127,因此结果溢出。
3、由3个“1”和5个“0”组成的8位二进制补码,能表示的最小整数是-125.补码整数表示时,负数的符号位为1,数值位按位取反,末位加1,因此剩下的2个“1”在最低位时,表示的是最小整数,位1000 0011,转换成真值为-125. (取反)0111 1100-》(加1)0111 1101 -》(考研保留符号位)1 111 1101 = - 2^7-1-2 = -125
【20.5 浮点数IEEE754标准解析及实战计算演示】
在C语言中,要使用float关键字或double关键字定义浮点型变量。float型变量占用的内存空间为4字节,double型变量占用的内存空间为8字节。与整型数据的存储方式不同,浮点型数据是按照指数形式存储的。系统把一个浮点型数据分成小数部分(用M表示)和指数部分(用E表示)并分别存放,指数部分采用规范化的指数形式,指数也分正、负(符号位,用S表示),如图1所示。
数符(即符号位)占1位,是0时代表整数,是1时代表负数,表1是IEEE-754浮点型变量存储标准。
S:是符号位,用来表示正、负,是1时代表负数,是0时代表正数。
E:E代表指数部分(指数部分的值规定只能是1到254,不能是全0,全1),指数部分运算前都要减去127(这是IEEE-754的规定),因为还要表示负指数,这里的1000 0001转换为十进制数为129,129-127=2,即实际指数部分为2.
M:M代表小数部分,这里0010 0000 0000 0000 000。底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.0010 0000 0000 0000 000。
上表1可以变为如下表格:
#include <stdio.h>int main(){float f = 4.5;float f1 = 1.456;printf("%f,%f1\n",f,f1);return 0;
}// 4090 0000 = 0100 0000 1001 0000 0000 0000 0000 0000 = 0 100 0000 1 001 0000 0000 0000 0000 0000 =
// 100 0000 1 = 2^7 + 1 = 129
// 129-127 = 2-》指数 2*2=4
// 001 0000 0000 0000 0000 0000
// 底数左边省略了1: 1.001 0000 0000 0000 0000 0000
// 指数2-》右移2位,乘以2*2 = 100.1 0000 0000 0000 0000 0000
// 转换成十进制:4.5 = 2^2+1*2^(-1) = 4.5// 3f ba 5e 35 = 0011 1111 1011 1010 0101 1110 0011 0101 = 0 011 1111 1 011 1010 0101 1110 0011 0101
// 011 1111 1 = 2^7-1 = 128 - 1 = 127
// 127 - 127 = 0->指数 2*0=1
// 底数左边省略了1: 1.011 1010 0101 1110 0011 0101
// 转换成十进制:2^0 + 2^-2 + 2^-3 + 2 ^-4 + 2^-6 = 1+0.25+0.166= 1.41
上图中可以看到4.5,也就是变量f的内存是00 00 90 40,因为是小端钝出,所以实际值是0x40900000.
先看f的小数部分,如下表所示,M(灰色)代表小数部分,为0010 0000 0000 0000 0000 000,总计23位,底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.001000000.
指数部分,计算机并不能直接计算10的幂次,f的指数部分是表2的EEEEEEEE所对应的部分,也就是1000 0001,其十进制值为129,129-127=2,即实际指数部分为2指数值为2,代表2的2次幂,因此将1.001向左移动2位即可,也就是100.1,然后转换为十进制数,整数部分是4,小数部分是2^-1,刚好等于0.5,因此十进制数位4.5.浮点数的小数部分是通过2^-1+2^-2+2^-3+...来近似一个浮点数的。
小数部分乘以指数部分,等价于左移2位,对于小数部分1.001,其十进制值为1.125,那么1.125*指数部分,也就是4,就是1.125*4=4.5。
1.456,也是就会死变量f1的内存是35 5e ba 3f,因为是小端存储,所以实际值是0x3fba5e35。
先看f1的小数部分,M(灰色)代表小数部分,这里为011 1010 0101,总计23位,底数左边省略了一个1(这是IEEE-754的规定),使用的实际底数表示为1.011 1010 0101 1110 0011 0101.
接着看指数部分,计算机并不能直接计算10的幂次,f1的指数部分是表3的EEEE EEEE所对应的部分,也就是0111 1111,其十进制值为127,127-127=0,即实际指数部分为0,指数值为0,代表2的0次幂,因此1.011 1010 0101 1110 0011 0101无需做移动。浮点数的戏哦啊数部分是通过2^0+2^-2+2^-3+2^-4+2^-6...来近似一个浮点数的。1+0.25+0.125+0.0625+0.015625=1.453125.
【20.6 浮点数精度丢失实战演示】
浮点型变量分为单精度(float)型、双精度(double)型。如表1所示,因为浮点数使用的是指数表示法,需要记忆数值的范围。
double有11位指数,52位小数。
上表中double类型是-1022到1023,是通过1-2046(因为不能是全0,全1,全1是2047)减去1023,得到-1022到1023.
如下例所示,赋给a的值为1.23456789e10,加20后,应该得到的值为1.234567892e10,但b输出结果却是b=12345678.000000,变得更小了。将这种现象称为精度丢失,因为float型数据能够表示的有效数组为7位,最多只保证1.234567e10的正确性,要使结果正确,就要把a和b改为double型,因为double可以表示的精度位15-16位。
【例】验证精度丢失现象的程序。
// float double
#include <stdio.h>int main(){float a = 1.23456789e10; // 赋值的一瞬间发生精度丢失,因为浮点常量默认是8个字节存储,double型。float的有效数字(精度)是6~7位float b;b = a + 20; // 计算时,精度丢失printf("a=%f\n",a); // 既可以输出float,也可以输出double类型printf("b=%f\n",b);double c = 1.23456789e10;double d;d = c + 20;printf("c=%f\n",c);printf("d=%lf\n",d);return 0;
}
double输出类型:lf
scanf读取double类型时,要用lf,如double d;scanf("%lf",&d);
另外针对强制类型转换,int转float可能造成精度丢失,因为int是有10位有效数字的,但是int强制转为double不会,float转为double也不会丢失精度。
【20.6 题】
1、(真题2010年14题)假定变量i、f和d的数据类型分别为int,float,double(int用补码表示,float和double分别用IEEE754单精度和双精度浮点数格式表示),已知i=785,f=1.5678e3,d=1.5e100.若在32位机器中执行下列关系表达式,下面四个表达式结果均为真。i=(int)(float)i(真). f=(float)(int)f(假,转换为int类型时精度丢失). f=(float)(double)f(真). (d+f)-d=f(假)
解释:题中是那种数据类型转换的顺序为int-float-double,若将float转换为int,小数位部分会被舍去,int是精确到32位的整数,而float只保存到1+32位,因此一个32位的int整数在转换为float时会有损失。但i是785,只有3位,低于浮点精度表示的6-7位。
将float型的f转换为int型,小数点后的数位丢失,1567.8(1.5678e3就是1567.8)会变为1567,与原有的1567.8不相等,结果为非真。
double的精度和范围都比float大,float转换为double不会有损失。
浮点运算d+f时需要对阶,对阶后f的位数有效位被舍去而变为0(对阶就是从左边开始,浮点数d有10的100次幂了,而浮点数f在数的地位,double有效位数只有15-16位,自然对阶时,低位直接忽略掉了,10的100次幂到85次幂之间的有效位数才是double要保存的),因此d+f仍然位d,再减去d后结果为0(因为double只能15位有效数字),结果非真。根据不同类型数据混合运算“类型提升”原则,(d+f)-d=f等号左边的类型位double型,结果非真。
注:从int转换为float时,虽然不会发生溢出,但由于位数位数的关系,可能有效数据舍入,影响精度,而转换为double则能保留精度。
对阶:对阶的原则是小阶向大阶方向化,误差比较小。之所以这样做是因为若大阶对小阶,则位数的数值部分的高位需移出,而小阶对大阶移出的是位数的数值部分的低位,这样损失的精度更小。
2、(真题2013年13题)某数采用IEEE754单精度浮点数格式表示为C640 0000H,则该数的值是-1.5*2^12(错误)。值为-1.5*2^13
解析:C640 0000H =
1 100 0110 0 100 0000
符号位1:负
指数部分:100 0110 0 = 2^7 + 2^3+2^2 - 127 = 1+8+4 = 13 = 1000 1100 - 0111 1111 =
1000 1100 + 1000 0001 = 1000 1101 = -(8+4+1) = 13
小数部分:100 0000 = 1.1 = 1.5
位数有隐含位,要加1.
3、(2014年14题)float型数据常用IEEE754单精度浮点格式表示,假设两个float型变量x和y分别存放在32位寄存器f1和f2中,若(f1)=CC90 0000H,(f2)=B0C0 0000H,则x和y之间的关系为x<y且符号相同。
解析:
f1 = 1100 1100 1001 0000 。。。。
f2 = 1011 0000 1100 0000 。。。。
f1的指数部分:1001 1001 = 2^7+2^4+2^3+1 - 127 = 2 + 16 + 8 = 26
f2的指数部分:0110 0001 = 2^6 + 2^5 + 1 = 64+32+1 = 97 - 127 = -30
f1的小数部分:1.001 加上符号位: -1.001 * 2^26
f2的小数部分:1.1 加上符号位: - 1.1*2^-30
1.001 * 2^26 > 1.1*2^-30. => -1.001 * 2^26 < -1.1*2^-30.
f1和f2对应的二进制分别是:。。根据IEEE754浮点数标准,可知f1的数符位1,阶码位1001 1001,位数为1.001,f2的数符位1,阶码位0110 0001,位数为1.1,则可知两数均为负数,符号相同。f1的绝对值为1.001*2^26,f2的绝对值为1.1*2^-30,则f1的绝对值比f2的绝对值大,而符号为负,真值大小相反,即f1的真值比f2的真值小,即x<y。
【20.7 选择题真题讲解】
2012年
13.假定编译器规定int和short型长度分别为32位和16位,执行下列C语言语句:unsigned short x = 65530;unsigned int y = x;得到y的机器数为。
x=ff fa y=0000 fffaH
解析:将一个16位unsigned short转换成32位形式的unsigned int,因为都是无符号数,新表示形式的高位用0填充。16位无符号整数所能表示的最大值为65535,其十六进制表示为FFFFH,故x的十六进制表示为FFFFH-5H = FFFAH,所以y的十六进制表示为0000 FFFAH。
14.float类型(即IEEE754单精度浮点数格式)能表示的最大正整数是。
解析:IEEE754单精度浮点数是尾数采取隐藏位策略的原码表示,且阶码用移码(偏置值为127)表示的浮点数。规格化的短浮点数的真值为:(1)^S*1.m*2^E-127,S为符号位,阶码E的取值为1-254(8位表示),尾数m为23位,共32位;故float类型能表示的最大整数是1.111...1*2^254-127=2^127*(2-2^-23) = 2^128 - 2^104.
1.111..1 * 2^(254-127)=1.111...1*2^127=(24个1)*2^(127-23)=(2^24-1)*2^104=2^128-2^104.
【另解】IEEE754单精度浮点数的格式如下图所示
当表示最大正整数时:数符取0;阶码取最大值为127;尾数部分隐含了整数部分的“1”,23位尾数全取1时尾数最大,为2-2^-23,此时浮点数的大小为(2-2^-23)*2^127=2^128-2^104
2013年
14.某字长为8位的计算机中,已知整型变量x,y的机器数分别为[x]补=1111 0100,[y]补=1011 0000.若整型变量z=2*x+y/2,则z的机器数为
解析:乘以左移,除以右移
2*x = 1110 1000
y/2 = 1101 1000
z= 1100 0000 = C0H
x原码:1000 1100 = -12*2 = -24
y原码:1101 0000 = -(2^6 + 2^4) = -(64+16) = -80/2 = -40
2*x,将x算术左移一位为1110 1000;y/2,将y算术右移一位为1101 1000,均无溢出或丢失精度。补码相加为1110 1000 + 1101 1000 = 1 100 0000,亦无溢出。也可以看x的值为-12y的值为-80,计算后是-64,对应的编码就是1100 0000.
2016年
13.有如下C语言程序段short si = -32767; unsigned short usi = si;执行上述两条语句后,usi的值为
解析:
si(补码,机器表示) = 1000 0000 0000 0001
usi = 1000 0001 = 2^15 + 1 = 32768 + 1 = 32769
short为16位,因C语言中的数据再内存中位补码表示形式,si对应的补码二进制表示为: 1000 0000 0000 0001B,最前面的一位“1”为符号位,表示负数,即-32767。由signed型转化为登场unsigned型数据时,符号位称为数据的一部分,负数转化为无符号数(即正数),其数值将发生变化。usi对应的补码二进制表示与si的表示相同,但表示正数,为32769.
14.某计算机字长为12位,按字节编址,采用小端(Little Endian)方式存放数据。假定有一个double型变量,某机器数表示为1122 3344 5566 7788.存放在0000 8040H开始的连续存储单元中,则存储单元0000 8046H中存放的是
解析:小端方式存储数据:低位在前,高位在后,低位在低地址,高位在高地址。
机器数:88 77 66 55 44 33 22 11
0000 8040H:88
0000 8041H:77
0000 8042H:66
0000 8043H:55
0000 8044H:44
0000 8045H:33
0000 8046H:22 即为答案
大端方式:一个字中的高位字节(Byte)存放在内存中这个字区域的低地址处。小端方式:一个字中的低位字节(Byte)存放在内存中这个字区域的低地址处。依次分析,各自节的存储分配如下表所示:
从而存储单元0000 8046H存放的是22H。
2018年
13.假定带符号整数采用补码表示,若int型变量x和y的机器数分别是FFFF FFDFH和0000 0041H,则x、y的值以及x-y的机器数分别是:
解析:
x补 = FFFF FFDFH = x的原码1000 0021 = -(2*16+1) = -33
y = 0000 0041H = 4*16+1 = 65 = 0100 0001
x-y = -98 =
(-y)补 = FFFF FFBFH -> 1011 1111
x的补码=1101 1111
y的补码=1011 1111
x-y的补码(机器数)=1001 1110 = 9E
利用补码转换成原码的规则:负数符号位不变数值位取反加1;整数补码等于原码。两个机器数对应的原码是[x]原 = .... 0021H,对应的数值是-33,[y]原=[y]补=.... 0041H = 65。x-y直接利用补码减法准则,[x]补-[y]补=[x]补+[-y]补,-y的补码连同符号位取反加1,最终减法变成加法,得出结果FFFF FF9EH.
14.IEEE754单精度浮点格式表示的数中,最小的规格化整数是。
解析:IEEE754单精度浮点数的符号位、阶码位、尾数位(省去正数位1)所占的位数分别是1、8、23位。最小正数,数符位取0,移码的取值范围是1~254,取1,的阶码值1-127=-126(127为规定的偏置值),尾数取全0,最终推出最小规格化正数为1.0*2^-126。
1.000...0(23个0,小数部分全0)
1.0 * 2^(1-127) (指数部分最小,指数部分的范围是:1-254)
1.0*2^-126
15.某32位计算机按字节编址,采用小端(Little Endian)方式。若语令“int i = 0;"对应指令的机器代码为“C7 45 FC 00 00 00 00”,则语句“int i = -64;"对应指令的机器代码是C7 45 FC C0 FF FF FF.
解析:
-64补 = 64的原码:0100 0000=取反:1100 0000 = FF FF FF C0
需要转换成小端存储方式:C0 FF FF FF.
按字节编址,采用小端方式,低位的数据存储在低地址位、高位的数据存储在高地址位,并且按照一个字节相对不变的顺序存储。存储0的位数是后32位,那么只需要把-64的补码按字节存储在其中即可,-64表示成32位的十进制数是FF FF FF C0,根据小端方式的特点,高字节存储在低地址,就是C0 FF FF FF.
16.整数x的机器数为1101 1000,分别对x进行逻辑右移和算术右移1位操作,得到的机器数各是
解析:逻辑右移:0110 1100;算术右移1110 1100
逻辑移位:左移和右移空位都补0,并且所有数组参与移动。算术一位:符号位不参与移动,右移空位补符号位,左移空位补0.
2019年
13.考虑以下C语言代码:unsigned short usi = 65535;short si = usi;执行上述程序段后,si的值是-1。
解析:usi (无符号短整型)= 1111 1111 1111 1111
si(短整型) = 补码 = 取反(符号位保持不变,其余位取反):1000 0000 0000 0000 = 加1(符号位不变,加1):1000 0000 0000 0001 = -1
unsigned short类型位无符号短整型,长度为2字节,因此unsigned short usi转换为二进制代码即1111 1111 1111 1111.short类型位短整型,长度位2字节,在采用补码的机器上,short si的二进制代码为1111 1111 1111 1111,因此si的值为-1。
【20 代码题】
浮点数4.5的指数部分的值为2,(129-127得到),小数部分是001 0000 0000 0000 0000 0000 0000 0000,里边1的个数只有1个,那么输出1,总计的输出就是2 1,每个数占用3个字符位置%3d。现在输入的浮点数是1.456,那么需要通过单步调试,看看内存,算一算,1.456的指数部分是多少,小数部分1的个数数一数,然后输出
【第21节 汇编语言零基础入门】
【21.2 与408关联】
1 与408关联
2017年44题
44.(10分)在按字节编址的计算机M上,题43中f1的部分源程序(阴影部分)与对应的机器级代码(包括指令的虚拟地址)如下:
1)计算机M是RISC还是CISC?为什么?
2)f1的机器指令代码共占多少字节?要求给出计算过程。
3)第20条指令cmp通过i减n-1实现对i和n-1的比较。执行f1(0)过程中当i=0时,cmp指令执行后,进/借位标志CF的内容是什么?要求给出计算过程。
4)第23条指令sh1通过左移操作实现了power*2运算,在f2中能否也用sh1指令实现power*2?为什么?
2019年45题
45.(16分)已知f(n)=n!=n*(n-1)*(n-2)*...*2*1,计算f(n)的C语言函数f1的源程序(阴影部分)即其在32位计算机M上的部分机器级代码如下:
其中,机器级代码包括行号、虚拟地址、机器指令和汇编指令,计算机M按字节编址,int型数据占32位。请回答下列问题:
1)计算f(10)需要调用函数f1多少次?执行哪条指令会递归调用f1?
2)上述代码中,哪条指令是条件转移指令》哪几条指令一定会使程序跳转执行?
3)根据第16行call指令,第17行指令的虚拟地址应是多少?已知第16行call指令采用相对寻址方式该指令中的偏移量应是多少(给出计算过程)?已知第16行call指令的后4字节偏移量,M采用大端还是小端方式?
4)f(13)=6227020800,但f1(13)的返回值为1932053504,为什么两者不相等?要使f1(13)能返回正确的结果,应如何修改f1源程序?
5)第19行imul eax,ecx表示有符号数乘法,乘数为R[eax]和R[ecx],当乘法器输出的高、低32位乘积之间满足什么条件是,溢出标志OF=1?要是CPU在发生溢出时转异常处理,编译器应在imul指令后加一条什么指令?
2 本节内容介绍
本大节课分为21.3小节到21.7小洁,包含汇编指令格式讲解-C语言转汇编方法讲解,汇编常用指令讲解,各种变量赋值、选择循环和函数调用汇编实战解析
21.3小节是汇编指令格式讲解-C语言转汇编方法讲解
21.4小节是汇编常用指令讲解
21.5小节是各种变量赋值汇编实战解析
21.6小节选择循环汇编实战解析
21.7小节是函数调用汇编实战解析
【21.3 汇编指令格式】
1 汇编指令格式
在去看汇编指令前,先看下CPU是如何执行程序的,如下图所示,编译后的可执行程序,也就是main.exe,是放在代码段的,PC指针寄存器存储了一个指针,始终指向要执行的指令,读取了代码段的某一条指令后,会交给译码器来解析,这时译码器就知道要做什么事情了,CPU中的计算单元加法器不能直接对栈上的某个变量a,直接做加1操作的,需要首先将内存上的数据,加载到寄存器中,然后再用加法器做加1操作,再从寄存器搬到内存上去。
寄存器-》通过一个指针:读指令-〉译码器解析-》从内存上加载数据到:寄存器-〉加法器计算操作
操作码字段:表征指令的操作特性与功能(指令的唯一标识)不同的指令操作码不能相同。
地址码字段:指定参与操作的操作数的地址码。
指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储器地址,也可包含寄存器编号。
指令中可以有一个、两个或者三个操作数,也可以没有操作数,根据一条指令有几个操作数地址,可将指令分为零地址指令,一地址指令、而地址指令、三地址指令...
零地址指令:只有操作码,没有地址码(空操作 停止等)
一地址指令:指令编码中只有一个地址码,指出了参加操作的一个操作数的存储位置,如果还有另一个操作数则隐含在累加器中。
eg:INC AL 自加指令
二地址指令:指令编码中有两个地址,分别指出了参加操作的两个操作数的存储位置,结果存储在其中一个地址中。
eg:MOV AL,BL
ADD AL,30
三地址指令:指令编码中有3个地址码,指出了参加操作的两个数的存储位置和一个结果的地址。
(op a1,a2,a3:a1和a2的结果放入a3)
二地址指令格式中,从操作数的物理位置来说又可归为三种类型。
寄存器-寄存器(RR)型指令:需要多个通用寄存器或个别专用寄存器,从寄存器中取操作数,把操作结果放入另一个寄存器,机器执行寄存器-寄存器型的指令非常快,不需要访存。
寄存器-寄存器(RS)型指令:执行此类指令时,既要访问内存单元,又要访问寄存器。
存储器-存储器(SS)型指令:操作时都是涉及内存单元,参与操作的数都是放在内存里,从内存某单元中取操作数,操作结果存放至内存另一单元中,因此机器执行指令需要多次访问内存。
寄存器英文:register
存储器英文:storage
复杂指令集:变长 x86 CISC Complex Instruction Set Computer
简单指令集:等长 arm RISC Reduced Instruction Set Computer
2 生成汇编方法
编译过程
第一步:main.c->编译器->main.s文件(.s文件就是汇编文件,文件内是汇编代码)
第二部:main.s汇编文件->汇编器->main.obj
第三部:main.obj文件->链接器->可执行文件exe
teminal:
gcc -S -fverbose-asm main.cpp
3 汇编常用指令讲解
3.1 相关寄存器
————————————
仅用于本人学习
来源:网络