目录
1. 函数简介
- 函数(function) 是完成特定任务的独立程序代码单元
- 使用函数的原因
- 使用函数可以省去编写重复代码的时间
- 函数让程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善
1.1 创建并使用简单函数
#include <stdio.h>
// 函数原型
void ABC();
int main() {
ABC(); // 函数调用
}
// 函数定义
void ABC() {
printf("ABC\n");
}
// 等价于
#include <stdio.h>
// 函数原型
void ABC(void);
int main(void) {
ABC(); // 函数调用
}
// 函数定义
void ABC(void) {
printf("ABC\n");
}
1.2 分析程序
- 函数原型(function prototype):告诉编译器函数的类型
- 函数调用(function call):在此处使用函数
- 函数定义(function definition):明确指出函数要执行的操作
- 一般而言,函数原型指明了函数的返回值类型和函数接收的参数类型;这些信息被称为该函数的签名(signature)
- 函数中定义的变量是局部变量(local variable),该变量只属于该函数;可以在程序中的其他地方使用同名变量,不会引起名称冲突,是同名的不同变量
1.3 函数参数
#include <stdio.h>
void ABC(int);
int main() {
ABC(1);
}
void ABC(int i) {
printf("%d\n", i);
}
1.4 定义带形式参数的函数
- 在函数定义时声明的参数被称为形式参数(formal argument)
- 形式参数也时局部变量,该函数私有
- 用圆括号括起来的参数体被称为参数列表
void a(int i, char c)
// 等效于
void a(i, c)
int i;
char c;
1.5 调用带实际参数的函数
- 在函数表用中,实际参数(actual argument) 提供了形式参数的值
- 形式参数是被调函数(called function) 中的变量
- 实际参数是主调函数(calling function) 赋给被调函数的具体值
- 实际参数是具体的值,该值要被赋给作为形式参数的变量
- 因为被调函数使用的值是从主调函数中拷贝而来,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据
1.6 黑盒视角
- 黑盒方法的核心:函数私有的局部变量
- 黑盒中发生了什么对主调函数是不可见的
1.7 使用 return 从函数中返回值
- 被这几用于测试函数的程序有时被称为驱动程序(driver),该驱动程序调用一个函数
#include <stdio.h>
float ABC(int, int);
int main() {
printf("%f\n", ABC('A', 'a'));
// 65.000000
printf("%f\n", ABC(1.11, 1.23));
// 1.000000
}
float ABC(int i, int j) {
return (i < j) ? i : j;
printf("ABC");
}
1.8 函数类型
- 声明函数时必须声明函数的类型
- 带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为 void 类型
1.9 所有的C函数皆平等
- 每个函数都可以调用其他函数,或被其他函数调用
- main() 函数特殊:
- 当 main() 函数与程序中的其他函数放在一起时,最开始执行的是 main() 函数中的第1条语句
- main() 函数也可以被自己或其他函数递归调用
2. ANSI C 函数原型
与原书不同,自写
- 函数调用的参数列表中的参数数量,只与函数原型有关,参数可以多,但是不可以少
- 函数定义与函数原型没有一点关系,参数类型可以不同,参数数量可以不同,只有返回值类型必须相同
- 函数调用时,只会根据函数原型的参数类型和数量,向函数定义传参
- 先根据函数原型的参数类型和数量,获取函数调用时的参数
- 调用时按照从左往右的顺序获取参数
- 并将参数的数据类型按照函数原型中的数据类型一一转换
- 之后将转换成的参数传入函数定义中
- 函数定义处理传入的参数
- 传入参数数量 > 所需参数数量:不影响函数使用
- 传入参数数量 = 所需参数数量:不影响函数使用
- 传入参数数量 < 所需参数数量:函数会使用栈(stack) 中的其他值将缺失数据补全
- 先根据函数原型的参数类型和数量,获取函数调用时的参数
3. 递归
- C允许函数调用它自己,这种调用过程称为递归(recursion)
3.1 尾递归
- 最简单的递归形式时把递归调用置于函数的末尾,这种形式的递归被称为尾递归(tail recursion)
#include <stdio.h>
int value = 1;
int ABC(int);
int main() {
int i = 0;
printf("i = %d, value = %d\n", i, ABC(i));
}
int ABC(int i) {
if (i >= 1)
{
value *= i;
return ABC(i - 1);
}
else
{
return value;
}
}
// 优化
#include <stdio.h>
int ABC(int);
int main() {
int i = 3;
printf("i = %d, value = %d\n", i, ABC(i));
}
int ABC(int i) {
return (i >= 1) ? i * ABC(i - 1) : 1;
}
3.2 递归的优缺点
- 优点:递归为某些编程问题提供了最简单的解决方案
- 缺点:
- 一些递归算法会快速消耗计算机的内存资源
- 递归不方便阅读和维护
4. 编译多源代码文件的程序
- 使用多个函数最简单的方法是把它们都放在同一个文件中,然后像编译只有一个函数的文件那样编译该文件即可
4.1 UNIX
cc file1.c file2.c
- 此命令将编译两个文件并生成一个名为 a.out 的可执行文件,两个分别名为 file1.o 和 file2.o 的目标文件
cc file1.c file2.o
- 如果后来改动了 file1.c,而 file2.c 不变,可以使用此命令编译第1个文件,并与第2个文件的目标代码合并
4.2 Linux
gcc file1.c file2.c
- 此命令将编译两个文件并生成一个名为 a.out 的可执行文件,两个分别名为 file1.o 和 file2.o 的目标文件
gcc file1.c file2.o
- 如果后来改动了 file1.c,而 file2.c 不变,可以使用此命令编译第1个文件,并与第2个文件的目标代码合并
4.3 DOS 命令行编译器
- 绝大多数 DOS 命令行编译器的工作原理和 UNIX 的 cc 命令类似
- 其中的一个区别为:对象文件的扩展名是 .obj,而不是 .o
- 一些编译器生成的不是目标代码文件,而是汇编语言或其他特殊代码的中间文件
4.4 Windows 和苹果的 IDE 编译器
-
Windows 和 Macintosh 系统使用的集成开发环境中的编译器是面向项目的
-
项目(project) 描述的是特定程序使用的资源
-
资源包括源代码文件
4.5 使用头文件
- Test.c
#include <stdio.h>
#include "Head.h"
int main() {
int i = 3;
printf("i = %d, value = %d\n", i, ABC(i));
}
- Achieve.c
#include <stdio.h>
#include "Head.h"
int ABC(int i) {
return (i >= 1) ? i * ABC(i - 1) : 1;
}
- Head.h
int ABC(int i);
5. 查找地址:&运算符
- 指针(pointer) 是C语言最重要的概念之一,用于存储变量的地址
- 一元 & 运算符给出变量的存储地址
int i = 1;
printf("i = %d, &i = %#x = %p\n", i, &i);
6. 更改主调函数中的变量
- 交换两个变量的数值
int a = 1;
int b = 2;
int i = 0;
printf("a = %d, b = %d\n", a, b);
i = b;
b = a;
a = i;
printf("a = %d, b = %d\n", a, b);
- 如像通过函数交换主调函数中两个变量的值,需要使用指针
7. 指针简介
- 指针(pointer) 是一个值为内存地址的变量(或数据对象)
7.1 间接运算符:*
- 间接运算符(indirection operator) 可以用来获取存储在地址中的值,该运算符有时也称为 解引运算符(dereferencing operator)
i = &a;
j = *i;
// 等效于
j = a;
7.2 声明指针
-
声明指针变量时必须指定指针所指向变量的类型
- 因为不用的变量类型占用不同的内存空间,一些指针操作要求知道操作对象的大小
- 另外,程序必须知道存储在指定地址上的数据类型
int * a; // a 是指向 int 类型变量的指针,*a 是 int 类型 char * b; float * c, * d;
-
指针名和 * 之间的空格可有可无
- 程序员在声明时使用空格
- 在解引用变量时省略空格
7.3 使用指针在函数间通信
#include <stdio.h>
void change(int *, int *);
int main() {
int a = 1;
int b = 2;
printf("a = %d, b = %d\n", a, b);
change(&a, &b);
printf("a = %d, b = %d\n", a, b);
}
void change(int * a, int * b) {
int i;
i = *b;
*b = *a;
*a = i;
}