基于鲁班猫cat1的嵌入式 C语言完全入门教程(四):函数与递归——模块化编程与分治思想
2026/5/9 18:01:09 网站建设 项目流程

🎯 告别“一main到底”的混乱代码!本篇文章带你掌握C语言的函数,把复杂任务拆解成清晰的小模块;同时深入递归,体会函数调用自身的精妙之美。

在鲁班猫ARM64 Linux上编写真正的可复用代码。

💡打开鲁班猫终端,创建目录~/stydy,跟着代码敲一遍,编译、运行、修改——亲手实践记忆最深。

🛠️ 一、环境确认

bash

cd ~ 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 x=5; return &x; }返回静态/全局/堆分配的地址
递归没有终止条件void endless() { endless(); }严格检查基线条件
递归过深导致栈溢出输入10000计算fib_rec(10000)改用迭代或尾递归优化
全局变量滥用多个函数随意修改全局状态尽量通过参数传递,少用全局
数组参数大小信息丢失void f(int arr[]) { sizeof(arr); }额外传size参数

鲁班猫/嵌入式特注

  • 递归在中断服务函数(ISR)中几乎禁止使用,因为栈空间极其有限。

  • 使用static局部变量保存状态时要注意多线程/重入问题,若涉及中断或RTOS,需要加保护或避免使用。

📚 八、练习作业

  1. 素数判断函数:写一个函数is_prime(int n)返回1表示是素数,0不是。然后在主函数中输出1~100的所有素数。

  2. 递归求和:写递归函数求1+2+...+n,并与迭代版本比较效率。

  3. 递归反转字符串:编写函数reverse(char *str, int left, int right)递归反转字符串。

  4. 最大公约数(GCD):用递归实现欧几里得算法gcd(a, b) = gcd(b, a%b)

  5. 打印图案:用函数print_line(int n, char ch)打印n个字符ch,再调用它输出一个菱形。

💬 完成作业后可以贴在评论区,我会给你点评!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询