调用一个函数的目的有两种,一种是直接在被调函数里输出结果,另一种是从它那里得到一种供程序继续使用的中间值。
19.3.1 函数的类型力求简单
如果要求使用一个二维数组定义复数结构并写出复数加法的计算函数。假设定义一个二维数组
double a[3][2];
分别用这个数组0和1行存储两个运算数据,结果存入第3行。
可以用不同的方法设计这个程序,比较几种方法的特点。
【例19.16】下面是使用指针并返回实数型的复数加法程序,分析一下它存在的问题。
#include <stdio.h>double *add(double*);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double *p=a[0]; add(p); printf("%lf+%lfi/n",p[4],p[5]); p=b[0]; add(p); printf("%lf+%lfi/n",p[4],p[5]); p=c[0]; add(p); printf("%lf+%lfi/n",p[4],p[5]); return 0;}double *add(double *p){ p[4] = p[0] + p[2]; p[5] = p[1] + p[3]; return p;}
这个程序将函数add设计得很复杂,是返回指针的函数。add函数里面使用语句
return p;
即可。但在主程序中,add修改了数组的内容,所以不需要设计指针变量接收函数的返回值。但在主函数中调用时,由于主函数里的printf使用指针输出,所以不能使用数组首地址。可以改为使用数组输出,例如:
add(b[0]);printf("%lf+%lfi/n",b[2][0],b[2][1]);
其实,没有必要将函数设计为返回指针的函数。如果将函数原型改为
double add(double *);
在add函数里使用
return *p;
即可。主程序的调用方法一样,既可以使用指针,也可以使用数组的首地址。
既然不使用函数返回值,最简单的应该是将它设计为void类型。
【例19.17】将add设计为void类型,参数使用指针的例子。
将add函数的原型设计为
void add(double *);
函数设计如下:
void add(double *p){ p[4] = p[0] + p[2]; p[5] = p[1] + p[3];}
在主函数里面既可以使用指针,也可以使用数组首地址。
19.3.2 实参要与函数形参的类型匹配
【例19.18】将add设计为void类型,参数使用数组的例子。
重点主要是数组的表示方法。这里的add函数仍然使用原来的设计方法,这就要求数组的标识符也选择p,省去改写add函数的麻烦。完整的程序如下。
#include <stdio.h>void add(double[ ]);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double *p=a[0]; add(p); printf("%lf+%lfi/n",a[2][0],a[2][1]); add(b[0]); printf("%lf+%lfi/n",b[2][0],b[2][1]); add(c[0]); printf("%lf+%lfi/n",c[2][0],c[2][1]); return 0;}void add(double p){ p[4] = p[0] + p[2]; p[5] = p[1] + p[3];}
这个程序使用语句
double *p=a[0];
定义指针变量p,是因为a是二维数组,其首地址是a[0]。如果使用语句
double *p=a;
在第1次扫描时会有警告信息,虽然能通过二次扫描,运行结果也正确,但应该避免警告信息,即应采取正规的方法定义指针变量。
运行结果如下:
6.000000+4.000000i7.000000+7.000000i5.000000+2.000000i
【例19.19】下面也是将add设计为void类型,参数使用数组的例子。虽然运行正常,但有编译警告信息,请消除这些警告信息。
#include <stdio.h>void add(double [ ][2]);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double *p=&a[0][0]; add(p); printf("%lf+%lfi/n",a[2][0],a[2][1]); add(b[0]); printf("%lf+%lfi/n",b[2][0],b[2][1]); add(c[0]); printf("%lf+%lfi/n",c[2][0],c[2][1]); return 0;}void add(double a[2]){ a[2][0] = a[0][0] + a[1][0]; a[2][1] = a[0][1] + a[1][1];}
【解答】二维函数原型声明中的参数只需要列数,不需要行数。Add函数原型声明和定义均无问题,看来是调用方式产生的警告信息。
对它进行第1次扫描,编译信息如下:
warning C4047: 'function' : 'double (*)[2]' differs in levels of indirection from 'double *'warning C4024: 'add' : different types for formal and actual parameter 1warning C4047: 'function' : 'double (*)[2]' differs in levels of indirection from 'double [2]'warning C4024: 'add' : different types for formal and actual parameter 1warning C4047: 'function' : 'double (*)[2]' differs in levels of indirection from 'double [2]'warning C4024: 'add' : different types for formal and actual parameter 1answer.obj - 0 error(s), 6 warning(s)
果真如此!分析警告信息,把调用b和c改为如下方式:
add(b);add(c);
这时只有指针p作为参数带来了警告信息。注意“double(*)[2]”意思是需要一维数组的指针才能匹配。应该用如下方式定义并使用指针。
double (*p)[2];p=a;
或者直接定义如下:
double (*p)[2]=a;//完整程序#include <stdio.h>void add(double [ ][2]);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double (*p)[2]; // 可直接用double (*p)[2]=a; p=a; add(p); printf("%lf+%lfi/n",a[2][0],a[2][1]); add(b); printf("%lf+%lfi/n",b[2][0],b[2][1]); add(c); printf("%lf+%lfi/n",c[2][0],c[2][1]); return 0;}void add(double a[2]){ a[2][0] = a[0][0] + a[1][0]; a[2][1] = a[0][1] + a[1][1];}
19.3.3 正确设计函数的返回方式
【例19.20】使用一个二维数组定义复数结构并编写复数除法的计算函数。
【解答】假设定义一个二维数组
double a[3][2];
分别用这个数组0和1行存储两个运算数据,结果存入第3行。
因为用数组作为参数,所以能够返回计算结果。对这种情况,最简单的是将函数设计为void类型。参数选择指针,原型如下:
void pe(double *);
完整的源程序如下:
#include <stdio.h>#include <stdlib.h>void pe(double *);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double *p; p=a[0]; pe(p); printf("%lf%+lfi/n",p[4],p[5]); p=b[0]; pe(p); printf("%lf%+lfi/n",p[4],p[5]); p=c[0]; pe(p); printf("%lf%+lfi/n",p[4],p[5]); return 0;}void pe(double *p){ double d; if ((p[2]==0)&&(p[3]==0)){ printf("除数为0,退出!/n"); exit(1) ; } d=p[2] * p[2]+p[3] * p[3]; p[4] = (p[0] * p[2]+p[1] * p[3])/d; p[5] = (p[1] * p[2] - p[0] * p[3])/d;}
运行结果如下:
2.200000+0.400000i0.689655-0.724138i除数为0,退出!
主函数里使用指针下标计算,所以3个输出语句完全一样。
对于除法函数而言,如果不处理除数为0的情况,就会使程序出错。所以在这个函数里使用exit函数退出。
因为pe函数的参数是double型指针,所以可以使用指针下标进行计算。
如果使用数组,其方法与例19.19的相同。使用数组的源程序如下:
#include <stdio.h>#include <stdlib.h>void pe(double [2]);int main(){ double a[3][2]={4,3,2,1}; double b[3][2]={5,2,2,5}; double c[3][2]={5,2}; double (*p)[2]; p=a; pe(p); printf("%lf%+lfi/n",a[2][0],a[2][1]); pe(b); printf("%lf%+lfi/n",b[2][0],b[2][1]); pe(c); printf("%lf%+lfi/n",c[2][0],c[2][1]); return 0;}void pe(double a[2]){ double d; if ((a[1][0]==0)&&(a[1][1]==0)){ printf("除数为0,退出!/n"); exit(1) ; } d=a[1][0]*a[1][0]+a[1][1]*a[1][1]; a[2][0] = (a[0][0] * a[1][0]+a[0][1] * a[1][1])/d; a[2][1] = (a[0][1] * a[1][0]-a[0][0] * a[1][1])/d;}
【例19.21】使用一个二维数组定义复数结构并编写复数除法的计算函数。要求不断从键盘输入两个复数进行计算,当两个复数均为0时,结束程序运行。找出下面程序中的错误并改正之。
#include <stdio.h>#include <stdlib.h>void pe(double *);int main(){ double a[3][2]={0,0}; double *p=a[0]; for(;;) { printf("输入第1个复数:"); scanf("%lf%lfi",&p[0],&p[1]); printf("输入第2个复数:"); scanf("%lf%lfi",&p[2],&p[3]); if((p[0]==0)&&(p[1]==0)&&(p[2]==0)&&(p[3]==0)) { printf("再见!/n"); return 0; } if((p[0]==0)&&(p[1]==0)) printf("被除数为0,本次作废,继续输入。/n"); pe(p); printf("计算结果:%lf%+lfi/n",p[4],p[5]); }}void pe(double *p){ double d; if ((p[2]==0)&&(p[3]==0)){ printf("除数为0,本次作废,继续输入。/n"); return; } d=p[2] * p[2]+p[3] * p[3]; p[4] = (p[0] * p[2]+p[1] * p[3])/d; p[5] = (p[1] * p[2] - p[0] * p[3])/d; return;}
先运行程序检验给定的几个条件,以便从中找出错误。
运行结果如下:
输入第1个复数:0 0输入第2个复数:9 8被除数为0,本次作废,继续输入。计算结果:0.000000+0.000000i输入第1个复数:0 0输入第2个复数:0 3被除数为0,本次作废,继续输入。计算结果:0.000000+0.000000i输入第1个复数:0 0输入第2个复数:0 0再见!
由此可见,满足前2个条件时,执行了printf语句。被除数为0时,不应调用pe函数。增加
continue;
语句,使它返回到循环的起点即可。第2个问题也是执行了printf语句。可以在pe函数里返回一个值,例如置“p[2]=-1”在主程序中判别这个值,如果“p[2]=-1”,则返回起点。
修改后的源程序如下:
#include <stdio.h>#include <stdlib.h>void pe(double *);int main(){ double a[3][2]={0,0}; double *p=a[0]; for(;;){ printf("输入第1个复数:"); scanf("%lf%lfi",&p[0],&p[1]); printf("输入第2个复数:"); scanf("%lf%lfi",&p[2],&p[3]); if((p[0]==0)&&(p[1]==0)&&(p[2]==0)&&(p[3]==0)) { printf("再见!/n"); return 0; } if((p[0]==0)&&(p[1]==0)) { printf("被除数为0,本次作废,继续输入。/n"); continue; } pe(p); if(p[2]==-1) continue; printf("计算结果:%lf%+lfi/n",p[4],p[5]); }}void pe(double *p){ double d; if ((p[2]==0)&&(p[3]==0)){ printf("除数为0,本次作废,继续输入。/n"); p[2]=-1; return; } d=p[2] * p[2]+p[3] * p[3]; p[4] = (p[0] * p[2]+p[1] * p[3])/d; p[5] = (p[1] * p[2] - p[0] * p[3])/d; return;}
运行测试如下:
输入第1个复数:0 0输入第2个复数:2 5被除数为0,本次作废,继续输入。输入第1个复数:2 5输入第2个复数:0 0除数为0,本次作废,继续输入。输入第1个复数:1 0输入第2个复数:1 1计算结果:0.500000-0.500000i输入第1个复数:0 1输入第2个复数:1 1计算结果:0.500000+0.500000i输入第1个复数:0 1输入第2个复数:1-1计算结果:-0.500000+0.500000i输入第1个复数:0 0输入第2个复数:0 0再见!
针对这个程序而言,可以将判别除数为0的任务交给主程序,简化pe程序的设计。也就是只有满足计算条件的情况下,才调用pe函数。
【例19.22】这是个修改例19.21的程序。程序计算结果正确,但不能退出程序,请找出原因。
#include <stdio.h>#include <stdlib.h>void pe(double *);int main(){ double a[3][2]={0,0}; double *p=a[0]; for(;;) { printf("输入第1个复数:"); scanf("%lf%lfi",&p[0],&p[1]); printf("输入第2个复数:"); scanf("%lf%lfi",&p[2],&p[3]); if((p[0]==0)&&(p[1]==0)) { printf("被除数为0,本次作废,继续输入。/n"); continue; } if((p[2]==0)&&(p[3]==0)){ printf("除数为0,本次作废,继续输入。/n"); continue; } if((p[0]==0)&&(p[1]==0)&&(p[2]==0)&&(p[3]==0)) { printf("再见!/n"); return 0; } pe(p); printf("计算结果:%lf%+lfi/n",p[4],p[5]); }}void pe(double *p){ double d; d=p[2] * p[2]+p[3] * p[3]; p[4] = (p[0] * p[2]+p[1] * p[3])/d; p[5] = (p[1] * p[2] - p[0] * p[3])/d;}
【解答】判断语句的顺序错误,造成不可能执行。一定要把复合语句
if((p[0] == 0) && (p[1 ]== 0) && (p[2] == 0) && (p[3] == 0))
放在第1个,以便保证能够退出。主函数只有这个出口,所以在for循环体之外是不需要return语句的。注意不要多加这条执行不到的“return 0;”语句。
19.3.4 正确设计和使用函数指针
【例19.23】本程序是输出多项式x 2+5x+8和x 3-6x在区间[-1,+1]增长步长为0.1时的所有结果。找出程序中的错误。
#include <stdio.h>double f1(double x);double f2(double x);#define STEP 0.1int main( ){ int i; double x, (*p)(double); //double (*p)(double)与double (*p)( )等效 for( i=0; i<2; i++) { printf("第%d个方程:/n",i+1); if ( i==0) p = f1(x); else p = f2(x); for( x = -1; x <= 1; x += STEP) printf("%f/t%f/n", x, (*p)(x)); } return 0;}//函数 f1( )double f1(double x){ return ( x*x + 5*x +8);}// 函数f2( )double f2(double x){return( x*x*x-6*x );}
给函数指针变量赋值时,只需要给出函数名而不必给出参数。因为语句
p=f1;
是将函数入口地址赋给指针变量p,不涉及实参与形参结合的问题;而写成
p=f1(x);
的形式则是错误的。
正如数组名代表的是数组起始地址,这里的函数名则代表函数的入口地址。这里的p就是指向函数f1的指针变量,它和f1都指向函数的开头,调用p就是调用f1。与过去所讲的变量的重要区别是:它只能指向函数的入口处,而不能指向函数中间的具体指令。因此,*(p+1)、p+n、p--及p++等运算对它都是没有意义的。
同理,“p=f2(x);”也是错误的,应为“p=f2;”。
“double(*p)();”语句仅仅说明定义的p是一个指向函数的指针变量,此函数带回实型的返回值。但它并不是固定指向哪一个函数的,而只是表示定义了这样一个类型的变量,专门用来存放函数的入口地址。在程序中把哪一个函数的地址赋给它,它就指向那一个函数。这个程序是使用循环语句分别计算两个函数。
下面给出部分运行结果。
第1个方程:-1.000000 4.000000-0.900000 4.310000… …1.000000 14.000000第2个方程:-1.000000 5.000000… …0.900000 -4.6710001.000000 -5.000000
【例19.24】这是一个使用指向函数的函数指针变量作为参数的例子。第1次扫描有警告信息,请消除这个警告信息。
#include <stdio.h>void all (int, int, int (*)());int max(int,int ), min(int,int ),mean(int,int );void main( ){ int a, b; printf("输入a和b的值:"); scanf("%d %d",&a,&b); printf("max="); all(a,b,max); printf("min="); all(a, b, min); printf("mean="); all(a,b,mean);}void all (int x, int y,int (*func)(int x, int y)){ printf("%d/n",(*func)(x,y)); }int max(int x, int y){ if ( x > y ) return x; else return y;}int min(int x, int y){ if ( x < y ) return x; else return y;}int mean(int x, int y){ return( (x+y)/2 );}
【解答】不要认为是用第3句程序存在问题。用int在一行声明3个函数是完全正确的。如果使用“void all(int x,int y,int(*func)(int,int)”定义,则声明原型时必须与定义的参数形式一致,即应该使用
void all (int, int, int (*)(int,int));
其实,声明时不带参数最方便。所以可以修改定义为
void all (int x, int y,int (*func)( )){ printf("%d/n",(*func)(x,y)); }
这里是用函数指针作为函数参数,所以应将函数指针看做形参,函数名看做实参,用实参直接代替形参。
如果要按部就班将函数名赋给函数指针,在主程序中声明一个函数指针即可。下面是使用这种方法的源程序。
#include <stdio.h>void all (int, int, int (*)());int max(int,int ), min(int,int ),mean(int,int );int main( ){ int a, b; int (*p)(); //声明函数指针 printf("输入a和b的值:"); scanf("%d %d",&a,&b); p=max; //指向一个具体函数 printf("max="); all(a,b,p); //作为函数的参数 printf("min="); p=min; all(a, b, p); printf("mean="); p=mean; all(a,b,p); return 0;}void all (int x, int y,int (*func)()){ printf("%d/n",(*func)(x,y)); }int max(int x, int y){ if ( x > y ) return x; else return y;}int min(int x, int y){ if ( x < y ) return x; else return y;}int mean(int x, int y){ return( (x+y)/2 );}
运行示例:
输入a和b的值:32 98max=98min=32mean=65
【例19.25】分别使用声明函数指针和直接实参和形参结合的两种方法,编写求函数10x 2-9x+2在区间[0,1]内x以0.01的增量变化的最小值。
先声明函数指针,将被调函数赋给指针的参考程序如下。
#include <stdio.h>double s1=0.0;double s2=1.0;double step=0.01;double func( ),MinValue(double(*)( ));int main ( ){ double (*p)( ); //定义函数指针 p=func; //指向目标函数 printf("最小值是:%2.3f/n", MinValue(p)); return 0;}double func(double x) //目标函数{return (10*x*x-9*x+2);}double MinValue(double(*f)( )) //定义求最小值函数,它包括函数指针{ double x=s1, y=(*f)(x); //定义变量y与func函数返回类型一致 while( x <= s2 ){ if( y > (*f)(x) ) y=(*f)(x); x += step; } return y;}
运行结果为
最小值是:- 0.025
直接将函数指针作为参数。
#include <stdio.h>double s1=0.0;double s2=1.0;double step=0.01;double func( ),value(double(*)( ));int main ( ){ printf("最小值是:%2.3f/n", value(func)); //直接使用目标函数作为参数 return 0;}double func(double x) //目标函数{return (10*x*x-9*x+2);}double value(double(*f)( )) //定义求最小值函数,它包括函数指针{ double x=s1, y=(*f)(x); while( x <= s2 ){ if( y > (*f)(x) ) y=(*f)(x); x += step; } return y;}
19.3.5 如何解读函数声明
虽然程序组合方式的定义都很完备,但这些定义有时也容易引起混淆,甚至与人们的直觉相悖。有些语法结构的用法与意义与人们想当然的认识也不一致,例如运算符的优先级、if语句和函数调用等。
这里重点讨论一下如何理解函数声明问题。假如一个函数原型声明如下:
(*( void ( * ) ( ) ) 0 ) ( );
如何理解这个函数的含义呢?
构造函数表达式应遵循的一条简单规则:按照使用的方式来声明。
1.声明一个给定类型的变量
任何C变量的声明都由两部分组成:类型以及一组类似表达式的声明符。从表面上看,声明符与表达式有些类似,对它求值应该返回一个声明中给定类型的结果。最简单的声明符就是单个变量。下面的声明
float f , g;
的含义是:当对其求值时,表达式f和g的类型为浮点数(float)类型。因为声明符与表达式的相似,所以可以在声明符中任意使用括号。按此推理,可知
float ( ( f ) );
的含义是:当对其求值时,表达式((f))的类型为浮点类型,由此可推知,f也是浮点类型。而声明
float f f ( );
的含义是:表达式f f()的求值结果是一个浮点数,也就是说,f f是一个返回值为浮点类型的函数。类似地,
float *pf;
声明的含义是:*pf是一个浮点数,也就是说,pf是一个指向浮点数的指针。
就像在表达中进行组合一样,也可以把以上这些形式在声明中组合起来。因此,
float * g ( ) , ( *h ) ( );
表示*g()与(*h)()是浮点表达式。因为()的结合优先级高于*,也就是*(g())。这显然表明g是一个函数,该函数的返回值类型为指向浮点数的指针。同理,可以分析(*h)()中后面的括号代表函数,而(*h)代表函数指针,h是一个函数指针,h所指向的函数的返回值为浮点类型。
2.类型转换符
一旦知道了如何声明一个给定类型的变量,就很容易得到该类型的类型转换符了。只需要把声明中的变量名和声明尾部的分号去掉,再将剩余的部分用一对括号全部括起来即可。例如,下面的声明
float ( *h ) ( );
表示h是一个指向返回值为浮点类型的函数指针,因此
( float ( * ) ( ) )
就表示一个“指向返回值为浮点类型的函数指针”的类型转换符。
3.分析表达式(*(void(*)())0)()的含义
(1)假定变量fp是一个函数指针,则*fp就是该指针所指向的函数,所以调用该函数的方式是:
(*fp) ( );
注意:必须用一对括号将*fp括起来,否则*fp()实际上与(*fp())的含义完全一样。因为函数运算符“()”的优先级高于运算符,所以(*fp)的含义才与其区别开来。
(2)找一个恰当的表达式替换fp。设计是想在计算机启动时,硬件将调用首地址为0位置的函数。显然,根据定义写出
(*0) ( );
的声明,C编译器并不认可。因为运算符“*”必须要有一个指针来做操作数。而且这个指针还应该是一个函数指针,以便保证经运算符“*”作用后的结果能够作为函数被调用。因此,必须对0进行类型转换,转换后的类型可以描述为“指向返回值为void类型的函数指针”。
如果fp是一个指向返回值为void类型的函数指针,那么(*fp)()的值为void,fp的声明如下:
void (*fp) ( );
这就可以保证用“(*fp)();”完成调用存储位置为0的函数。
但这种写法的代价是多些了一个“哑”变量。
(3)因为一旦知道如何声明一个变量,自然也就知道如何对一个常数进行类型转换,将其转换为该变量的类型。只需在变量声明中将变量名去掉即可,将常数0转换为“指向返回值为void类型的函数指针”类型,可以写作:
(void (*) ( )) 0
用(void(*)())0替换(*fp)()中的fp,从而得到:
(*(void (*) ( )) 0 ) ( );
尾部的分号使得表达式成为语句。
当然,使用typedef能够使表述更加清晰。
typedef void ( *funcptr ) ( );( *funcptr ) 0 ) ( );
4.signal函数
signal库函数里signal函数原型比较复杂。一般表示为:
void ( * signal ( int , void (* )(int)))(int);
signal函数接受两个参数:一个是代表需要“被捕获”的特定signal的整数值;另一个是指向用户提供的函数的指针,该函数用于处理“捕获到”的特定signal,返回值类型为void。
假设用户的信号处理函数为:
void sigfunc( int n){ // 特定信号处理部分}
由此可得sigfunc函数的声明如下:
void sigfunc( int )
声明一个指向sigfunc函数的指针变量sfp,因为sfp指向sigfunc函数,则*sfp就代表了sigfunc函数,从而*sfp可以被调用。假定sig是一个整数,则(*sfp)(sig)的值为void类型,因此可以声明sfp如下:
void (*sfp) ( int ) ;
因为signal函数的返回值类型与sfp的返回类型一样,上式也就声明了signal函数,即
void (*signal(something) ) ( int ) ;
something代表signal函数的参数类型,这里还没有完成对它的声明。
现在已经完成的声明是:传递适当的参数调用signal函数,对signal函数的返回值(为函数指针类型)解除引用(dereference),然后传递一个整数参数调用解除引用后所得函数,最后返回值为void类型。因此,signal函数是一个指向返回值为void类型函数的指针。
现在是要确定signal函数的参数something。signal函数接受两个参数:一个是整型的信号编号,另一个是指向用户定义的信号处理函数的指针。前面已经定义了指向用户定义的信号处理函数的指针sfp如下:
void (*sfp) ( int ) ;
sfp的类型可以通过将上面的声明中的sfp去掉而得到,即void(*)(int)。此外,signal函数的返回值是指向调用前的用户定义的信号处理函数的指针,这个指针的类型与sfp指针类型一致。参数something就是如下形式:
int , void ( * ) ( int )
由此可知,声明signal函数如下:
void (*signal( int , void ( * ) ( int ) ) ) ( int ) ;
同样,使用typedef可以简化上面的函数声明。
typedef void ( *HANDLER ) ( int );HANDLER signal ( int, HANDLER );