《【教学课件】第9章编程技巧.ppt》由会员分享,可在线阅读,更多相关《【教学课件】第9章编程技巧.ppt(26页珍藏版)》请在三一办公上搜索。
1、第9章 编程技巧,第一节 表达式解释计算第二节 C与操作系统接口设计第三节 C与汇编语言的接口第四节 程序调试问题,1/26,C语言程序设计 龙昭华主编,第一节 表达式解释计算,(第九章 编程技巧),2/26,一、表达式句法定义,需要定义一个含有+、-、*、/的运算表达式。加、减、乘、除四则运算符及括号的意义完全与平常的算术运算习惯一致。程序接受一行字符串,并对它进行表达式的解释计算,这就相当于一个简单的编译系统。,二、各句法对应函数,第一节 表达式解释计算,3/26,(第九章 编程技巧),为了便于识别表达式的各成份,并计算它们的值,需要定义相应的函数:num()函数:将数的字符序列转换成数值
2、。如:9、8.12、-10.30、1230等。factor()函数:计算因子的值。如:-91.89、(90*10.8+5.8)、60等。term()函数:计算项的值。如:89、(9/2+10)、90*7/10、(10-2)/2等。exor()函数:计算表达式的值。如:9+(10/2-9)、200-90、30+0.99等。由于表达式句法的递归定义,这些函数也将通过相互递归调用求得各自对应成份的值。假设主函数控制流程是反复读入一行正文,调用表达式计算函数expr(),求出表达式的值并输出,直至输入空行结束。程序中引入下列全局变量:buf字符数组,存储一行正文信息。cpt为当前正待识别的字符指针。r
3、esult为存储表达式计算结果的变量。主函数的算法为:读入一行存buf数组。如果输入行非空转,否则转。置cpt初值。计算表达式值result=expr();输出result的值。转。退出。在第4步中所用到的函数需要编写,其余函数用标准库函数完成。,1、expr()函数算法,第一节 表达式解释计算,4/26,(第九章 编程技巧),根据表达式的句法定义,expr()函数有以下形式:double expr()value1=term();/*调用函数term()求项值*/while(当前有效字符为+或-)保存运算符;value2=term();/*求下一个项值*/if(运算符=+)value1+=va
4、lue2;else value1-=value2;return value1;,函数term()的算法与函数expr()的算法类似。函数term()调用factor()完成因子计算,处理乘除法运算符*和/。对于除法需要考虑除数为0的情况。,说明:当前有效字符是指非空白字符。,2、factor()函数算法,第一节 表达式解释计算,5/26,(第九章 编程技巧),当前有效字符为数字符,因子是一个数。转换该数的字符序列为数值,作为因子的值。当前有效字符为左括号(,因子是一个带括号的表达式。递归调用函数expr(),以表达式的值作为因子的值。且表达式计算后,当前有效字符应为右括号),否则因括号不正确配
5、对,是个句法错误的表达式。当前有效字符为其它字符,也是个句法错误的表达式。,double factor()if(当前有效字符为数字符)return num();if(当前有效字符=()移动字符指针至下一个有效字符;fvalue=expr();if(当前有效字符=)移动字符指针至下一个有效字符;,else 报告表达式句法错误;return fvalue;else 报告表达式句法错误;return 1.0;,注意:这里没有讨论有)而没有(与之匹配的问题。,三、完整的程序,第一节 表达式解释计算,6/26,3、num()函数算法 num()函数实际上就是字符转换成数值。先转换整数部分,整数每位数字乘
6、以10,使用循环完成。如果还有小数点.,则小数点后面的数字乘以0.1,仍使用循环完成。,(第九章 编程技巧),例9.1 表达式解释计算的详细程序见li9_1.c。运行结果:9+8(enter)he result is 17.000000(10-20)*2+5(enter)he result is-15.0000009+9*9+1-(10/2+1.9)(enter)he result is 84.100000(enter),注:最后一个回车退出循环。,第二节 C与操作系统接口设计,(第九章 编程技巧),7/26,一、BIOS中断表,在C语言编程中,常常需要利用操作系统如DOS的低级资源,而这些资
7、源不能使用C编译程序访问,必须使用DOS中断(软中断)才能完成。中断是一种特殊类型的指令,它停止执行当前程序,把系统当前状态保留在堆栈中,然后转移到由中断号确定的相应的中断处理子程序上。当中断子程序执行完时,它执行中断返回,使原先运行的程序恢复执行。中断分为硬中断和软中断两种基本类型。8086CPU允许程序通过INT指令执行软中断,跟在INT指令后的数字指定所用的中断号。如:INT 21h执行21h号中断。中断号是用来找出相应的中断处理程序的。在PC-DOS中,使用软中断访问操作系统功能,每个中断命令都有其专门的访问功能类型,而且这些功能函数是由AH寄存器中的值决定的。如果需要增加信息,所增加
8、的信息传给AX、BX、CX和DX寄存器。PC-DOS操作系统分为ROM-BIOS(Basic I/O System)和DOS(Disk OperatingSystem)两部分。ROM-BIOS提供最低层子例程,而DOS用这些低层子例程提供进一步的高级功能。二者是交叉在一起的,用户访问它们的方法基本相同:都是通过软中断。,BIOS中断表(续),第二节 C与操作系统接口设计,8/26,(第九章 编程技巧),在PC-DOS中,ROM-BIOS有12个中断,如下表所示:,访问上表中断有两种方法:一是使用系统调用函数int86(),二是使用汇编语言接口来实现。大多数C都提供了int86()函数。int8
9、6()函数的一般格式如下:,#include int int86(int intnum,union REGS*in,union REGS*out),二、利用int86()函数访问BIOS系统功能,第二节 C与操作系统接口设计,9/26,在int86()中需要用到的结构体与共用体如下:,(第九章 编程技巧),/*字符寄存器*/struct WORDREGS unsigned int ax,bx,cx;unsigned int dx,si,di unsigned int cflag,flags;,struct BYTEREGS unsigned char al,ah;unsigned char b
10、l,bh;unsigned char cl,ch;unsigned char dl,dh;/*寄存器字节*/,unionREGS struct WORDREGS x;struct BYTEREGS h;,例9.2 利用10h号中断功能6,可以实现清屏。,#include void cls()union REGS r;r.h.ah=6;/*屏幕转动程序*/r.h.al=0;/*清屏程序*/r.h.ch=0;/*上转起始行*/,r.h.cl=0;/*列起始*/r.h.dh=24;/*上转结束行*/r.h.dl=79;/*列结束*/r.h.bh=7;/*空行是黑色*/int86(0 x10,例9.3
11、 调用16h号中断0号功能读取键盘扫描码。,第二节 C与操作系统接口设计,10/26,(第九章 编程技巧),在为IBM-PC及其兼容机编程时,最难读到的是箭头键和功能键,以及INS、DEL、PGUP、PGDN、END、HOME等键的ASCII码值。键值存放如下:,当用户在IBM-PC机上按下一个键时,产生一个称为扫描码的两个字节(16位)的值。该扫描码由两部分组成:低位字节内含相应键的ASCII码(若它是标准键),高位字节内含该键在键盘上的定位码。对于标准键的定位码为0,因此低位的值就是它的ASCII码值(8位)。而对于特殊键,它的低位值为0,高位的值才是它的键值。要得到特殊键值,不能使用ge
12、ts()、scanf()等函数,只能用这里提供的方法。其程序参见li9_3.c。运行结果显示部分键盘扫描码为:,A-65 a-97 0-48 9-57(-40+-43-60=-61左箭头-75 右箭头-77 上箭头-72 下箭头-80INS-82 DEL-83 F2-60 F3-61 F10-68 ESC-27 PGUP-73 PGDN-81 END-79 HOME-71,int get_key()union REGS r;r.h.ah=0;return(int86(0 x16,三、利用DOS访问系统功能,第二节 C与操作系统接口设计,11/26,(第九章 编程技巧),PC-DOS中由ROM-
13、BIOS引导装入程序装入和执行的部分叫DOS。其中包含了大部分在ROM-BIOS例程中找不到的各种各样的高级功能,利用AH寄存器传送所请求的DOS功能调用号,通过中断21h可以访问DOS的所有功能。如1号功能为从键盘读字符,2号功能为在屏幕上显示字符,3号功能从异步端口读字符,4号功能写字符到异步端口,5号功能在打印机上打印字符,B号功能检查键盘状态,2A号功能读取系统日期,2B号功能设置系统日期,2C号功能读取系统时间等。虽然可以像ROM-BIOS功能一样,利用int86()函数访问DOS功能,但很多系统都有一专门函数bdos(),该函数用来执行21h号中断调用,调用操作系统中的某个高级功能
14、。bdos()函数原型如下:int bdos(int fnum,unsigned int Reg_DX,unsigned int Reg_AL)其中:fnum是DOS功能号;Reg_DX的值赋给DX寄存器;Reg_AL的值赋给AL寄存器;返回值:bdos()回送AX寄存器的值。,例9.4 调用12h号中断Bh号功能检查键盘状态。,第二节 C与操作系统接口设计,12/26,(第九章 编程技巧),#include int kbhit()return(char)bdos(0 xB,0,0);,除了第一个参数外。其余都用0,因为不需要其它信息。把返回值强行变成char型是必要的,因为所返回的是在AL中
15、的状态,而AL没有定义。,kbhit()函数的返回值为:如果按下键,则返回“真”,否则返回“假”。kbhit()函数的一个非常普通的用途就是可以让某个子例程被用户命令所中断。例9.5 调用12h号中断的3号功能读串口,4号功能写串口。如果需要编写一个调制解调器程序,就要用到在异步串行口上进行读写字符。,/*向串口写字符*/#include int put_async(char ch)bdos(0 x4,ch,0);,/*从串口读字符*/#include int get_async()return(char)bdos(0 x3,0,0);,这里又强行作了char转换,以保证放在AH寄存器中的任何
16、值,都不会让任何调用例程乱了套。,第三节 与汇编语言的接口,(第九章 编程技巧),13/26,在C语言编程中,需要使用汇编语言编写例程大概有三个方面的原因:为了提高速度和效率。为了实现某些C语言中不具备、但为不同的机器所特有的功能。为了利用通用的汇编语言例程。把汇编程序模块和用户的C程序结合起来,主要有两种方法:第一种,单独编写汇编例程,然后再将它同自己的程序连接起来。第二种,使用大多数C编译器所具有的内部汇编程序功能。有的C编译系统内部汇编使用#asm开头,使用#endasm结束,中间全部是汇编语句。而TurboC则不同,每行汇编语句都要以asm开头。如:两数相乘的函数。,int mul(i
17、nt a,int b)#asm mov ax,word ptr 8bp imul ax,word ptr 10bp#endasm,int mul(int a,int b)asm mov ax,word ptr 8bp asm imul ax,word ptr 10bp,第四节 程序调试问题,(第九章 编程技巧),14/26,一、程序易出错问题,1、忘记定义变量。如:main()x=6;y=8;z=x+y;在使用变量之前,必须加:int x,y,z;定义变量。2、输入输出数据的类型与所用格式说明符不一致。如:int a=3;float b=4.5;printf(“%f,%dn”,a,b);结果可
18、能不是所需。3、未注意int型数据的取值范围。如:int num;num=89101;printf(“%dn”,num);造成num超界益出。4、输入变量时忘记使用地址符。如:scanf(“%d%d”,a,b);应改为:scanf(“%d%d”,这时屏幕显示:Intput a&b:后再输入:3,4(Enter),6、误把“=”作为“等于”比较符。,第四节 程序调试问题,15/26,(第九章 编程技巧),如:if(a=b)应改为:if(a=b)应该用“=”作为“等于”比较符。前则为赋值操作。7、语句后面漏分号。如:a=4 b=5 应为:a=4;b=5;复合语句的最后一句也应该有分号。如:t=a;
19、a=b;b=t 应改为:t=a;a=b;b=t;8、在不该加分号的地方加分号。如:if(ab);printf(“a is larger than b.n”);再如:for(i=0;i10;i+);scanf(“%d”,while语句后面少了右括号。,11、在用标识符时,忘记了大写字母和小写字母的区别。,第四节 程序调试问题,16/26,(第九章 编程技巧),如:int a,b,c;a=2;b=3;C=A+B;大写与小写字母在C语言中为两个不同的标识符。12、引用数组元素时误用了圆括号。如:int i,a(10);for(i=0;i10;i+)scanf(“%d”,16、混淆字符数组与字符指针的
20、区别。,第四节 程序调试问题,17/26,(第九章 编程技巧),如:char s120,*s2;s1=“Computer and C”;s2=“C and Computer”;printf(“s1=%s,s2=%sn”,s1,s2);编译出错。s1是数组名,代表数组首地址,是常量,不能再赋值。而s2是指向字符数据的指针变量,对s2可以赋值。s1的赋值应改为:strcpy(s1,”Computer and C);或 char s120=“Computer and C”;17、在引用指针变量之前没有对它赋予确定的值。如:char*p,c20;scanf(“%s”,p);应在这两句间加上:p=c;语
21、句。18、switch语句的各分之中漏写break语句。,switch(score)case 5:printf(“Very good!”);case 4:printf(“Good!”);break;case 3:printf(“Pass!”);case 5:printf(“Fail!”);defult:printf(“data error!”);,如果score为5时,将打印出:Very good!Good!而我们希望只有Very good!输出。程序应改为:在每个case标号后执行完对应的语句之后要加上break;语句。case后是一个常量值,可用整型数据常量或字符数据常量,只起语句标号作用
22、。,19、混淆字符和字符串的表示形式。,第四节 程序调试问题,18/26,(第九章 编程技巧),如:char sex,a10=a;sex=“M”;应改为:char sex,a10=“a”;sex=M;20、使用自加(+)和自减(-)运算符时出错。如:int*p,a4=1,3,5,7,x;p=a;x=*p+;作用是将*p(这时应为a0)赋给x,然后p再自加变成指向a1元素。但过段时间后,程序员觉得应该为:将p所指向元素的值乘以p所指向元素的值再赋给x。写成:x=*p+*(*p);其结果相当于:a0*a1,这不是我们要的结果a0*a0。应为:x=*p*(*p+);21、有人习惯用传统的方式对函数形
23、参进行声明,但却把对函数的形参和函数中的局部变量混在一起定义。如:max(x,y)应改为:max(x,y)int x,y,z;int x,y;int z;z=xy?x:y;z=xy?x:y;return(z);return(z);,22、所调用的函数在调用语句之后才定义,而又在调用之前未加声明。,第四节 程序调试问题,19/26,(第九章 编程技巧),如:main()float max(float x,float y)float x=3.5,y=-7.6,z;return(xy?x:y);z=max(x,y);必须在main()函数中声明:printf(“%fn”,z);float max(f
24、loat,float);或在main()函数之前定义max函数。23、误认为形参值的改变会影响实参的值。如:main()swap(int x,int y)int a=3,b=4;int t;swap(a,b);t=x;x=y;y=t;printf(“%d,%dn”,a,b);可用指针方式定义swap函数解决传递。24、函数的实参和形参类型不一致。如:main()fun(float x,float y)int a=3,b=4,c;c=fun(a,b);可改为:fun(int x,int y)或a,b为float。,25、不同类型的指针混用。,第四节 程序调试问题,20/26,(第九章 编程技巧)
25、,如:main()int i=3,*p1;float a=1.5,*p2;p1=,应改为:main()int i=3,*p1;float a=1.5,*p2;p1=,26、没有注意函数参数的求值顺序。如:i=3;printf(“%d,%d,%dn”,i,+i,+i);有些系统输出:3,4,5。而TurboC等有些系统则输出:5,5,4。即求函数参数表达式的值的顺序有:从左向右的,也有从右向左的,注意区分顺序。一般改用先赋值,再带入不含+或-的变量。27、混淆数组名与指针变量的区别。,如:int i,a5;for(i=0;i5;i+)scanf(%d”,a+);,应改为:int i,a5,*p;
26、for(p=a,i=0;i5;i+)scanf(%d”,p+);,28、混淆结构体类型与结构体变量的区别,对一个结构体类型赋值。,第四节 程序调试问题,21/26,(第九章 编程技巧),如:struct worker int num;char name20;worker.num=1001;strcpy(worker.name,”Li”);,29、使用文件时忘记打开,或打开方式与使用情况不匹配。如:对文件的读写,用只读方式打开,却企图向该文件输出数据。此外,有的程序常忘记关闭文件,虽然系统会自动关闭所用文件,但可能会丢失数据。其它还有指针引用错误造成野指针、函数重名、恶性语法错误、边界错误、函数
27、说明的疏漏、调用参数错误、scanf()与gets()函数的区别等等。以上错误在多练习C语言编程后可以克服,并且也容易检查。在深入使用C语言后,还会出现其它一些更深入、更隐蔽的错误。,应改为:struct worker int num;char name20;worker1;worker1.num=1001;strcpy(worker1.name,”Li”);,二、程序出错的三种情况,第四节 程序调试问题,22/26,(第九章 编程技巧),1、语法错误 程序违背C语法规定引起的错误。编译程序一般能发现错误,根据“出错信息”可以很快发现并及时纠正。2、逻辑错误 程序没有语法错误,但执行结果与原意
28、不符。如:sum=0;i=1;while(i=100)sum=sum+i;i+;最后两句应用一对花括号括起来。这种错误比较难查,要求程序员有较丰富的经验。3、运行错误 程序既无语法错误,也无逻辑错误,但在运行时出现错误,甚至停止运行。如:int a,b,c;scanf(“%d%d”,如果输入a的值为0的话,就会出错。因此程序应经受各种数据的“考验”,应具有“健壮性”。写完一个程序只能说完成任务的一半(甚至不到一半)。调试程序往往比写程序更难,更需要精力、时间和经验。程序员需要通过大量的实践来掌握调试程序的方法和技术。,三、程序调试,第四节 程序调试问题,23/26,(第九章 编程技巧),所谓程
29、序调试是指对程序的查错和排错。调试程序一般经过以下几个步骤。1、人工检查,即静态检查 程序写好后,首先应进行人工检查。作为一个程序员应当养成严谨的科学作风,每一步都要严格把关,不要把问题留给后面的工序。为了更有效地进行人工检查,编程人员应力求做到以下几点:应当采用结构化程序方法编程,以增加可读性。尽量多加注释,以帮助理解每段程序的作用。在编写复杂程序时,不要将全部语句都写在main函数中,而要多利用函数,用一个函数来实现一个单独的功能。各函数之间除用参数传递数据这一渠道外,数据间尽量少出现偶合关系,便于分别检查和处理。编写程序时,要注意编程风格。好的编程风格也便于查错。2、上机调试 通过上机发
30、现错误称为动态检查。主要根据编译时给出的错误信息来纠错。应当注意:有时提示的出错行并不是真正的出错行,应往上查找。可能改了前面的某一个错误后,后面的大片错误就消失。要分析,找出真正的错误行。,3、检查运行结果的正确性,第四节 程序调试问题,24/26,(第九章 编程技巧),在改正“错误”(error)和“警告”(warning)后,程序经过连接(link)就得到可执行的目标程序。运行程序,输入程序所需要的数据,就可得到运行结果。还应当分析运行结果,检查它是否符合要求。在验证程序时,应精心选择典型、苛刻而带有刁难性的几组数据进行输入,看它是否能够得出满足要求的结果。4、检查运行结果错误的方法 如
31、果运行结果不对,大多属于逻辑错误。对这类错误往往需要仔细检查和分析才能发现。可以采用以下方法:将程序与算法仔细对照。算法正确,就是程序错误,仔细对照就很快发现错误。算法不对,就修正算法,再改写程序。如果实在找不到错误,就采取“分段检查”的方法。在程序不同位置设计个printf()函数输出相关信息或变量值,逐段往下查。直到找到在某一段中数据不对为止。也可用“条件编译”命令来处理调试用的printf()函数语句,用完不必删除。如果程序没有错误,就要检查算法了。修改算法后再修定程序。有的系统还提供了debug(调试)工具,可跟踪程序运行并给出相应信息。,三、C的存储方式,第四节 程序调试问题,25/
32、26,(第九章 编程技巧),在使用8086系列处理机上运行C程序时,C编译系统都提供了6种存储方式:小方式、一般方式、中等方式、压缩方式、大方式、特大方式。8086用分段存储结构,有四个段:代码段、数据段、堆栈段和附加段。一个段在RAM中占64K,段的起点正好是16字节的偶数倍。1、小方式 要求程序、数据和栈都必须在同一64K段内。用它编译的程序,目标码最少,执行起来最快。可用DOS的EXE2BIN命令转换成.COM文件。2、一般方式 这种方式是缺省方式,用得最多。程序代码单独占用64K,数据码占用另外64K。按这种方式编译的程序最大为128K。3、中等方式 机器码超过了一般方式下的一个段的限
33、制的大程序,就要用中等方式编译。这种方式适合只使用少量数据的大程序。4、压缩方式 压缩方式与中等方式相反,适合使用大量数据,但本身程序不大的情况。,5、大方式,第四节 程序调试问题,26/26,(第九章 编程技巧),大方式允许程序码和数据都使用多个段。但最大数据项(如数组)单项最多只能占用64K当程序和数据都很大时,要用大方式。6、特大方式 特大方式与大方式有一点不同,数据单独可占64K以上的内存。方式的选择:除非有特殊理由,一般情况下都应使用一般方式。当程序很大,但数据不多时,可选用中等方式。而程序不大,数据却很多时,应选用压缩方式。如果程序和数据都不小,则选用大方式。当某些数据项目单项大于64K时,用特大方式。在8086上运行C程序,还可以使用far、near、huge三个说明符来解决存储方式混用的问题,它们只能用于指针和函数。详细信息参见有关资料。,