
04_C语言-函数篇
本文最后更新于 2025-03-14,学习久了要注意休息哟
第一章 函数的概念
1.1 函数的定义
📌 核心理解
函数是 具有特定功能的独立代码模块,包含四个基本要素:
要素 | 说明 | 示例 |
---|---|---|
函数名 | 符合标识符规则,见名知意 | calculateSum |
参数列表 | 输入接口,定义接收的数据类型 | (int a, int b) |
返回类型 | 输出结果的数据类型(无返回值用void) | double / void |
函数体 | 实现具体功能的代码块 | { return a+b; } |
// 典型函数定义示例
int isEven(int num) { // 判断奇偶的函数
return (num % 2 == 0); // 返回布尔值结果
}
1.2 函数的作用
▌四大核心价值
① 模块化设计
✅ 将复杂程序分解为多个简单函数
✅ 例:学生管理系统 = 输入函数 + 计算函数 + 输出函数
② 代码复用
✅ 避免重复代码
✅ 例:多次调用sort()
函数实现排序
③ 逻辑封装
✅隐藏实现细节,暴露必要接口
✅ 例:直接使用printf()
无需了解内部实现
④ 协作开发
✅多人并行开发不同函数
1.3 函数的语法
📜 标准语法结构
// 函数声明(告诉编译器函数存在)
返回类型 函数名(参数类型列表);
// 函数定义(具体实现)
返回类型 函数名(形参列表) {
局部变量声明;
执行语句;
return 返回值; // void函数可省略
}
🔑 关键语法点
参数传递
// 值传递示例:原始数据不受影响
void add(int x) { x = x + 1; }
// 地址传递示例:原始数据会被修改
void realAdd(int *x) { *x = *x + 1; }
返回值规则
- 基本类型直接返回
- 数组通过指针返回
- 无返回值时使用
void
函数调用
// 带返回值调用
int result = func(3, 5);
// 无返回值调用
func();
⚠️ 常见错误
// 错误1:缺少返回值
int sum(int a, int b) {
int s = a + b; // 缺少return语句
}
// 错误2:参数类型不匹配
double avg(int a, int b) {...}
avg(3.5, 4.2); // 实际传入double类型,但形参是int
📌 本章要点总结
- 函数 = 功能模块 + 输入接口 + 输出结果
- 先声明后使用的基本原则
- 值传递与地址传递的本质区别
- return语句的正确使用方法
第二章 函数的基础
2.1 函数调用
📌 完整调用流程演示
示例代码:
#include <stdio.h>
// 函数声明
int add(int a, int b);
int main() {
int x = 3, y = 5;
// 函数调用
int result = add(x, y);
printf("Sum: %d\n", result);
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
🎨 调用过程图解
[主函数栈帧] [add函数栈帧]
| main()执行环境 | | a=3, b=5 |
| x=3 | ◀──┐ |临时变量区 |
| y=5 | │ |返回值=8 |
| result未初始化 | └──参数传递
↓
控制权转移 → 执行add函数
↓
[主函数恢复执行]
result = 8
关键步骤说明:
- 参数压栈:将x=3和y=5的值复制到add函数的参数a,b
- 控制转移:跳转到add函数代码段
- 栈帧创建:为add函数分配局部变量存储空间
- 执行运算:计算a+b得到8
- 返回值:将结果存入指定寄存器
- 栈帧销毁:释放add函数的局部变量空间
- 结果接收:主函数获取返回值存入result
2.2 参数传递
🔄 值传递 vs 地址传递
示例对比:
// 值传递(复制数据)
void modifyCopy(int num) {
num = 99; // 只修改副本
}
// 地址传递(操作原数据)
void modifyReal(int *num) {
*num = 99; // 修改原变量
}
int main() {
int a = 10;
modifyCopy(a); // 调用后a仍为10
modifyReal(&a); // 调用后a变为99
return 0;
}
内存变化演示:
调用modifyCopy(a)时:
主函数内存 函数参数内存
a: 0x1000 num: 0x2000
| 10 | | 10 | → 修改为99
调用modifyReal(&a)时:
主函数内存 指针操作
a: 0x1000 num存储0x1000
| 99 | ←───────通过地址修改
📦 数组参数传递规范
正确写法:
// 必须显式传递数组长度
void printArray(int arr[], int size) {
for(int i=0; i<size; i++) { // 安全访问
printf("%d ", arr[i]);
}
}
int main() {
int data[] = {1,2,3,4,5};
printArray(data, 5); // 同时传递数组和长度
return 0;
}
错误示范分析:
void printArrayWrong(int arr[5]) { // 错误!实际仍视为指针
int size = sizeof(arr)/sizeof(int); // 得到指针大小而非数组长度
// 导致循环次数错误
}
2.3 作用域
🌐 三级作用域演示
示例代码:
int global = 100; // 全局作用域
void demo() {
int func_var = 50; // 函数作用域
if(1) {
int block_var = 20; // 块作用域
printf("Block内: %d %d %d\n",
global, func_var, block_vasr); // 全部可见
}
// printf("%d", block_var); // 错误!block_var不可见
}
int main() {
// printf("%d", func_var); // 错误!func_var不可见
demo();
return 0;
}
可视化作用域范围:
程序内存布局:
┌──────────────┐
│ 全局变量区 │ ← global
├──────────────┤
│ 栈区 │
│ ├─demo() │ ← func_var
│ │ └─if块 │ ← block_var
│ └─main() │
└──────────────┘
2.4 生命周期
⏳ 三类变量对比
完整示例:
#include <stdio.h>
#include <stdlib.h>
int global_var; // 静态存储期(自动初始化为0)
void test() {
int auto_var = 0; // 自动存储期
static int static_var = 0; // 静态存储期
auto_var++;
static_var++;
int *heap_var = malloc(sizeof(int)); // 动态存储期
*heap_var = 100;
printf("auto:%d static:%d heap:%d\n",
auto_var, static_var, *heap_var);
free(heap_var); // 必须手动释放
}
int main() {
test(); // auto:1 static:1 heap:100
test(); // auto:1 static:2 heap:100
return 0;
}
生命周期示意图:
程序执行过程:
┌─────────────┐ test()第一次调用
│ global_var │────┐
├─────────────┤ │ 创建auto_var
│ 堆内存分配 │◀─┐ │ 创建static_var
└─────────────┘ │ │
│ │
test()结束后: │ │
auto_var销毁 │ │
heap_var内存释放 ←┘ │
static_var保留 ────┘
📌 本章重点总结
概念 | 核心要点 |
---|---|
函数调用 | 通过栈帧实现环境隔离,严格遵循声明→调用顺序 |
参数传递 | 基本类型默认值传递,数组/指针使用地址传递 |
作用域 | 全局>函数>块,同名变量遵循就近原则 |
生命周期 | 自动变量随栈帧销毁,静态变量持久存在,堆内存需手动管理 |
第三章 函数的高级应用
3.1 递归函数
📌 核心概念
- 基线条件:递归终止条件
- 递归条件:函数调用自身的条件
- 执行原理:函数调用栈(后进先出)
💡 经典案例
// 阶乘计算 5 * 4 * 3 * 2 * 1
int factorial(int n) {
if(n <= 1) return 1; // 基线条件
return n * factorial(n-1); // 递归条件
}
// n = 5 factorial(4) (n = 5) * 24 return 120
// n = 4 factorial(3) (n = 4) * 6 return 24
// n = 3 factorial(2) ( n = 3 ) * 2 return 6
// n = 2 factorial(1) ( n = 2 ) * 1 return 2
// n = 1 return 1
// 斐波那契数列
int fibonacci(int n) {
if(n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
🚨 风险与优化
栈溢出:深度递归可能导致栈空间耗尽
优化策略:
- 尾递归优化(需编译器支持)
- 改为迭代实现
3.2 回调函数
📌 实现原理
函数指针:返回值类型 (*指针名)(参数列表)
典型应用:
- 排序算法(如
qsort
) - 事件驱动编程
💡 案例:通用排序
#include <stdlib.h>
回调函数 过程
通过传入 一个函数指针 去是实现某种功能 这种函数使用的方式叫做 回调函数
主函数
负责调用 传入的函数指针
函数指针
负责指向 功能函数
功能函数
负责实现 主函数中所需的某个功能
// 比较函数原型
typedef int (*CompareFunc)(const void*, const void*);
void sortArray(int arr[], int n, CompareFunc cmp) {
qsort(arr, n, sizeof(int), cmp);
}
// 比较函数实现
int compareInt(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 使用
int nums[] = {5,2,7,1};
sortArray(nums, 4, compareInt);
3.3 可变参函数
📌 实现方法
头文件:#include <stdarg.h>
关键宏:
宏 | 功能 |
---|---|
va_list | 定义参数列表指针 |
va_start | 初始化参数列表 |
va_arg | 获取下一个参数 |
va_end | 清理参数列表 |
💡 案例:可变参数求和
#include <stdarg.h>
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for(int i=0; i<count; i++){
total += va_arg(args, i);
}
va_end(args);
return total;
}
// 调用
int s = sum(3, 10, 20, 30); // s=60
⚠️ 注意事项
- 必须明确参数类型
- 缺乏类型安全检查
3.4 内联函数
📌 特性与使用
- 关键字:
inline
- 作用:减少函数调用开销(编译器决定是否内联)
- 适用场景:简单且频繁调用的函数
inline int max(int a, int b) {
return a > b ? a : b;
}
// 编译建议(GCC)
__attribute__((always_inline));
🆚 与宏函数对比
特性 | 内联函数 | 宏函数 |
---|---|---|
类型检查 | 有 | 无 |
调试支持 | 支持 | 展开后不可见 |
副作用 | 无 | 可能产生意外副作用 |
3.5 宏函数
📌 定义与使用
#define SQUARE(x) ((x) * (x))
#define MAX(a,b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; \
})
💡 案例:泛型打印
#define PRINT_VAR(x) _Generic((x), \
int: printf("%d\n", x), \
float: printf("%f\n", x), \
default: printf("Unsupported type\n") \
)
// 使用
int num = 5;
PRINT_VAR(num);
🚨 常见陷阱
错误示例 | 问题分析 | 修正方案 |
---|---|---|
#define SQUARE(x) x*x | SQUARE(3+2) → 3+2*3+2=11 | 添加括号:(x)*(x) |
多语句未用do{}while(0) | 宏展开后逻辑错误 | 使用语句块包裹 |
3.6 命令行参数
在 C 语言中,main
函数可以接受命令行参数,使得程序可以在运行时动态获取输入参数,增强程序的灵活性。
📌 main
函数的参数格式
int main(int argc, char *argv[]);
argc:表示命令行参数的个数,至少为 1(包括程序自身的名称)。
argv:存储命令行参数的字符串数组,
argv[0] 是程序自身的名称
argv[1] 及之后的元素为用户输入的参数。
💡命令行参数的示例
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("参数个数: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("参数 %d: %s\n", i, argv[i]);
}
return 0;
}
📜字符串转换成整数
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("使用方法: %s <num1> <num2>\n", argv[0]);
return 1;
}
int num1 = atoi(argv[1]); // 将字符串转换为整数
int num2 = atoi(argv[2]);
printf("两个数的和: %d\n", num1 + num2);
return 0;
}
综合案例:日志系统
#include <stdarg.h>
// 支持可变参数的日志函数
void log_message(const char* format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
}
// 带调试级别的宏
#define LOG_DEBUG(...) log_message("[DEBUG] " __VA_ARGS__)
#define LOG_ERROR(...) log_message("[ERROR] " __VA_ARGS__)
// 使用
LOG_DEBUG("用户%d登录", 1001);
LOG_ERROR("内存分配失败!");
🚨 高频错误总结
错误类型 | 错误示例 | 解决方案 |
---|---|---|
递归无终止条件 | void f() { f(); } | 添加基线条件 |
错误函数指针类型 | int (*fp)(char) = &atoi | 类型严格匹配 |
宏参数多次求值 | MAX(++a, ++b) | 使用内联函数替代 |
📚 核心要点记忆卡
1. 递归必须包含基线条件
2. 回调函数通过函数指针实现
3. 可变参函数需用va_系列宏
4. 内联函数由编译器决定是否展开
5. 宏函数注意括号和副作用
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 小道士