C语言入门指南:函数与函数递归
大家好!今天我们来聊聊C语言中两个非常重要的概念:函数和递归。那我们开始吧!形参 (形式参数):在定义函数时,写在括号里的那些变量名。它们是函数内部用来接收数据的“占位符”。实参 (实际参数):在调用函数时,真正传递给函数的具体数据或变量。举个例⼦:写⼀个加法函数,完成2个整型变量的加法操作。//定义一个加法函数int add(int a,int b) //这里的a,b就是形参int main()
目录
2.3.1. ⚠️ 在函数内部,无法通过 sizeof 获取数组长度!
前言
大家好!今天我们来聊聊C语言中两个非常重要的概念:函数和递归。
那我们开始吧!
一、函数是什么?为什么要用函数
那我们为什么要用函数呢?
想象一下,如果你要写一个很长的程序,里面有好多重复的操作,比如计算两个数的和、打印欢迎信息等。每次都把代码写一遍,不仅麻烦,还容易出错。这时候,函数就派上用场了!
函数就像一个“小工厂”。你给它一些“原料”(输入),它帮你完成特定的任务,然后把“成品”(输出)交给你。这样,下次再需要做同样的事情时,直接“召唤”这个小帮手就行了,不用再重复写代码。
1.1函数的基本结构
一个函数通常长这样:
返回值类型 函数名(参数列表)
{
函数体(要做的事情)
return 返回结果(把结果交上去)
}
- 返回值类型:函数做完事后要交给你什么类型的东西?是整数(
int
)、小数(float
)、字符(char
),还是什么都不给(void
)? - 函数名:给你的工厂起个名字,方便以后叫它。
- 参数列表:你需要给工厂提供哪些“原料”?可以是一个或多个,也可以没有。
- 函数体:工厂具体怎么干活的步骤。
- return语句:告诉工厂把最终结果交出来。如果函数类型是
void
,可以没有return
,或者只用return;
表示结束
1.2 形参和实参:函数沟通的桥梁
1.2.1 基本定义:
- 形参 (形式参数):在定义函数时,写在括号里的那些变量名。它们是函数内部用来接收数据的“占位符” 。
- 实参 (实际参数):在调用函数时,真正传递给函数的具体数据或变量 。
#include<stdio.h>
//定义一个加法函数
int add(int a,int b) //这里的a,b就是形参
{
return a + b;
}
int main()
{
int n =0;
int m =0;
scanf("%d %d",&m,&n);
int result = add(n,m); //这里的m,n就是实参,调用add函数
printf("%d\n",result);
return 0;
}
在这里,a
和b
是形参,它们在函数add
内部代表传进来的两个数。m和n是实参,是我们在main
函数里真正要相加的两个数。
1.2.2形参与实参的关系
值传递(Pass by Value)
- 实参的值被复制给形参。
- 形参的修改不影响实参。
关系详解:
总结一句话:
实参是“数据”,形参是“容器”;调用时,数据装进容器,函数用容器里的数据干活。
1.3 return语句使用的注意事项
• return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。
• return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
二、数组做函数参数
在 C 语言中,数组作为函数参数是一个重要且容易混淆的知识点。它的核心机制是:数组名作为实参传递时,实际上传递的是数组首元素的地址(即指针),而不是整个数组的副本。
2.1 基本语法
1. 函数声明/定义时形参的写法(三种等价形式)
void func(int arr[]); // 形式1:不指定大小
void func(int arr[10]); // 形式2:指定大小(编译器忽略)
void func(int *arr); // 形式3:指针形式(最本质)
这三种写法在函数参数中完全等价! 编译器都会将其视为
int *arr
。
2.2 调用时实参的传递
int main() {
int a[5] = {1, 2, 3, 4, 5};
func(a); // 传递数组名 → 实际上传递的是 &a[0]
return 0;
}
数组名
a
在大多数情况下代表首元素地址&a[0]
(类型为int*
)
2.3 重要特性与注意事项
2.3.1. ⚠️ 在函数内部,无法通过 sizeof 获取数组长度!
void func(int arr[]) {
printf("%zu\n", sizeof(arr)); // 输出的是指针大小(如 8 字节),不是数组总大小!
}
✅ 解决方法:必须额外传递数组长度
void printArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int a[] = {10, 20, 30, 40, 50};
int len = sizeof(a) / sizeof(a[0]); // 只能在定义数组的作用域内这样计算
printArray(a, len);
return 0;
}
2.3.2. ✅ 在函数内修改数组元素,会影响原数组!
void modify(int arr[], int len) {
for (int i = 0; i < len; i++) {
arr[i] *= 2; // 直接修改原数组内容
}
}
int main() {
int a[] = {1, 2, 3, 4, 5};
modify(a, 5);
// a 现在是 {2, 4, 6, 8, 10}
return 0;
}
2.3.3. 🚫 不能直接返回局部数组!
int* badFunc() {
int arr[5] = {1, 2, 3, 4, 5};
return arr; // ❌ 错误!返回局部数组地址,函数结束后内存被回收
}
✅ 正确做法:
- 使用静态数组(static)
- 使用动态分配(malloc)
- 或通过参数传入目标数组
三、递归函数:自己调用自己的神奇操作
递归听起来很玄乎,其实很简单。递归就是一个函数在执行过程中,直接或间接地调用自己。就像俄罗斯套娃,一层套一层。
3.1 递归的核心思想
递归的基本原理是将一个大问题分解为一个或多个更小的问题,然后通过调用自身来解决这些更小的问题,直到达到基本情况,即不再需要递归调用的情况
3.2 递归的两大要素
#include <stdio.h>
void daz(int n)
{
printf("%d\n",n);
daz(n+1);
}
int main()
{
daz(0);
return 0;
}
运行一下:
这个时候我们发现,程序陷入了无限死循环,此时按 ctrl + L 可以停止
那么为什么会死循环呢?看一下这个流程图
我们通过这个流程图可以发现,一旦这个程序开始运行,进入蓝色方框后就出不来了,于是就一直转下去,陷入了死循环
那么我们如何避免死循环呢?即
如何正确的递归:两要素
- 递归基 (Base Case) / 终止条件:这是递归的“出口”。当问题小到一定程度时,就不再调用自己,而是直接给出答案。没有终止条件的递归会导致无限循环,最终栈溢出 。忘记定义基准情形是编写递归函数时的常见错误之一。(利用return)
- 递归关系 (Recursive Case):这是递归的“核心”。
3.3 经典例子 1 :计算阶乘(n!)
阶乘的定义:
- 0!= 1 (递归基)
- n!= n × (n-1)!(递归关系)
#include <stdio.h>
int factorial(int n) {
// 1. 递归基:最简单的情况
if (n == 0) {
return 1; // 0的阶乘是1
}
// 2. 递归关系:把问题变小
else {
return n * factorial(n - 1); // 调用自己计算(n-1)!
}
}
int main() {
int n = 5;
int result = factorial(n);
printf("%d! = %d\n", n, result); // 输出: 5! = 120
return 0;
}
执行过程模拟 (n=3时):
factorial(3)
调用 -> 因为3!=0,执行3 * factorial(2)
factorial(2)
调用 -> 因为2!=0,执行2 * factorial(1)
factorial(1)
调用 -> 因为1!=0,执行1 * factorial(0)
factorial(0)
调用 -> 因为0==0,直接返回1
(递归基触发!)- 返回到第3步:,返回1
1 * 1 = 1
- 返回到第2步:,返回2
2 * 1 = 2
- 返回到第1步:,返回6
3 * 2 = 6
3.4 经典例子2:斐波那契数列
斐波那契数列:1, 1, 2, 3, 5, 8, 13...
- F(1) = 1, F(2) = 1 (递归基)
- F(n) = F(n-1) + F(n-2) (递归关系)
#include <stdio.h>
int fibonacci(int n) {
// 递归基
if (n == 1 || n == 2) {
return 1;
}
// 递归关系
else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
int main() {
int n = 6;
int result = fibonacci(n);
printf("第%d个斐波那契数是: %d\n", n, result); // 输出: 第6个斐波那契数是: 8
return 0;
}
四、递归的优缺点和注意事项
4.1 优点
- 代码简洁优雅:对于某些问题(如树的遍历、汉诺塔、分治算法),递归写法比循环更直观、更容易理解。
- 符合人类思维:很多问题天然具有递归结构,用递归解决很自然。
4.2 缺点和风险
- 栈溢出风险:每次函数调用都会占用一定的栈空间。如果递归层次太深(比如无限递归),栈空间会被耗尽,导致程序崩溃。无限递归导致栈空间耗尽 。
- 效率可能较低:函数调用本身有开销(保存现场、恢复现场)。像斐波那契数列的例子,会有大量重复计算。
- 调试困难:层层嵌套的调用,追踪起来比较麻烦。
五、递归 vs 循环 (迭代)
很多递归能解决的问题,用循环也能解决。它们各有千秋:
选择建议:
- 如果问题结构天然适合递归,且深度可控,优先用递归,代码更易读。
- 如果追求极致性能,或者递归深度可能很大,优先用循环。
六、总结
恭喜你!看到这里,你已经掌握了C语言函数和递归的核心知识。
- 函数是代码复用的利器,理解值传递和地址传递的区别是关键。
- 递归是一种强大的编程技巧,记住“递归基”和“递归关系”两大法宝,同时警惕栈溢出的风险。
函数和递归是C语言乃至所有编程语言的基础。多加练习,动手写代码,你一定能熟练运用它们,写出更优秀的程序!加油!
更多推荐
所有评论(0)