概述

函数是可以完成某些功能的一段代码块(函数,function,功能、作用的意思),它是C语言结构化程序设计的实现方式。通过函数,可以将程序分解成各个子模块,通过编写这些模块,最后组装出我们的程序。使用函数的方式,一方面降低了编码的难度,因为在模块化之后,我们只需要面对一个模块内部的逻辑关系,而不需要再从整体去考虑。另一方面,模块化还降低了调试程序的难度,因为都分成了模块,哪个模块有问题,只需要修复对应的模块就可以了。

函数定义

定义函数的方法:

返回值类型 函数名称 (参数列表)
{
    语句块;
}

示例:

int add(int a, int b)
{
    int sum = 0;
    sum = a + b;
    return sum;
} 

调用函数:

int main()
{
    int a = 1;
    int b = 2;
    int sum = 0;
    sum = add(a, b);
    printf("%d\n", sum);
} 

函数调用过程与函数内存图

我们把调用其他函数的函数称为主调函数(主函数),把被调用的函数称为被调函数(子函数),那么函数的调用流程如下:

  1. 按顺序执行主调函数中的语句,直到调用被调函数前的语句为止;
  2. 跳转到被调函数中执行,一直到被调函数正常结束,或是调用return返回;
  3. 返回主调函数中,从被调函数后的第一个语句开始继续开始执行。

从上面的过程可以看出,子函数通过参数列表得到输入,通过返回值得到输出。对于主调函数来说,只关心给出什么输入可以得到什么输出,对函数内部具体如何实现,并不关心。

Alt text

每个函数被调用时都有自己的栈空间,这块栈空间是私有的,只归本函数使用,用于存储参数和局部变量。当函数调用结束(代码结束或是遇到return语句)时,这块栈空间会被编译器自动回收,存储在上面的数据将不能再被使用。当再次调用到该函数时,该函数又会获取到一块栈空间,只是这块栈空间和上次被调用时的栈空间已经没有什么关系了。
Alt text

函数的形参实参与返回值

定义函数时,函数参数列表中的内容称为形式参数(形参),在主函数中进行函数调用时,传入的参数称为实际参数(实参)。实参是我们要操作的对象,而形参用于接收实参的值,它们在数量上和顺序上应该严格一致。
Alt text

在函数未被调用时,不会给形参分配内存单元,只有当函数被调用时,形参才会被分配内存单元(一般是在栈上分配)。形参变量被创建之后,会用传入的实参值对其进行初始化。调用结束后,形参的内存单元会被释放。

从以上过程可以看到,形参和实参在内存中是完全不相关的两块内存,它们在存储位置上没有任何关系。

函数可以无返回值或者仅有一个返回值,C语言无法实现一次返回多个值。当函数有返回值时,需要在定义函数时指定返回值的类型,并在函数中用return语句返回对应的类型值,两者类型不匹配时,以函数类型为准。

当函数没有返回值或没有参数时,应该严格用void类型来限定,否则,函数将仍然可以接收参数并默认返回一个int类型的整数,如下:

void func(void)
{
    printf("hello, world\n");
}

函数的声明

当函数的定义出现在调用之前时,函数可以正常调用。而当函数的定义出现在调用之后时,函数的调用就会出现无法识别的问题(GCC编译器需要添加 -Wall选项才可以看到错误提示信息)。如果函数的定义出现在调用之后,那么可以通过对函数添加前置声明的方式解决问题,如下:

int add(int a, int b); //函数声明,告诉编译器,add函数已经在他处有定义

int main()
{
    int a = add(1, 2); //函数调用
}

int add(int a, int b)  //函数定义
{
    return a + b;
}

在声明函数时,可以对形参的参数名进行省略,因为编译器在检查函数声明时不关注参数名,只关注参数类型。所以,类似下面这样的声明也是可以的:

int add(int, int); //省略参数名称的声明方式

指针与函数

指针类型的函数参数

指针是一个变量的地址,形参变量是指针时,形参同样是实参的副本,只不过这两者都指向了同一个地址,因此可以在子函数中修改这个地址的内容。当函数调用结束后,形参指针变量本身所占用的内存也会被释放。

void increase(int *p)
{
    *p = *p + 1;
    return;
}
int main()
{
    int a = 1;
    increase(&a);
    printf("%d\n", a); // a = 2;
} 

Alt text

指针类型的返回值

可以通过return语句返回一个指针变量,此时,main函数可以用相同类型的指针变量来接收它的值。对于指针类型的返回值,有一点要非常注意,不可以返回指向栈内存的指针,因为栈空间在函数返回后就已经被回收了,类似下面的写法是错误的:

int* func(void)
{
    int a = 1;
    return &a; // a存储在func的栈上,func结束之后栈就被清空回收了
} 

数组与函数

把一维数组作为函数参数时,编译器总是将它解析成一个指向数组首地址的指针。因此,形参写成指针形式或是数组形式,对编译器来说没有区别,都表示这个参数是指针。为了防止在子函数中发生数组越界,一般数组作为参数时,都需要再定义一个参数用于传入数组的长度,在子函数根据这个长度去访问数组,就可以保证不发生越界,以下三种写法是完全一样的:

void sort(int* a, int n);
void sort(int a[], int n);
Void sort(int a[100], int n);


由于C语言中函数不能一次返回多个值,所以函数只能返回数组中的某一个元素,而不能返回整个数组。

二维数组作为数组参数时,需要传入的是指向二维数组第一个元素的数组指针,如下:

void func(int (*p)[3], int n)
{
    // do something
}

int main(void)
{
    int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
    func(a, 2);
    return 0;
}

main函数参数与返回值

main函数可以带参数,也可以不带参数。C语言规定main函数的参数只能有两个,第一个是整数,第二个是指向字符串的指针数组,一般写成argc和argv。main函数默认返回整型,不准将main函数定义成void类型。以下是main函数的标准写法:

int main(int argc, char* argv[])
{
    // do something
}

main函数的实参由操作系统给出,在linux中可以用命令行的形式传给可执行程序的main函数。第一个整型的argc表示的是从命令行传入参数的个数(执行程序本身也算一个参数),第二个参数表示的是从命令行传入的字符串个数(执行程序本身是第一个参数),如下:

执行命令:./file1 China Beijing

argc与argv的值:

argc ==> 3

argv[0] ==> "./file1"

argv[1] ==> "China"

argv[2] ==> "Beijing"

递归函数

函数可以调用自己调用自己,函数自己调用自己称为函数的递归。函数递归尤其要注意的一点是,递归需要有一个终止条件,否则函数一直递归下去,栈空间终会被消耗完,导致程序的段错误。

使用递归可以写出非常简洁高效的代码,很多巧妙的算法都是用递归实现的。使用递归的重点是要理解递归的思路,尤其是在何时该终止递归,在使用中,需要多加练习,多加分析。通过足量的练习,锻炼自己对于递归的敏感度。下面展示一些递归的巧妙用法。

求正整数n的阶乘:

int fact(int n)
{
    if (n == 1)
    {
        return 1;
    }
    else
    {
        return n * fact(n - 1);
    }
} 

不使用任何变量求字符串长度:

int my_strlen(char *str)
{
    if (*str == '\0')
    {
        return 0;
    }
    else
    {
        return (1 + my_strlen(++str));
    }
} 

输出一个数的2进制值:

#define SCALE 2
void base_conversion(int num)
{
    if(num < SCALE)
    {
        printf("%d", num);
        return;
    }
    else
    {
        base_conversion(num / SCALE);
        printf("%d", num % SCALE);
    }
}

函数指针

每个函数在经过编译后最终都会形成一段二进制代码,而这些代码在程序运行时也会载入内存。所以每个函数都会在内存中有一个存储的地址。如果知道了这个地址,那是不是也可以调用到函数呢?

答案是可以的,与变量一样,函数也有地址。函数的地址就是函数名,这点与数组类似。我们可以将这个地址保存到一个指针里面,然后通过指针调用到这个函数。保存函数地址的类型称为函数指针,定义如下:

函数类型 (*指针名称)(形参列表);

比如,有某个函数定义如下:

int add(int a, int b)
{
    return a + b;
}

则可以定义一个函数指针,让它指向这个函数:

int (*padd)(int a, int b); //定义函数指针
padd = add;                //给函数指针赋值
int a =padd(3, 2);         //通过函数指针调用函数

定义函数指针时,同样可以不指定形参的名称,类似下面的写法也是可以的:

int (*padd)(int, int); //定义函数指针,忽略形参名称

既然通过函数名就可以调用函数,那为什么还要用函数指针呢?请查阅标准库函数qsort然后自行体会。(linux命令行输入man qsort即可看到qsort的原型和使用方法)

回调函数

如果某个函数的参数中带有函数指针,那么我们称这个函数为回调函数。

int add(int a, int b)
{
    return a + b;
}

int callback(int num1, int num2, int (*pfun)(int, int)) //回调函数
{
    return pfun(num1, num2);
} 

  • 无标签