一、函数的本质与作用
1. 本质理解
函数是 一段可复用的代码块,相当于“工具”:
- 输入:通过参数接收外部数据
- 处理:在函数体内完成逻辑
- 输出:通过返回值给外部反馈
比如计算两数之和的函数,就像一个“加法工具”,给它两个数(输入),它返回和(输出)。
2. 核心作用
- 模块化编程:把大问题拆成小函数(如游戏开发中,移动、攻击、渲染拆成独立函数)
- 代码复用:避免重复写相同逻辑(比如多个地方要排序,写一个排序函数反复用)
- 降低复杂度:读代码时,看函数名就知道功能(如 calculateSalary() ,不用看内部细节)
二 、函数的基本语法(定义+声明+调用)
1.函数定义:完整实现逻辑
返回值类型 函数名(参数列表) {
// 函数体:实现具体功能
return 返回值; // 若返回值类型为void,可省略return
}
- 返回值类型:
- 如 int (返回整数)、 float (返回小数)、 void (无返回值)
- 决定了 return 后数据的类型,比如 int 函数必须 return 整数
- 函数名:
- 遵循标识符规则(字母、数字、下划线,首字母不能是数字)
- 建议见名知意(如 getMax() 表示“获取最大值”)
- 参数列表:
- 格式: 类型 变量名 ,多个参数用逗号分隔(如 int a, float b )
- 作用:接收外部传入的数据,相当于函数的“原材料”
- 函数体:
- 用 {} 包裹的代码,实现具体逻辑
- 可以定义局部变量(仅函数内有效)
示例:计算两数之和
int add(int a, int b) { // 定义函数add,接收两个int参数
int sum = a + b; // 函数体:计算和
return sum; // 返回int类型结果
}
2. 函数声明:告诉编译器“函数存在”
- 场景:如果函数定义在调用之后(或在其他文件),需要提前声明
- 语法: 返回值类型 函数名(参数列表); (注意末尾有分号)
示例:声明 + 调用 + 定义
// 声明函数(告诉编译器:有一个add函数,接收int a和int b,返回int)
int add(int a, int b);
int main() {
int result = add(3, 5); // 调用函数(必须先声明/定义)
return 0;
}
// 定义函数(实现具体逻辑)
int add(int a, int b) {
return a + b;
}
3. 函数调用:使用函数完成功能
- 语法: 函数名(实参列表);
- 实参:调用时传入的具体值,必须和形参类型、顺序、数量匹配
示例:多种调用方式
#include <stdio.h>
// 无返回值函数
void printHello() {
printf("Hello!\n");
}
// 有返回值函数
int multiply(int a, int b) {
return a * b;
}
int main() {
// 调用无返回值函数(直接执行)
printHello();
// 调用有返回值函数(结果存到变量)
int res = multiply(4, 5);
printf("4*5=%d\n", res);
// 直接用返回值参与运算
printf("(3*2)+(4*5)=%d\n", multiply(3,2) + multiply(4,5));
return 0;
}
三、函数的分类(按定义主体)
1. 库函数:C标准库提供的“现成工具”
- 特点:
- 由C标准库实现,直接可用
- 需包含对应头文件(如 <stdio.h> 、 <math.h> )
- 常用分类:
- 输入输出: printf() (输出)、 scanf() (输入)
- 字符串操作: strlen() (求长度)、 strcpy() (复制字符串)
- 数学运算: sqrt() (平方根)、 pow() (幂运算)
- 内存操作: malloc() (动态分配内存)、 free() (释放内存)
示例:用 sqrt() 计算平方根
#include <stdio.h>
#include <math.h> // 必须包含数学库头文件
int main() {
double num = 16.0;
// 调用库函数sqrt,计算平方根
double result = sqrt(num);
printf("sqrt(%.1f) = %.1f\n", num, result); // 输出:4.0
return 0;
}
2. 自定义函数:自己实现的“专属工具”
- 特点:根据需求自由定义逻辑,完全可控
- 场景:实现业务逻辑(如游戏中的“玩家攻击函数”、“数据排序函数”)
示例:判断是否为质数
#include <stdio.h>
#include <stdbool.h> // 启用bool类型(C99及以上支持)
// 自定义函数:判断n是否为质数
bool isPrime(int n) {
if (n <= 1) return false; // 1及以下不是质数
for (int i=2; i*i <=n; i++) {
if (n%i == 0) return false; // 能被整除,不是质数
}
return true; // 是质数
}
int main() {
int num = 17;
if (isPrime(num)) {
printf("%d是质数\n", num); // 输出:17是质数
} else {
printf("%d不是质数\n", num);
}
return num;
}
四、参数传递的2种方式(值传递 vs 地址传递)
1. 值传递:“复制一份”给函数
- 原理:调用时,实参的值复制给形参,函数内修改形参不影响实参
- 语法:直接传变量/常量(如 add(3,5) 、 printNum(num) )
示例:值传递的特点
#include <stdio.h>
void changeValue(int x) {
x = 100; // 修改的是形参x(实参num不受影响)
printf("函数内x=%d\n", x); // 输出:100
}
int main() {
int num = 10;
changeValue(num); // 传值调用
printf("函数外num=%d\n", num); // 输出:10(实参没被修改)
return 0;
}
2. 地址传递(指针传递):“传变量的地址”
- 原理:
- 实参是变量的地址(如 &num ),形参是指针(如 int *p )
- 函数内通过 *p 操作,直接修改实参的内存值
- 语法:形参用指针( int *p ),调用时传地址( changeValue(&num) )
示例:用地址传递交换两个数
#include <stdio.h>
// 地址传递:交换a和b的值
void swap(int *a, int *b) {
int temp = *a; // *a访问指针指向的变量(实参num1)
*a = *b; // 修改实参num1的值
*b = temp; // 修改实参num2的值
}
int main() {
int num1 = 5, num2 = 10;
printf("交换前:num1=%d, num2=%d\n", num1, num2); // 5,10
swap(&num1, &num2); // 传地址
printf("交换后:num1=%d, num2=%d\n", num1, num2); // 10,5
return 0;
}
3.两种传递方式的对比
传递方式 | 特点 | 适用场景 |
值传递 |
形参是实参的“副本” 修改不影响实参 |
只需要用实参的值, 不修改实参 |
址传递 |
形参是实参的“地址” 可直接修改实参 |
需要修改实参、 传大数据(优化) |
五、函数的存储与生命周期(内存视角)
1. 函数代码存在哪?
- 代码段(Code Segment):
- 函数的二进制指令存在这里,只读(防止被意外修改)
- 多个调用共享同一段代码(高效复用)
2. 函数的参数和局部变量存在哪?
- 栈(Stack):
- 调用函数时,参数、局部变量会被压入栈
- 函数返回时,栈帧(Stack Frame)销毁,局部变量失效
- 特点:自动分配和释放,速度快,但空间有限
栈帧示意图(简化)
| 高地址 |
| ... |
| 调用者的栈帧 |
| 函数A的栈帧 | ← 调用函数A时,参数和局部变量存在这里
| 低地址 |
3. 全局变量和静态变量存在哪?
- 全局/静态区(Global/Static Segment):
- 全局变量(定义在函数外)、 static 修饰的局部变量存在这里
- 程序运行期间一直存在,生命周期长
示例:静态变量的生命周期
#include <stdio.h>
void test() {
static int count = 0; // 静态变量,存在全局区
count++;
printf("count=%d\n", count);
}
int main() {
test(); // count=1(第一次调用,初始化)
test(); // count=2(第二次调用,复用之前的count)
test(); // count=3
return 0;
}
六、函数的高级特性
1. 递归函数:自己调用自己
- 核心条件:
- 终止条件(否则无限递归,栈溢出!)
- 每次递归缩小问题规模,逼近终止条件
- 经典示例:计算阶乘
#include <stdio.h>
// 递归计算n!:n! = n * (n-1)! ,终止条件n=0时返回1
int factorial(int n) {
if (n == 0) return 1; // 终止条件
return n * factorial(n-1); // 递归调用
}
int main() {
int n = 5;
printf("%d! = %d\n", n, factorial(n)); // 输出:120
return 0;
}
- 递归的风险:
- 栈溢出:递归太深(如计算 factorial(10000) ),栈帧过多导致栈溢出
- 调试难度:多层递归时,调用栈复杂
2. 函数指针:指向函数的指针
- 语法: 返回值类型 (*指针名)(参数列表);
- 作用:
- 用指针“存储函数地址”,实现动态选择函数(如排序时选不同比较函数)
示例:用函数指针实现“动态运算”
#include <stdio.h>
// 加法函数
int add(int a, int b) { return a + b; }
// 减法函数
int sub(int a, int b) { return a - b; }
int main() {
// 定义函数指针:指向“接收两个int,返回int”的函数
int (*op)(int, int);
// 让指针指向add函数
op = add;
printf("3+5=%d\n", op(3,5)); // 输出:8
// 让指针指向sub函数
op = sub;
printf("10-4=%d\n", op(10,4)); // 输出:6
return 0;
}
3. 回调函数:把函数当参数传递
- 原理:
- 把函数指针作为参数,传给其他函数
- 被调用的函数(回调函数)在特定时机执行
- 经典场景:排序( qsort 库函数需要传入比较函数)
示例:用 qsort 排序数组(自定义比较函数)
#include <stdio.h>
#include <stdlib.h> // 包含qsort
// 比较函数:用于qsort,升序排序int数组
int compareInt(const void *a, const void *b) {
// 转换为int指针,取值比较
return *(int*)a - *(int*)b;
}
int main() {
int arr[] = {5, 2, 8, 1, 9};
int len = sizeof(arr)/sizeof(arr[0]);
// qsort参数:数组、元素个数、元素大小、比较函数(回调)
qsort(arr, len, sizeof(int), compareInt);
for (int i=0; i<len; i++) {
printf("%d ", arr[i]); // 输出:1 2 5 8 9
}
return 0;
}
4. 内联函数(Inline Function):减少函数调用开销
- 语法:用 inline 修饰函数(建议配合 static 或在头文件定义)
- 原理:
- 编译时,把函数体“内联”到调用处,替代函数调用
- 优点:减少调用开销(跳转、栈操作等)
- 缺点:代码膨胀(每个调用处复制函数体)
- 适用场景:短小、频繁调用的函数(如获取配置参数)
示例:内联函数的使用
#include <stdio.h>
// 内联函数:返回两数中的较小值
inline static int min(int a, int b) {
return a < b ? a : b;
}
int main() {
// 编译时,这里会替换成:int res = (3 < 5 ? 3 : 5);
int res = min(3, 5);
printf("min is %d\n", res); // 输出:3
return 0;
}
七、函数的设计原则(写出高质量函数)
1. 单一职责原则
- 要求:一个函数只做一件事(如 calculateSum() 只计算和, printResult() 只负责输出)
- 反例:一个函数又计算又打印又修改全局变量,逻辑混乱
2. 高内聚、低耦合
- 高内聚:函数内部逻辑紧密相关,不依赖外部无关变量
- 低耦合:函数之间通过参数、返回值交互,减少全局变量依赖
3. 避免副作用
- 副作用:函数除了返回值,还修改全局变量、外部状态(难预测、难调试)
- 建议:优先写“纯函数”(输入决定输出,不影响外部)
示例:纯函数 vs 有副作用的函数
// 纯函数(推荐):输入a和b,输出和,不影响外部
int add(int a, int b) {
return a + b;
}
// 有副作用(不推荐):修改全局变量,逻辑隐蔽
int globalNum = 10;
int badAdd(int a) {
globalNum += a; // 修改全局变量
return globalNum;
}
4. 合理控制参数和返回值
- 参数:
- 数量不宜过多(最多5-7个,否则难维护)
- 区分输入参数(只读)和输出参数(通过指针修改)
- 返回值:
- 清晰表达函数的结果(如 bool isSuccess() 返回执行状态)
- 复杂结果可用指针作为输出参数(如 void getStats(int *min, int *max) )
八、常见问题与调试技巧
1. 常见错误
- 未声明函数:调用函数前没声明/定义,编译器报错“implicit declaration”
- 参数不匹配:实参与形参类型/数量不一致(如 add(3, 5.0) ,形参是 int )
- 返回值遗漏:有返回值的函数忘记 return (或 return 类型不匹配)