欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第7章 函数

程序员文章站 2022-07-11 11:58:10
...

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;  
}  

程序执行结果如下:
aaa@qq.com:~/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章 函数

7.4回调函数

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

第7章 函数

图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;  
}  

执行结果如下:
第7章 函数
回调函数应用实例二:

#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);  
}  

执行结果如下:
第7章 函数

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;  
}  

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

aaa@qq.com:~/qiang/hanshu$ ./hanshu2 h world
‘h’ is not in the string!

aaa@qq.com:~/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;  
}  

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

2
1
p = 2

aaa@qq.com:~/qiang/hanshu$ ./hanshu1

Please input x and n:
2
5
p = 194
aaa@qq.com:~/qiang/hanshu$

相关标签: 函数