C Primer Plus学习笔记(八)- 函数
函数简介
函数(function)是完成特定任务的独立程序代码单元
使用函数可以省去编写重复代码的苦差,函数能让程序更加模块化,提高程序代码的可读性,更方便后期修改、完善
#include <stdio.h> void test(void); // 函数原型 int main(void) { printf("Before run function\n"); test(); // 调用函数 printf("After run function\n"); return 0; } void test(void) // 定义函数 { printf("Running function\n"); }
运行结果
函数原型(function prototype)告诉编译器函数 test() 的类型,函数原型指明了函数的返回值类型和函数接受的参数类型,这些信息称为该函数的签名(signature)
void test(void); // 函数原型
圆括号表明 starbar 是一个函数名;第 1 个 void 是函数类型,void 类型表明函数没有返回值;第 2 个 void(在圆括号中)表明该函数不带参数;分号表明这是在声明函数,不是定义函数
函数原型要在使用前声明
函数调用(function call)表明在此处执行函数
test(); // 调用函数
当执行到这条语句时,会找到该函数的定义并执行其中的内容,执行完 test() 中的代码后,计算机返回主调函数(calling function)继续执行下一行
函数定义(function definition)明确地指定了函数要做什么
void test(void) // 定义函数 { printf("Running function\n"); }
函数头中 test() 后面没有分号,告诉编译器这是定义 test(),而不是调用函数或声明函数
识别不了 void 的编译器,要把没有返回值的函数声明为 int 类型
函数中的变量是局部变量,只在该函数中有效,即该变量只属于该函数。如果在其他函数中定义同名的函数,不会引起名称冲突,他们是同名的不同变量
函数参数
声明带形式参数函数的原型
在使用函数之前,要用 ANSI C 形式声明函数原型
void test(char ch, int num);
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型
也可以省略变量名
void test(char, int);
在原型中使用变量名并没有实际创建变量,char 仅代表了一个 char 类型的变量
ANSI C 也接受过去的声明函数形式,即圆括号内没有参数列表
void test();
定义带形式参数的函数
函数定义从下面的 ANSI C 风格的函数头开始
void test(char ch, int num)
该行告诉编译器 test() 使用两个参数 ch 和 num,ch 是 char 类型,num 是 int 类型,这两个变量被称为形式参数(formal argument,标准推荐使用 formal parameter),简称形参
和定义在函数中的变量一样,形式参数也是局部变量,属该函数私有
每次调用函数,就会给这些变量赋值
ANSI C 要求在每个变量前都声明其类型,即不能像声明普通变量那样使用同一类型的变量列表
void test(int x, y, z) // 无效的函数头 void test(int x, int y, int z) // 有效的函数头
ANSI C 也接受 ANSI C 之前的形式,但是将其视为废弃不用的形式
void test(ch, num) char ch; int num;
圆括号中只有参数名列表,而参数的类型在后面声明
普通的局部变量在左花括号之后声明,而上面的变量在函数左花括号之前声明
如果变量是同一类型,这种形式可以用逗号分隔变量名列表
void test(x, y, z) int x, y, z;
调用带实际参数的函数
实际参数(actual argument,简称实参),即实际传给函数的值
形式参数是被调函数(called function)中的变量,实际参数是主调函数(calling function)赋给被调函数的具体值
实际参数可以是常量、变量或者是更复杂的表达式,无论实际参数是何种形式都要被求值,然后该值被拷贝给被调函数相应的形式参数
实际参数是具体的值,该值要被赋给作为形式参数的变量
因为被调函数使用的值是从主调函数中拷贝而来,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据
注意:实际参数和形式参数
实际参数是出现在函数调用圆括号中的表达式
形式参数是函数定义的函数头中声明的变量
使用 return 从函数中返回值
关键字 return 后面的表达式的值就是函数的返回值
#include <stdio.h> int test(int); int main(void) { int a = 2; int b; b = test(a); printf("%d\n", b); return 0; } int test(int n) { int m = 5; if (m > n) return m; else printf("n is bigger than m\n"); }
运行结果
变量 m 属于 test() 函数私有,但是 return 语句把 m 的值传回了主调函数
return 返回值不一定是变量的值,也可以是任意表达式的值
如果函数返回值的类型与函数声明的类型不匹配的话,实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值
一个 int 类型的函数,要返回一个 double 类型的值,就把这个 double 类型的值赋给 int 类型的变量,然后返回 int 类型变量的值
使用 return 语句的另一个作用是,终止函数并把控制返回给主调函数的下一条语句
在函数中可以使用多个 return,用于条件判断
还可以这样使用 return 语句
return;
这条语句会导致终止函数,并把控制返回给主调函数。因为 return 后面没有任何表达式,所以没有返回值,只有在 void 函数中才会用到这种形式
函数类型
声明函数的时候必须声明函数的类型
带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为 void 类型
类型声明是函数定义的一部分
函数类型指的是返回值的类型,不是函数参数的类型
ANSI C 函数原型
主调函数把它的参数储存在被称为栈(stack)的临时存储区,被调函数从栈中读取这些参数
主调函数根据函数调用中的实际参数决定传递的类型,而被调函数根据它的形式参数读取值
当 float 类型被作为参数传递时会被升级为 double 类型
如果参数的类型不匹配,编译器会把实际参数的类型转换为形式参数的类型
无参数和未指定参数
一个支持 ANSI C 的编译器会假定用户没有用函数原型来声明函数,它将不会检查参数
为了表明函数确实没有参数,应该在圆括号中使用 void 关键字
void test(void);
支持 ANSI C 的编译器解释为 test() 不接受任何参数,然后在调用该函数时,编译器会检查以确保没有使用参数
一些函数接受(如,printf() 和 scanf())许多参数
例如对于 printf(),第一个参数是字符串,但是其余参数的类型和数量都不固定。对于这种情况,ANSI C 允许使用部分原型
例如,对于 printf() 可以使用下面的原型:
int printf(const char *, ...);
这种原型表明,第一个参数是一个字符串,可能还有其他未指定的参数
C 库通过 stdarg.h 头文件提供了一个定义这类(形参数量不固定的)函数的标准方法
递归
C 允许函数调用它自己,这种调用过程称为递归(recursion)
演示递归
#include <stdio.h> void up_and_down(int); int main(void) { up_and_down(1); return 0; } void up_and_down(int n) { printf("Level %d: n location %p\n", n, &n); // #1 if (n < 4) up_and_down(n + 1); printf("LEVEL %d: n location %p\n", n, &n); // #1 }
运行结果
首先,main() 调用了带参数 1 的 up_and_down() 函数,执行结果是 up_and_down() 中的形式参数 n 的值是 1,所以打印语句 #1 打印 Level 1。然后,由于 n 小于 4,up_and_down()(第 1 级)调用实际参数为 n+1(或 2)的 up_and_down()(第 2 级)。于是第 2 级调用中的 n 的值是 2,打印语句 #1 打印 Level 2。与此类似,下面两次调用打印的分别是 Level 3 和 Level 4
当执行到第 4 级时,n 的值是 4,所以 if 测试条件为假。up_and_down() 函数不再调用自己。第 4 级调用接着执行打印语句 #2,即打印 LEVEL 4,因为 n 的值是 4。此时,第 4 级调用结束,控制被传回它的主调函数(即第 3 级调用)。在第 3 级调用中,执行的最后一条语句是调用 if 语句中的第 4 级调用。被调函数(第 4 级调用)把控制返回在这个位置,因此,第 3 级调用继续执行后面的代码,打印语句 #2 打印 LEVEL3。然后第 3 级调用结束,控制被传回第 2 级调用,接着打印 LEVEL 2,以此类推
每级递归的变量 n 都属于本级递归私有
Level 1 和 LEVEL 1 的地址相同,Level 2 和 LEVEL 2 的地址相同,Level 3 和 LEVEL 3 的地址相同,Level 4 和 LEVEL 4 的地址相同
递归的基本原理
第 1,每级函数调用都有自己的变量
第 2,每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归
第 3,递归函数中位于递归调用之前的语句,均按被调函数的顺序执行
第 4,递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行
第 5,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码
最后,递归函数必须包含能让递归调用停止的语句
尾递归
最简单的递归形式是把递归调用置于函数的末尾,即正好在 return 语句之前
这种形式的递归被称为尾递归(tail recursion),因为递归调用在函数的末尾
尾递归是最简单的递归形式,因为它相当于循环
递归的优缺点
优点是递归为某些编程问题提供了最简单的解决方案
缺点是一些递归算法会快速消耗计算机的内存资源,递归也不方便阅读和维护
编译多源代码文件的程序
UNIX
假设在 UNIX 系统中安装了 UNIX C 编译器 cc(最初的 cc 已经停用,但是许多 UNIX 系统都给 cc 命令起了一个别名用作其他编译器命令,典型的是 gcc 或 clang)
假设 file1.c 和 file2.c 是两个内含 C 函数的文件,下面的命令将编译两个文件并生成一个名为 a.out 的可执行文件
cc file1.c file2.c
另外,还生成两个名为 file1.o 和 file2.o 的目标文件
如果后来改动了 file1.c,而 file2.c 不变,可以使用一下命令编译第 1 个文件,并于第 2 个文件的目标代码合并:
cc file1.c file2.o
UNIX 系统的 make 命令可以自动管理多文件程序
Linux
假定在 Linux 系统上安装了 GNU C 编译器 GCC
假设 file1.c 和 file2.c 是两个内含 C 函数的文件,下面的命令将编译两个文件并生成名为 a.out 的可执行文件:
gcc file1.c file2.c
另外,还生成两个名为 file1.o 和 file2.o 的目标文件
如果后来改动了 file1.c,而 file2.c 不变,可以使用以下命令编译第 1 个文件,并于第 2 个文件的目标代码合并:
gcc file1.c file2.o
使用头文件
在 UNIX 和 DOS 环境中,#include "hotels.h" 指令中的双引号表明被包含的文件位于当前目录中(通常是包含源代码的目录)
如果使用 IDE,需要知道如何把头文件合并成一个项目