前言

上一节我们学习了函数的基本思想、定义、调用、参数传递,这一节我们先来复习巩固,然后进入 C 语言重点难点 —— 递归函数,最后学习数组作为函数参数(一维、二维、字符数组),是编程必须掌握的核心内容。

一、函数知识复习

1. 函数的核心思想

  • 自顶向下、逐步求精
  • 把复杂大问题拆解为多个简单小问题
  • 一个函数只实现一个功能(功能单一原则)

2. 函数语法关键

函数定义位置
  • 写在 main 函数之前:定义 + 声明一次完成,无需额外声明
  • 写在 main 函数之后:必须先声明(复制函数头 + 分号结尾)
函数声明格式
返回值类型 函数名(参数类型1, 参数类型2...);

3. 函数定义细节

  • 返回值类型:支持 intfloatcharvoid(无返回值)等
  • 函数名:遵循见名知意原则,符合标识符命名规范
  • 形参:个数、类型、顺序必须与实参一一对应
  • 函数体:封装具体功能实现逻辑

4. 函数设计原则

  • 功能单一,避免一个函数实现多个无关功能
  • 高可复用性,支持多次调用
  • 代码简洁,逻辑清晰,便于调试和维护

5. 函数数据传递

  • 值传递:实参将值拷贝给形参,形参修改不影响实参
  • 全局变量:可实现函数间数据传递,慎用(易造成命名冲突)

6. 基础代码调试方法

最常用:添加 printf 打印语句,输出关键变量的值,定位代码错误位置。

二、函数递归

1. 什么是递归?

递归 = 函数自己调用自己

核心特点

  • 是一种特殊的循环实现方式
  • 必须设置结束条件,否则会导致栈空间耗尽(栈溢出)
  • for/while 循环更适合解决分治类问题(如汉诺塔、斐波那契数列)

2. 递归解题思想(以 1+2+…+100 求和为例)

将大问题拆解为同类型的小问题,逐步递推至可直接解决的基础问题:

sum(100) = sum(99) + 100
sum(99) = sum(98) + 99
...
sum(2) = sum(1) + 2
sum(1) = 1  // 基础问题,直接求解

3. 递归两个必备要素

递归的实现必须同时满足以下两个条件,缺一不可:

① 递推公式(问题间的关联关系)
sum(n) = sum(n-1) + n;
② 递归结束条件(基础问题的解)
if(n == 1) return 1;

4. 递归代码(标准模板)

以 1 到 n 求和为例,递归函数的标准实现格式:

int sum(int n)
{
    // 第一步:判断递归结束条件
    if(n == 1)
    {
        return 1;
    }
    // 第二步:不满足结束条件则递归调用自身
    else
    {
        return sum(n-1) + n;
    }
}

5. 递归的限制与适用场景

限制

递归调用层次不能太深,否则会耗尽栈空间,导致程序崩溃。

适用场景

适合解决阶乘、斐波那契数列、汉诺塔、树结构遍历、迷宫求解等分治问题。

6. 递归经典练习

练习 1:求 n 的阶乘(n!)
int fact(int n)
{
    // 0! 和 1! 的结果都是 1,作为结束条件
    if(n == 1 || n == 0)
        return 1;
    // 递推公式:n! = (n-1)! * n
    return fact(n-1) * n;
}
练习 2:汉诺塔问题核心思想

汉诺塔问题是递归的经典应用,核心解题步骤(将 n 个盘子从 A 柱移到 C 柱,B 柱为辅助):

  1. 将 n-1 个盘子从 A 柱 → B 柱(以 C 柱为辅助)
  2. 将最后 1 个盘子从 A 柱 → C 柱
  3. 将 n-1 个盘子从 B 柱 → C 柱(以 A 柱为辅助)

三、数组作为函数参数

1. 数组元素做函数参数

数组单个元素作为函数参数,属于普通值传递,与基本数据类型传参规则一致:

void printEle(int ele){}

调用时直接传入数组的单个元素即可,函数内修改形参不会影响原数组元素。

2. 整个数组做函数参数的核心结论

重点:数组作为函数参数时,传递的不是数组本身,而是数组首元素的地址

原因:数组具有连续性、单一性、有序性的特点,只要拿到首元素地址,就能通过地址偏移访问整个数组的所有元素。

3. 数组做函数参数的两种写法

形式写法(数组形式,易理解)
void printArray(int a[], int len);
本质写法(指针形式,底层实现)
void printArray(int *a, int len);

重要注意点:函数内部通过 sizeof(a) 计算的是指针变量的大小(32 位系统 4 字节,64 位系统 8 字节),而非原数组的实际大小,因此数组传参必须同时传递数组长度

4. 数组传参通用规则

  • 形参:数组形式 + 长度参数(告知函数数组的实际元素个数)
  • 实参:数组名 + 实际长度(数组名代表数组首元素地址)

四、一维数组传参经典练习

1. 找数组中的最大值

int getMax(int a[], int len)
{
    int max = a[0];  // 假设第一个元素为最大值
    for(int i=1; i<len; i++){
        if(a[i] > max) 
            max = a[i];  // 遍历更新最大值
    }
    return max;
}

2. 数组逆序(原地反转)

void reverse(int a[], int len)
{
    int i,j;
    // 双指针法,首尾元素交换,直到指针相遇
    for(i=0,j=len-1; i<j; i++,j--){
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

3. 数组元素查找

通用设计思路

  • 找到目标元素:返回元素下标(或返回 1 表示找到)
  • 未找到目标元素:返回 -1(数组下标无负数,易区分)

五、字符数组(字符串)做函数参数

字符数组用于存放字符串,字符串自带结束标志 \0,函数可通过 \0 判断字符串结束,因此无需传递数组长度

1. 字符数组传参写法

void fun(char s[]){}

调用时直接传入字符数组名即可:fun(str);

2. 模拟实现 string.h 库函数(重点)

手动实现字符串库函数,深入理解字符数组传参和字符串操作:

① 模拟 strlen:求字符串长度(不含 \0
int Strlen(char s[])
{
    int cnt=0;
    // 遍历到结束标志 \0 停止
    while(s[cnt] != '\0') 
        cnt++;
    return cnt;
}
② 模拟 strcpy:字符串拷贝(src → dest)
void Strcpy(char dest[], char src[])
{
    int i=0;
    // 逐个拷贝字符,直到 src 到 \0
    while(src[i] != '\0'){
        dest[i] = src[i];
        i++;
    }
    // 给目标字符串添加结束标志
    dest[i] = '\0';
}
③ 模拟 strcat:字符串拼接(src 拼接到 dest 末尾)
void Strcat(char dest[], char src[])
{
    int i=0,j=0;
    // 先找到 dest 的结束位置
    while(dest[i] != '\0')
        i++;
    // 从 dest 末尾开始拼接 src
    while(src[j] != '\0'){
        dest[i++] = src[j++];
    }
    // 拼接后添加结束标志
    dest[i] = '\0';
}
④ 模拟 strcmp:字符串比较

比较规则:按 ASCII 码值逐字符比较,返回第一个不同字符的 ASCII 差值

  • 返回 0:两个字符串相等
  • 返回正数:s1 > s2
  • 返回负数:s1 < s2
int Strcmp(char s1[], char s2[])
{
    int i=0;
    // 逐字符比较,直到字符不同或到字符串结束
    while(s1[i] == s2[i] && s1[i] != '\0')
        i++;
    // 返回差值
    return s1[i] - s2[i];
}

六、二维数组做函数参数

1. 二维数组传参关键要求

二维数组作为函数参数时,必须显式指定列数,行数可省略,因为二维数组的底层是 “一维数组的数组”,需通过列数确定每行的元素个数。

2. 二维数组传参的两种写法

形式写法(数组形式)
void print(int a[3][4], int row);  // row 为行数
本质写法(指针形式)
void print(int (*a)[4], int row);  // 指向包含4个int的一维数组的指针

3. 二维数组传参规则

  • 形参:指定列数的数组形式 + 行数参数
  • 实参:二维数组名 + 实际行数

七、思考

  1. 实现函数:从键盘输入一个字符串到字符数组中
  2. 实现函数:在字符串数组中找到字典序最大的字符串
  3. 实现三种排序算法:冒泡排序、选择排序、插入排序(针对整型数组 / 字符串数组)
  4. 实现函数:二分查找字符串数组中的指定字符串(找到返回下标,未找到返回 -1)

八、本章核心总结

  1. 递归的本质是函数自己调用自己,必须设置结束条件,否则会栈溢出
  2. 递归三要素:递推公式、结束条件、递归调用自身
  3. 数组做函数参数,底层传递的是首元素地址,而非整个数组
  4. 字符数组(字符串)传参无需传递长度,因为字符串自带结束标志 \0
  5. 二维数组传参必须显式指定列数,行数可作为参数传递
  6. 函数设计遵循功能单一、高可复用、易调试原则
  7. 递归适合解决分治类问题,数组传参是嵌入式 / C 语言编程的核心基础
Logo

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

更多推荐