目录

前言

一、函数是什么?为什么要用函数

1.1函数的基本结构

1.2  形参和实参:函数沟通的桥梁

1.2.1 基本定义:

1.2.2形参与实参的关系

1.3 return语句使用的注意事项

二、数组做函数参数

2.1 基本语法

1. 函数声明/定义时形参的写法(三种等价形式)

2.2 调用时实参的传递

2.3 重要特性与注意事项

2.3.1. ⚠️ 在函数内部,无法通过 sizeof 获取数组长度!

2.3.2. ✅ 在函数内修改数组元素,会影响原数组!

2.3.3. 🚫 不能直接返回局部数组!

三、递归函数:自己调用自己的神奇操作

3.1 递归的核心思想

3.2 递归的两大要素

3.3 经典例子 1 :计算阶乘(n!)

3.4 经典例子2:斐波那契数列

四、递归的优缺点和注意事项

4.1 优点

4.2 缺点和风险

五、递归 vs 循环 (迭代)

六、总结


前言

大家好!今天我们来聊聊C语言中两个非常重要的概念:函数和递归。

那我们开始吧!

一、函数是什么?为什么要用函数

数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y=kx+b ,k和b都是常数,给⼀个任意的x,就得到⼀个y值。
其实在C语⾔也引⼊函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。 C语⾔中的函数就是⼀个完成某项特定的任务的⼀⼩段代码。

那我们为什么要用函数呢?

想象一下,如果你要写一个很长的程序,里面有好多重复的操作,比如计算两个数的和、打印欢迎信息等。每次都把代码写一遍,不仅麻烦,还容易出错。这时候,函数就派上用场了!

函数就像一个“小工厂”。你给它一些“原料”(输入),它帮你完成特定的任务,然后把“成品”(输出)交给你。这样,下次再需要做同样的事情时,直接“召唤”这个小帮手就行了,不用再重复写代码。

1.1函数的基本结构

一个函数通常长这样:

返回值类型  函数名(参数列表)
{
    函数体(要做的事情)
    return 返回结果(把结果交上去)  
}
  • 返回值类型:函数做完事后要交给你什么类型的东西?是整数(int)、小数(float)、字符(char),还是什么都不给(void)?
  • 函数名:给你的工厂起个名字,方便以后叫它。
  • 参数列表:你需要给工厂提供哪些“原料”?可以是一个或多个,也可以没有。
  • 函数体:工厂具体怎么干活的步骤。
  • return语句:告诉工厂把最终结果交出来。如果函数类型是void,可以没有return,或者只用return;表示结束

1.2  形参和实参:函数沟通的桥梁

1.2.1 基本定义:

  • 形参 (形式参数):在定义函数时,写在括号里的那些变量名。它们是函数内部用来接收数据的“占位符” 。
  • 实参 (实际参数):在调用函数时,真正传递给函数的具体数据或变量 。
举个例⼦:
写⼀个加法函数,完成2个整型变量的加法操作。
#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;
}

在这里,ab是形参,它们在函数add内部代表传进来的两个数。m和n是实参,是我们在main函数里真正要相加的两个数。

函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。
上⾯只是⼀个例⼦,未来我们是根据实际需要来设计函数,函数名、参数、返回类型都是可以灵活变化的

1.2.2形参与实参的关系

 值传递(Pass by Value)

  • 实参的值被复制给形参。
  • 形参的修改不影响实参。

 关系详解:

总结一句话:

实参是“数据”,形参是“容器”;调用时,数据装进容器,函数用容器里的数据干活。

1.3 return语句使用的注意事项

•  return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。

•  return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。

•  return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
•  return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
•  如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。

二、数组做函数参数

在 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 可以停止

那么为什么会死循环呢?看一下这个流程图

我们通过这个流程图可以发现,一旦这个程序开始运行,进入蓝色方框后就出不来了,于是就一直转下去,陷入了死循环

那么我们如何避免死循环呢?即

如何正确的递归:两要素

  1. 递归基 (Base Case) / 终止条件:这是递归的“出口”。当问题小到一定程度时,就不再调用自己,而是直接给出答案。没有终止条件的递归会导致无限循环,最终栈溢出 。忘记定义基准情形是编写递归函数时的常见错误之一。(利用return)
  2. 递归关系 (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时):

  1. factorial(3)调用 -> 因为3!=0,执行3 * factorial(2)
  2. factorial(2)调用 -> 因为2!=0,执行2 * factorial(1)
  3. factorial(1)调用 -> 因为1!=0,执行1 * factorial(0)
  4. factorial(0)调用 -> 因为0==0,直接返回1(递归基触发!)
  5. 返回到第3步:,返回11 * 1 = 1
  6. 返回到第2步:,返回22 * 1 = 2
  7. 返回到第1步:,返回63 * 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语言乃至所有编程语言的基础。多加练习,动手写代码,你一定能熟练运用它们,写出更优秀的程序!加油!

                

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐