第7章 函数

来源:互联网 发布:mv软件下载 编辑:程序博客网 时间:2024/06/05 16:56

7.1 函数基础

7.1.1函数定义

函数是一个完成特定 独立功能的代码模块,其程序代码独立,通常带有返回值,也可以是空值。
一般形式:
<数据类型><函数名称>(形式参数说明)
{
语句系列;
return(<表达式>);
}
注意:
1)<函数名称>是一个标示符,要求符合标识符的命名规则;
2)<数据类型>是整个函数的返回类型,如果无返回值,应该写为void;
3)(形式参数说明)用“,”分隔,通常称为形参;
4)大括号的内容是函数体;在函数体中表达式必须事先声明;
5)return(<表达式>)语句中表达式的值,要和函数的<数据类型>一致,若返回为空,可以省略表达式。

7.1.2函数声明

函数的声明就是指函数原型,其中形参变量名可以省略,但类型不能省。

7.1.3函数的传参与调用

传参的方式 (实质上:拷贝传值)
1)赋值传递
2)传递数组 (传数组名,数组长度);
3)传地址
4)通过全局变量实现函数间的通信。
函数的调用
调用形式:
函数名(实际参数);
注意:函数调用可以作为运算量在表达式中出现,也可以单独形成一条语句;对于无返回值的函数来讲,只能形成一个函数调用语句。
函数名(实参);给形参分配空间,实参的值赋值给形参;
int x = a, int y = b;
返回值
调用完后销毁。

7.2指针函数与函数指针

7.2.1指针函数

指针函数是一个函数,返回值是一个指针而已,
return 本来就只能带回一个值,如何返回一个指针,就可以获得一片空间。

void *malloc(size_t size) //在堆区申请一片空间   

7.2.2函数指针

函数指针是一个指针,指向一个函数。

7.2.3指针与函数的关系

可以把一个指针声明成为一个指向函数的指针。

int fun1(char*,int); int(*pfun1)(char*,int); pfun1=fun1; .... .... int a=(*pfun1)("abcdefg",7);//通过函数指针调用函数。  

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。
例一:

int fun(char*); int a; char str[]="abcdefghijklmn"; a=fun(str); ... ... intfun(char*s) { int num=0; for(int i=0;i{ num+=*s;s++; } return num; }  

  这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同时对str进行了自加1运算。

7.3递归函数

递归函数自己调用自己。(容易找不到出口)
缺点:
1>不断得开辟函数栈空间,调用完后销毁,
2>造成函数栈溢出,
高中的时候就出现很多递归函数,应该是在“级数”那里的习题中出现的,而且还不少。还是从例子开始吧:
f(x)=f(x-1)+x*x ,其中x>0且f(0)=0求f(4)
解: 由于f(0)=0:
当x=1 时 f(1)=f(0)+1*1=1;
当x=2 时 f(2)=f(1)+2*2=5;
当x=3 时 f(3)=f(2)+3*3=14;
当x=4 时 f(4)=f(3)+4*4=30;
所以, f(4)=30.
上学的时候,可能会这样做出来。
f(x)=f(x-1)+x*x ,其中x>0且f(0)=0就是一个递归函数,它用到了f(x)是用f(x-1)定义的。细心的人还可以发现x>0且f(0)=0也是函数的一部分:
x>0提供一个递归区间,而f(0)=0提供了一个初始条件(思维方向不同,在电脑思维中这个条件为终止条件,详见下文)。
或许大家觉得和我们课堂上的递归还是有点不同,不同在哪呢?
这就是人脑和电脑的区别:电脑不会直接去找初始条件去向问题递推。 而是从问题出发,递推下去,直到找到终止条件(解题时的初始条件)。
电脑思维:
f(4)=f(3)+4*4;
f(3)=f(2)+3*3
f(2)=f(1)+2*2
f(1)=f(0)+1*1
f(0)=0; //终止条件
f(1)=f(0)+1*1=1;
f(2)=f(1)+2*2=5;
f(3)=f(2)+3*3=14;
f(4)=f(3)+4*4=30;
这个是电脑的思维过程,也就是计算过程,不会在前台显示出来。“遇到问题,解决问题,输出结果”——这是电脑处理问题的流程。关键在于,怎么写个递归函数让电脑认识。明白递归函数的定义,其实很简单。
递归函数有三个充分条件:第一是函数体,第二是递归区间,第三个是终止条件,只要在代码中全部申明出来,一个递归函数的就写出来了。
上面的递归函数的就可以写出下面的代码:

function squaresum($x){           if($x>0)                                                   //递归区间                  $result=squaresum($x-1)+$x*$x;        //函数体           elseif($x=0)                                              //终止条件                  return $result=0;           return $result;   }   echo squaresum(4); //输出30   

其中用到了if…elseif…语句,这就是来声明递归函数的递归区间和终止条件(x>0且f(0)=0)的。
现在在来写一个正整数n的n!的递归函数就思路很明确了。
分析:正整数n , f(n)=n! =>
函数体:f(n)=n*f(n-1); 递归区间:n.> 1; 终止条件:n=1;

function rank($n)  {           if($n>1)                  $result=$n*rank($n-1);           elseif($n=1)                  return $result=1;           return $result.'<br>';   }   

由此我们可以发现当要写一个递归函数,找到终止条件,一个递归函数就很明朗了,剩下就是语法问题了。
到linux C这块,我们做一个例题:
例:求斐波那契数列第n项。斐波那契数列的第一项和第二项是1,后面每一项是前两项之和,即1,1,2,3,5,8,13,。。。
下面程序采用直接递归调用:

#include <stdio.h>  long fib(int n)  {      if(n == 0 || n == 1)          return 1;      else          return (fib(n-1)+fib(n-2));  }  int main()  {      int i;      for(i = 0;i < 8;i++)          printf("%ld ",fib(i));      printf("\n");      return 0;  }  

程序执行结果如下:
fs@ubuntu:~/qiang/digui$ ./digui1

1 1 2 3 5 8 13 21
递归的条件:
上面已经简单提到,现在再说明一下
一个问题能否用递归来实现,看其是否有如下特点:
1、须有完成函数任务的语句。
例如:下面的代码定义了一个递归函数

#include <stdio.h>  void count(int val)  {      if (val > 1)          count(val - 1);      printf("OK:%d\n",val);  }  

该函数的任务是在输出设备上显示”ok: 整数值“。
2、一个任务是否能够避免递归调用的测试。
例如,上面的代码中,语句”if (val > 1)”便是一个测试,如果不满足条件,就不进行递归调用。
3、一个递归调用语句
该递归调用语句的参数应该逐渐逼近不满足条件,以至最后断绝递归。
例如,上面的代码汇总,语句 “if( val > 1)”便是一个递归调用,参数在渐渐变小,这话总发展趋势能使测试 “if (val > 1)”最终不满足。
4,、先测试,后递归调用
在递归函数定义中,必须先测试,后递归调用。也就是说,递归调用是有条件的,满足了条件,才可以递归。
例如,下面的代码无限制的调用函数自己,造成无限制递归,终将使栈空间溢出;

#include <stdio.h>  void count(int val)  {      count(val - 1);//无限制递归      if (val > 1)          printf("OK:%d\n",val);  }  

下面是完整程序:

#include <stdio.h>  void count(int val)  {      if (val > 1)          count(val - 1);      printf("OK:%d\n",val);  }  int main()  {      int n = 10;      count(n);      return 0;  }  

程序执行结果如下:
这里写图片描述

7.4回调函数

我们先来回顾一下函数指针,函数指针是专门用来存放函数地址的指针,函数地址是一个函数的入口地址,函数名代表了函数的入口地址。当一个函数指针指向了一个函数,就可以通过这个指针来调用该函数,可以将函数作为参数传递给函数指针。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单的讲,一般写程序是你调用系统的API,如果把关系反过来,你写一个函数,让系统调用你的函数,那就是回调了,那个被系统调用的函数就是回调函数。
说详细点,我们知道,编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
我们可以这样理解:有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示:

这里写图片描述

图1

普通函数:你所写的函数调用系统函数,你只管调用,不管实现。
回调函数:系统调用你所写的函数,你只管实现,不管调用。
那回调函数到底是如何使用的呢?我们先来解决个小问题:
1、回调函数在什么场景有用?
我要在特定时候执行一个任务,至于是什么时候我自己都不知道。比如某一时间到了或者某一事件发生或者某一中断触发。
2、回调函数怎么起作用?
把我要执行的这个任务写成一个函数,将这个函数和某一时间或者事件或者中断建立关联。当这个关联完成的时候,这个函数华丽的从普通函数变身成为回调函数。
3、回调函数什么时候执行?
当该回调函数关心的那个时间或者事件或者中断触发的时候,回调函数将被执行。一般是触发这个时间、事件或中断的程序主体(通常是个函数或者对象)观察到有一个关注这个东东的回调函数的时候,这个主体负责调用这个回调函数。
4、回调函数有什么好处?
最大的好处是你的程序变成异步了。也就是你不必再调用这个函数的时候一直等待这个时间的到达、事件的发生或中断的发生(万一一直不发生,你的程序会怎么样?)。再此期间你可以做做别的事情,或者四处逛逛。当回调函数被执行时,你的程序重新得到执行的机会,此时你可以继续做必要的事情了。
借鉴知友的一个例子:
【注意】读者如果只有C语言的基础,以下解读可能看不懂,请自行跳过解读,等有C++的基础之后再来看吧。
你去食堂打饭,你喜欢吃小炒热饭菜,所以你去了一个小炒窗口。
你跟老板说了要×××盖饭,老板说:你是100号,喊到你的号你就来拿菜。
然后你在旁边跟同学吹牛、或者看手机、或者干点你想干的任何事情。。。
然后你听到老板喊100号并且把菜放到窗口,你走到窗口,拿到你的菜。
这里面有几个函数:
老板的部分:
1、老板提供一个点餐的函数 boss.Order(string 菜名,double 钱)
2、老板有个做饭的函数,此函数耗时较长boss.Cook()
3、老板提供一个事件,当boss.cook()执行完时,该事件被触发,boss.OnCookFinish;
你的部分:
1、你需要有一个函数去订餐,也就是你的函数中需要执行类似于boss.Order(“红烧肉盖浇饭”,20),比如是me.Hungry()
2、你需要有一个函数作为回调函数去关注boss.OnCookFinish事件,这样当老板做好饭,你就可以知道是不是你的好了。
由于老板的事件发生的时候中会喊编号并且吧菜放到窗口,所以你的回调函数需要能够接受1个编号和1个菜作为参数。
比如me.AcceptFood(int currNumber,object food)
所以整个程序的流程其实是这样的:

me.Hungry()  {      boss.Order("红烧肉盖浇饭",20);      boss.OnCookFinish+=me.AcceptFood;//此处表面,AcceptFood这个回调函数关心OnCookFinish事件,并且变成这个事件的回调函数  此时这个函数执行完,不再等待  }  boss.Order("红烧肉盖浇饭",20)  {      //收钱      //配菜 前2个耗时较短      boss.Cook();//此处一般会开新线程执行cook动作  }  boss.Cook()  {      //cooking~~~~~~~~~~      //完成了,下面将要触发事件,系统将检查这个事件是否有回调函数关心,有的话逐个回调。      OnCookFinish(100号,红烧肉盖浇饭);  }  

回调函数实例一:

#include<stdio.h>  //函数指针的格式为:int (*ptr)(char *p) 即:返回值(指针名)(参数列表)   //为回调函数命名,类型命名为CallBackFun,参数为char *p  typedef int (*CallBackFun)(char *p);   //Afun,格式符合 CallBackFun 的格式,因此可以看作是一个 CallBackFun     int Afun(char *p)  {      printf("Afun 回调打印出字符%s!\n", p);         return 0;  }  //函数Cfun,格式符合 CallBackFun 的格式,因此可以看作是一个CallBackFun  int Cfun(char *p)  {         printf("Cfun 回调打印:%s, Nice to meet you!\n", p);         return 0;  }  

//执行回调函数
方式一:通过命名方式,pCallBack可以看做是CallBackFun的别名

int call(CallBackFun pCallBack, char *p)  {         printf("call 直接打印出字符%s!\n", p);         pCallBack(p);         return 0;  }  

// 执行回调函数
方式二:直接通过方法指针

int call2(char *p, int (*ptr)())  //或者是int call2(char *p, int (*ptr)(char *))同时ptr可以任意取名  {      printf("======================================\n");          (*ptr)(p);  }  int main()  {         char *p = "hello";      call(Afun, p);         call(Cfun, p);      call2(p, Afun);         call2(p, Cfun);      return 0;  }  

执行结果如下:
这里写图片描述
回调函数应用实例二:

#include <stdio.h>  typedef void (*callback)(char *);  void repeat(callback function, char *para)  {      function(para);      function(para);  }  void hello(char* a)  {      printf("Hello %s\n",(const char *)a);  }  void count(char *num)  {         int i;      for(i = 1;i < (int)num;i++)          printf("%d",i);      putchar('\n');  }  int main(void)  {      repeat(hello,"xiaoqiang");      repeat(count, (char *)4);  }  

执行结果如下:
这里写图片描述

C 函数练习

学习函数主要学习的就是函数的声明、定义和调用,下面请看两个例子,来帮助我们学习函数:
题目一
编写一个函数iswithin(),它接受两个参数,一个是字符,另一个是字符串指针。其功能是如果字符在字符串中。就返回1 (真);如果字符不在字符串中,就返回0(假)。在一个使用循环语句为这个函数提供舒服的完整程序中进行测试。
代码如下:

#include <stdio.h>  int iswithin(char p,char *q)  {      while(*q)      {          if(p == *q)              return 1;          else              q++;      }              return 0;  }  int main(int argc, char *argv[])  {      int m;      char p,*q;      p = *argv[1];      q = argv[2];      m = iswithin(p,q);      if(m == 1)          printf("\'%c\' is in the string!\n",p);      else          printf("\'%c\' is not in the string!\n",p);      return 0;  }  

执行结果如下:
fs@ubuntu:~/qiang/hanshu$ ./hanshu2 h hello
‘h’ is in the string!

fs@ubuntu:~/qiang/hanshu$ ./hanshu2 h world
‘h’ is not in the string!

fs@ubuntu:~/qiang/hanshu$
注意函数传参的方式。

题目二
以下函数的功能是用递归的方法计算 x 的 n 阶勒让德多相式的值。已有调用语句p(n,x):编写函数实现功能。
代码如下:

#include <stdio.h>  int p(int n,int x)  {      int m;      if(n == 0)          return 0;      else          if(n == 1)              return x;          else          {              m = ((2*n - 1)*x*p(n - 1,x) - (n - 1)*p(n - 2,x))/n;              return m;          }  }   int main(int argc, const char *argv[])  {      int x, n;      int q;      printf("Please input x and n:\n");      scanf("%d%d",&x,&n);      q = p(n,x);      printf("p = %d\n",q);      return 0;  }  

执行结果如下:
fs@ubuntu:~/qiang/hanshu$ ./hanshu1
Please input x and n:

2
1
p = 2

fs@ubuntu:~/qiang/hanshu$ ./hanshu1

Please input x and n:
2
5
p = 194
fs@ubuntu:~/qiang/hanshu$

原创粉丝点击