函数是可以完成某些功能的一段代码块(函数,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); } |
我们把调用其他函数的函数称为主调函数(主函数),把被调用的函数称为被调函数(子函数),那么函数的调用流程如下:
从上面的过程可以看出,子函数通过参数列表得到输入,通过返回值得到输出。对于主调函数来说,只关心给出什么输入可以得到什么输出,对函数内部具体如何实现,并不关心。
每个函数被调用时都有自己的栈空间,这块栈空间是私有的,只归本函数使用,用于存储参数和局部变量。当函数调用结束(代码结束或是遇到return语句)时,这块栈空间会被编译器自动回收,存储在上面的数据将不能再被使用。当再次调用到该函数时,该函数又会获取到一块栈空间,只是这块栈空间和上次被调用时的栈空间已经没有什么关系了。
定义函数时,函数参数列表中的内容称为形式参数(形参),在主函数中进行函数调用时,传入的参数称为实际参数(实参)。实参是我们要操作的对象,而形参用于接收实参的值,它们在数量上和顺序上应该严格一致。
在函数未被调用时,不会给形参分配内存单元,只有当函数被调用时,形参才会被分配内存单元(一般是在栈上分配)。形参变量被创建之后,会用传入的实参值对其进行初始化。调用结束后,形参的内存单元会被释放。
从以上过程可以看到,形参和实参在内存中是完全不相关的两块内存,它们在存储位置上没有任何关系。
函数可以无返回值或者仅有一个返回值,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; } |
可以通过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函数可以带参数,也可以不带参数。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); } |