基于鲁班猫cat1的嵌入式 C语言完全入门教程(四):函数与递归——模块化编程与分治思想
告别“一main到底”的混乱代码本篇文章带你掌握C语言的函数把复杂任务拆解成清晰的小模块同时深入递归体会函数调用自身的精妙之美。在鲁班猫ARM64 Linux上编写真正的可复用代码。打开鲁班猫终端创建目录~/stydy跟着代码敲一遍编译、运行、修改——亲手实践记忆最深。️ 一、环境确认bashcd ~ mkdir -p stydy cd stydy gcc --version 二、函数代码的积木块1. 为什么用函数避免重复代码同一段逻辑只写一次多处调用。模块化每个函数完成一个独立功能便于调试和维护。复用写好的函数可以在多个项目中使用。抽象调用者不必关心内部实现细节。2. 函数的定义格式返回值类型 函数名(参数列表) { // 函数体 return 返回值; // 如果返回值类型为void则无需return或写return; }示例一个简单的加法函数#include stdio.h // 函数定义 int add(int a, int b) { return a b; } int main() { int result add(3, 5); printf(3 5 %d\n, result); return 0; }int是返回值类型返回整数add是函数名(int a, int b)是参数列表a和b是形参函数体中用return返回结果3. 函数声明原型C语言要求函数在被调用之前必须已知其返回值类型和参数类型。有两种方式满足把整个函数定义放在调用之前如上面的例子。或者先声明函数原型定义放在调用之后。#include stdio.h // 函数声明 int add(int a, int b); int main() { printf(%d\n, add(3, 5)); return 0; } // 函数定义 int add(int a, int b) { return a b; }通常将函数声明放在文件开头或头文件.h中实现模块化。4. 无返回值和无参数的函数void print_hello() { printf(Hello from LubanCat!\n); } int main() { print_hello(); return 0; }5. 形参与实参形参形式参数函数定义时括号内的变量只在该函数内部有效。实参实际参数调用函数时传入的具体值或变量。int square(int x) { // x是形参 return x * x; } int main() { int a 5; int result square(a); // a是实参 // 函数内部改变x不会影响外部的a因为C是值传递 } 三、参数传递值传递 vs 地址传递1. 值传递C语言默认采用值传递实参的值被拷贝给形参函数内部对形参的修改不会影响实参。void try_to_change(int x) { x 100; // 修改的是形参的副本 } int main() { int a 10; try_to_change(a); printf(a %d\n, a); // 仍然是10不是100 }2. 地址传递通过指针如果我们想要函数能够修改外部变量的值就需要传递变量的地址指针。void real_change(int *p) { *p 100; // 通过指针修改原变量 } int main() { int a 10; real_change(a); // 传入a的地址 printf(a %d\n, a); // 输出100 }嵌入式场景外设寄存器的地址也通过指针传递来修改硬件状态。3. 数组作为函数参数数组名在大多数情况下会被隐式转换为指向首元素的指针因此传递数组本质上是传递地址。void print_array(int arr[], int size) { for (int i 0; i size; i) { printf(%d , arr[i]); } printf(\n); } int main() { int nums[] {10, 20, 30}; print_array(nums, 3); // 传入数组名地址 return 0; }⚠️ 在函数内部sizeof(arr)得到的是指针大小8字节 on aarch64而不是整个数组大小。所以通常需要额外传递长度参数。 四、递归函数调用自身递归是一种编程技巧函数直接或间接调用自己。递归能将复杂问题分解为规模更小的子问题。1. 递归三要素终止条件递归必须有一个或多个不再递归调用的情形基线条件否则无限递归导致栈溢出。递推公式将大问题分解为小问题的方法。返回值传递逐步返回计算结果。2. 经典案例1阶乘#include stdio.h // 递归版阶乘 unsigned long factorial_rec(int n) { if (n 1) return 1; // 终止条件 return n * factorial_rec(n - 1); // 递推公式 } // 迭代版阶乘对比 unsigned long factorial_iter(int n) { unsigned long result 1; for (int i 2; i n; i) result * i; return result; } int main() { int n 5; printf(%d! %lu (递归)\n, n, factorial_rec(n)); printf(%d! %lu (迭代)\n, n, factorial_iter(n)); return 0; }3. 经典案例2斐波那契数列int fib_rec(int n) { if (n 1) return n; return fib_rec(n - 1) fib_rec(n - 2); }⚠️ 这种写法效率极低指数级重复计算实际中应使用迭代或记忆化递归。优化版尾递归某些编译器可优化int fib_tail(int n, int a, int b) { if (n 0) return a; if (n 1) return b; return fib_tail(n - 1, b, a b); } // 调用fib_tail(10, 0, 1)4. 经典案例3汉诺塔展示递归思想#include stdio.h void hanoi(int n, char from, char to, char aux) { if (n 1) { printf(移动盘子 1 从 %c 到 %c\n, from, to); return; } hanoi(n - 1, from, aux, to); printf(移动盘子 %d 从 %c 到 %c\n, n, from, to); hanoi(n - 1, aux, to, from); } int main() { int disks 3; hanoi(disks, A, C, B); return 0; }5. 递归的优缺点优点缺点代码简洁符合数学归纳思维函数调用开销大参数压栈、返回易于理解分治算法树、图遍历可能导致栈溢出深度过大某些问题天然适合递归如汉诺塔可能重复计算可加记忆化嵌入式建议递归深度不可控时例如依赖外部输入避免使用递归改用循环或自己维护堆栈。ARM64默认栈空间通常为8MB粗略估算每层消耗几十字节安全深度约几千层但依然不建议设计不确定深度的递归。 五、变量作用域与生命周期1. 局部变量定义在函数内部只在函数内有效生存期从函数调用开始到返回结束。void func() { int x 10; // 局部变量 // x 只能在这里使用 }2. 全局变量定义在函数外部任何函数都能访问生存期贯穿整个程序运行。慎用会导致模块间耦合。int global_counter 0; // 全局变量 void increment() { global_counter; }3. 静态局部变量用static修饰局部变量变量在静态存储区分配函数返回后不销毁保留值供下次调用。void counter() { static int calls 0; // 只初始化一次 calls; printf(该函数已被调用 %d 次\n, calls); } int main() { counter(); // 1 counter(); // 2 counter(); // 3 }4. 静态全局变量 / 函数用static修饰全局变量或函数限制其作用域为当前文件其他文件无法通过extern访问是模块化封装的重要手段。 六、综合实战用函数重构猜数字游戏将猜数字游戏拆解为多个函数展示模块化设计。#include stdio.h #include stdlib.h #include time.h // 函数声明 void init_game(); int generate_secret(); int get_guess(); void check_guess(int guess, int secret, int *attempts, int *finished); void play_game(); // 全局变量也可以封装到结构体这里演示静态变量 static int secret_number; static int attempt_count; int main() { init_game(); play_game(); return 0; } // 初始化随机种子 void init_game() { srand(time(NULL)); } // 生成1~100的随机数 int generate_secret() { return rand() % 100 1; } // 获取玩家输入 int get_guess() { int guess; printf(请输入你的猜测: ); scanf(%d, guess); return guess; } // 检查猜测结果通过指针修改attempts和finished void check_guess(int guess, int secret, int *attempts, int *finished) { (*attempts); if (guess secret) { printf(太小了再试试\n); } else if (guess secret) { printf(太大了再试试\n); } else { printf(恭喜你用了 %d 次猜中了\n, *attempts); *finished 1; } } // 游戏主逻辑 void play_game() { secret_number generate_secret(); attempt_count 0; int finished 0; printf( 鲁班猫猜数字游戏模块化版\n); printf(我已经想好了一个1~100之间的整数。\n); while (!finished) { int guess get_guess(); check_guess(guess, secret_number, attempt_count, finished); } }⚠️ 七、常见陷阱与最佳实践陷阱示例解决办法函数声明缺失调用写在前定义在后未声明提前声明或放置定义在调用前形参修改不影响实参想在函数内交换两个int传值无效传递指针swap(a, b)返回局部变量的地址int* func() { int x5; return x; }返回静态/全局/堆分配的地址递归没有终止条件void endless() { endless(); }严格检查基线条件递归过深导致栈溢出输入10000计算fib_rec(10000)改用迭代或尾递归优化全局变量滥用多个函数随意修改全局状态尽量通过参数传递少用全局数组参数大小信息丢失void f(int arr[]) { sizeof(arr); }额外传size参数鲁班猫/嵌入式特注递归在中断服务函数ISR中几乎禁止使用因为栈空间极其有限。使用static局部变量保存状态时要注意多线程/重入问题若涉及中断或RTOS需要加保护或避免使用。 八、练习作业素数判断函数写一个函数is_prime(int n)返回1表示是素数0不是。然后在主函数中输出1~100的所有素数。递归求和写递归函数求12...n并与迭代版本比较效率。递归反转字符串编写函数reverse(char *str, int left, int right)递归反转字符串。最大公约数GCD用递归实现欧几里得算法gcd(a, b) gcd(b, a%b)。打印图案用函数print_line(int n, char ch)打印n个字符ch再调用它输出一个菱形。 完成作业后可以贴在评论区我会给你点评