news 2026/4/17 17:37:37

【初识C语言】拆解函数:从基础用法到递归精髓

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【初识C语言】拆解函数:从基础用法到递归精髓

系列文章目录

学习系列文章:
【初识C语言】选择结构(if语句和switch语句)详细解答
【初识C语言】循环结构(while语句、do…while语句和for语句)详细解答
【初识C语言】数组(一维数组和二维数组)详细解答+避坑
【初识C语言】C语言指针从入门到进阶详细解答
【初识C语言】字符 / 字符串函数 + 内存函数(详解+模拟实现+避坑)
【初识C语言】探究数据在内存中的存储(整型、浮点型)

理解函数文章:
【初识C语言】include头文件双引号““和尖括号<>的区别以及使用顺序
【初识C语言】qsort 函数保姆级教程,搞定各种数据类型的排序

实战项目文章:
【初识C语言】经典扫雷C语言实战(原码+解析),看完就能上手拆解与修改


文章目录

  • 系列文章目录
  • 前言
  • 一、函数的核心概念:什么是函数?
  • 二、函数基础:从使用到定义
    • 1. 库函数:现成的 “工具包”
    • 2. 自定义函数:自己造 “工具”
    • 3. 形参和实参:谁在传递数据?
    • 4. return 语句:函数的 “出口”
    • 5. 数组做函数参数:注意!形参不创建新数组
    • 6. 函数的调用:嵌套与链式
      • (1)嵌套调用
      • (2)链式访问
    • 7. 函数的声明和定义:先声明后使用
      • (1)单文件场景:声明在前,定义在后
      • (2)多文件场景:声明在.h,定义在.c
    • 8. static 和 extern:控制函数 / 变量的 “访问权限”
      • (1)static 修饰局部变量:改变生命周期
      • (2)static 修饰全局变量 / 函数:限制作用域
      • (3)extern:声明外部符号
  • 三、函数递归:函数自己调用自己
    • 1. 递归的两个必要条件(缺一不可)
    • 2. 递归举例 1:求 n 的阶乘
    • 3. 递归举例 2:顺序打印整数的每一位
    • 4. 递归与迭代:该怎么选?
  • 四、函数避坑指南(高频踩坑点)

前言

函数是 C 语言的 “灵魂”—— 它能把复杂任务拆解成一个个独立模块,让代码更简洁、可复用,也是后续学习链表、栈等高级数据结构的基础。


一、函数的核心概念:什么是函数?

C 语言中的函数像一个 “小型加工厂”:

  • 输入:函数的参数(原材料);
  • 加工:函数体中的代码(生产流程);
  • 输出:返回值(最终产品);
  • 核心价值:复用性(一次编写,多次调用)、模块化(大任务拆小)。

C 语言的函数分为两类:

  1. 库函数:编译器自带的 “现成工具”(如printfscanfstrlen),无需自己实现,只需包含对应头文件即可使用;
  2. 自定义函数:根据需求自己编写的函数,灵活度高,是编程的核心。

二、函数基础:从使用到定义

1. 库函数:现成的 “工具包”

库函数是 ANSI C 标准规定的常用函数,由编译器厂商实现,质量和效率有保障。使用库函数的关键是:记住功能 + 包含对应头文件

举例:计算平方根(sqrt函数的作用类似数学里的开根号)

#include<stdio.h>#include<math.h>// sqrt函数的头文件intmain(){doubled=16.0;doubleresult=sqrt(d);// 调用库函数sqrtprintf("16的平方根:%.2lf\n",result);// 输出4.00return0;}

库函数学习技巧

  • 查文档:推荐cppreference.com或cplusplus.com,包含函数原型、参数、返回值、示例;
  • 记头文件:数学函数(math.h)、字符串函数(string.h)、输入输出(stdio.h)。

2. 自定义函数:自己造 “工具”

自定义函数的语法格式:

ret_typefun_name(参数列表){函数体;// 加工逻辑return返回值;// 产品输出(ret_type为void时可省略)}
  • ret_type:返回值类型(如intdouble,无返回值用void);
  • fun_name:函数名(见名知意,如Add表示加法);
  • 参数列表:可无参数(写void),也可多个参数(需指定类型和名称);
  • 函数体:完成核心功能的代码块。

举例:实现两个整数的加法

#include<stdio.h>// 自定义加法函数:输入两个int,返回它们的和intAdd(intx,inty){returnx+y;// 直接返回表达式结果}intmain(){inta=10,b=20;intsum=Add(a,b);// 调用自定义函数printf("10+20=%d\n",sum);// 输出30return0;}

3. 形参和实参:谁在传递数据?

调用函数时,参数分为 “实参” 和 “形参”,核心关系:形参是实参的临时拷贝

关键区别:

类型定义特点
实参调用函数时传递的实际数据(如Add(a,b)中的ab有具体值,占用内存
形参函数定义时的参数(如Add(int x,int y)中的xy仅在函数调用时创建内存,是实参的拷贝

举例证明:形参和实参的内存是独立的

#include<stdio.h>voidSwap(intx,inty){inttemp=x;x=y;y=temp;// 这里交换的是x和y(拷贝),实参a和b不变}intmain(){inta=10,b=20;Swap(a,b);printf("a=%d, b=%d\n",a,b);return0;}


原因xyab的拷贝,修改xy不会影响原变量。若要实现真正交换,需用指针(跳转->指针详解)。

4. return 语句:函数的 “出口”

return 语句的核心规则(必记):

  1. 可返回数值或表达式(如return x+y),表达式会先计算再返回;
  2. 无返回值(ret_typevoid)时,return可不写;
  3. return执行后,函数立即退出,后续代码不再执行;
  4. 若函数有返回类型(如int),必须保证所有分支都有return(否则返回值未知);
  5. 返回值类型与函数声明不一致时,编译器会自动隐式转换(如返回3.14int函数,会转为3)。

错误示例:分支缺少 return

// 错误:else分支无return,返回值未知intMax(intx,inty){if(x>y){returnx;}// 缺少else的return}

5. 数组做函数参数:注意!形参不创建新数组

数组作为参数传递时,形参不会创建新数组,本质是传递数组首元素的地址,函数内操作的是原数组。

核心规则:

  • 一维数组形参:可省略大小(如void arr(int arr[], int sz));
  • 二维数组形参:行可省略,列不能省略(如void 2darr(int arr[][5], int row));
  • 必须传递数组元素个数(sz),因为sizeof(arr)在函数内计算的是指针大小(不是数组总大小)。

举例:数组置为 - 1 并打印

#include<stdio.h>// 数组置为-1:arr是首元素地址,sz是元素个数voidset_arr(intarr[],intsz){for(inti=0;i<sz;i++){arr[i]=-1;// 操作原数组}}// 打印数组voidprint_arr(intarr[],intsz){for(inti=0;i<sz;i++){printf("%d ",arr[i]);}printf("\n");}intmain(){intarr[]={1,2,3,4,5};intsz=sizeof(arr)/sizeof(arr[0]);// 计算元素个数(5)set_arr(arr,sz);// 传递数组首地址和个数print_arr(arr,sz);return0;}

6. 函数的调用:嵌套与链式

函数调用不是孤立的,支持“嵌套调用”(函数调用函数)和“链式访问”(函数返回值作为另一个函数的参数)。

(1)嵌套调用

比如计算某年某月的天数,需先判断是否为闰年,再根据月份返回天数:

#include<stdio.h>// 判断闰年:是返回1,否返回0intleap_year(inty){return(y%4==0&&y%100!=0)||(y%400==0);}// 计算某月天数:调用is_leap_year函数intget_days(inty,intm){intdays[]={0,31,28,31,30,31,30,31,31,30,31,30,31};if(leap_year(y)&&m==2){return29;// 闰年2月29天}returndays[m];}intmain(){inty=0,m=0;scanf("%d %d",&y,&m);printf("%d年%d月有%d天\n",y,m,get_days(y,m));return0;}


注意:函数可以嵌套调用,但不能嵌套定义(不能在一个函数内写另一个函数)。

(2)链式访问

将一个函数的返回值作为另一个函数的参数,像链条一样串联:

#include<stdio.h>#include<string.h>intmain(){// 链式访问:strlen的返回值作为printf的参数printf("字符串长度:%d\n",strlen("abcdef"));// 输出6// 有趣的链式访问:printf的返回值是打印的字符个数printf("%d",printf("%d",printf("%d",43)));// 输出4321// 解析:最内层printf打印43(2个字符)→ 返回2;// 中间printf打印2(1个字符)→ 返回1;// 最外层打印1return0;}

7. 函数的声明和定义:先声明后使用

C 语言编译器按 “从上到下” 顺序扫描代码,若函数定义在调用之后,需先 “声明” 函数(告诉编译器函数的名称、返回类型、参数)。

(1)单文件场景:声明在前,定义在后

#include<stdio.h>// 函数声明:交代函数名、返回类型、参数(参数名可省略)// int leap_year(int); (参数名y可省略)intleap_year(inty);intmain(){inty=2026;if(leap_year(y)){// 调用前已声明printf("闰年\n");}else{printf("不是闰年\n");}return0;}// 函数定义:实现具体逻辑intleap_year(inty){return(y%4==0&&y%100!=0)||(y%400==0);}

声明格式:ret_type fun_name(参数类型列表);(如int leap_year(int);)。

如果主函数在自定义函数定义之后,也可以不需要声明。

#include<stdio.h>// 函数定义:实现具体逻辑intleap_year(inty){return(y%4==0&&y%100!=0)||(y%400==0);}intmain(){inty=2026;if(leap_year(y)){// 调用前已声明printf("闰年\n");}else{printf("不是闰年\n");}return0;}

(2)多文件场景:声明在.h,定义在.c

企业开发中,代码会按功能拆分到多个文件,规则:

  • 头文件(.h):存放函数声明、宏定义、类型定义;
  • 源文件(.c):存放函数实现;
  • 使用时:包含头文件(#include "add.h")。
    如果不清楚头文件为什么需要用"",可以看这篇文章

示例:

  • add.h(头文件):
// 函数声明intAdd(intx,inty);
  • add.c(源文件):
// 函数定义intAdd(intx,inty){returnx+y;}
  • test.c(主文件):
#include<stdio.h>#include"add.h"// 包含头文件,使用Add函数intmain(){printf("%d\n",Add(10,20));return0;}

8. static 和 extern:控制函数 / 变量的 “访问权限”

要理解staticextern,先明确两个概念:

  • 作用域:变量 / 函数能被访问的代码范围(局部变量→局部范围,全局变量→整个工程);
  • 生命周期:变量从创建(申请内存)到销毁(释放内存)的时间段(局部变量→进入作用域创建,出作用域销毁;全局变量→整个程序运行期间)。

(1)static 修饰局部变量:改变生命周期

默认局部变量存在“栈区”,出作用域销毁;static修饰后,变量移到“静态区”,生命周期与程序一致(出作用域不销毁,下次进入仍保留原值)。

示例对比:

#include<stdio.h>// 无static修饰:每次进入test,i重新创建为0voidtest1(){inti=0;i++;printf("%d ",i);// 输出1 1 1 1 1}// static修饰:i只创建一次,下次进入保留原值voidtest2(){staticinti=0;i++;printf("%d ",i);// 输出1 2 3 4 5}intmain(){for(inti=0;i<5;i++){test1();}printf("\n");for(inti=0;i<5;i++){test2();}return0;}


使用场景:需要保留函数调用后的状态(如计数器)。

(2)static 修饰全局变量 / 函数:限制作用域

全局变量和函数默认具有 “外部链接属性”(整个工程都能访问),static修饰后,变为 “内部链接属性”(仅当前源文件可访问)。

示例:static 修饰全局变量

  • add.c
staticintval=2026;// static修饰,仅add.c可访问
  • test.c
#include<stdio.h>externintval;// 声明外部全局变量intmain(){printf("%d\n",val);// 编译报错:无法访问add.c的static全局变量return0;}


使用场景:全局变量 / 函数仅当前文件使用,不想被其他文件访问(避免命名冲突)。

(3)extern:声明外部符号

extern用于声明 “在其他文件中定义的全局变量 / 函数”,告诉编译器 “这个符号在别处定义,先别报错”。

示例:

  • add.c
intAdd(intx,inty){returnx+y;}// 无static,外部链接
  • test.c
#include<stdio.h>externintAdd(intx,inty);// 声明外部函数intmain(){printf("%d\n",Add(10,20));// 调用add.c的Add函数return0;}

三、函数递归:函数自己调用自己

递归是函数的高级用法,核心思想是 “大事化小”—— 把复杂问题拆解成与原问题相似但规模更小的子问题,直到子问题无法拆分(递归终止)。

1. 递归的两个必要条件(缺一不可)

  1. 存在终止条件(满足条件时递归停止);
  2. 每次递归调用后,越来越接近终止条件

反例:死递归(无终止条件)

#include<stdio.h>intmain(){printf("hehe\n");main();// 自己调用自己,无终止条件return0;}

结果:栈溢出Stack overflow)—— 每次函数调用会在栈区开辟栈帧,死递归会耗尽栈空间。

2. 递归举例 1:求 n 的阶乘

阶乘公式:n! = n * (n-1)!,终止条件:0! = 1
原题出处:BC167 函数实现计算一个数的阶乘

代码实现:

#include<stdio.h>intFact(intn){if(n==0){return1;// 终止条件:0的阶乘是1}returnn*Fact(n-1);// 递归调用:n! = n*(n-1)!}intmain(){intn=5;printf("%d! = %d\n",n,Fact(n));// 输出120return0;}


递归推演(n=5)

Fact(5)=5*Fact(4)Fact(4)=4*Fact(3)Fact(3)=3*Fact(2)Fact(2)=2*Fact(1)Fact(1)=1*Fact(0)Fact(0)=1(终止)// 回归阶段:1*1 → 2*1 → 3*2 →4*6 →5*24=120

3. 递归举例 2:顺序打印整数的每一位

需求:输入1234,输出1 2 3 4(核心:先打印高位,再打印低位)。

代码实现

#include<stdio.h>voidPrint(intn){if(n>9){Print(n/10);// 递归处理高位(去掉最后一位)}printf("%d ",n%10);// 打印当前最后一位}intmain(){intm=1234;Print(m);// 输出1 2 3 4return0;}


递归推演(m=1234):

Print(1234)→ 先调用Print(123),再打印4Print(123)→ 先调用Print(12),再打印3Print(12)→ 先调用Print(1),再打印2Print(1)→ n<=9,直接打印1// 最终输出:1 2 3 4

4. 递归与迭代:该怎么选?

迭代就是 “循环”,很多递归问题都能用迭代实现。两者对比:

方式优点缺点适用场景
递归代码简洁,逻辑清晰栈溢出风险,效率低(栈帧开销)问题复杂、迭代难实现(如汉诺塔)
迭代效率高,无栈溢出风险代码逻辑可能更复杂简单问题(如阶乘、斐波那契数)

反例:递归求斐波那契数(低效!)
斐波那契公式:F(n) = F(n-1) + F(n-2)n<=2F(n)=1)。

  • 递归实现(不推荐):
#include<stdio.h>intFib(intn){if(n<=2){return1;}returnFib(n-1)+Fib(n-2);}intmain(){intn=40;printf("%d\n",Fib(n));// 计算缓慢,冗余计算极多return0;}

问题:递归过程中存在大量重复计算(如F(40)需要F(39)F(38)F(39)又需要F(38)F(37)F(38)被重复计算)。

  • 迭代实现(推荐!):
#include<stdio.h>intFib(intn){if(n<=2){return1;}inta=1,b=1,c=1;while(n>2){c=a+b;// 第n项 = 第n-1项 + 第n-2项a=b;// 更新n-2项为n-1项b=c;// 更新n-1项为第n项n--;}returnc;}intmain(){intn=40;printf("%d\n",Fib(n));// 瞬间出结果return0;}

因此,实际开发中,我们需要根据实际情况选择递归或迭代。

四、函数避坑指南(高频踩坑点)

  1. 数组传参时用sizeof求个数:错误!函数内arr是指针,sizeof(arr)是指针大小(4/8 字节),需手动传递元素个数sz
  2. 形参修改影响实参:错误!形参是临时拷贝,需用指针才能修改实参;
  3. 递归无终止条件或不接近终止条件:会导致栈溢出;
  4. 函数声明与定义不一致:如声明int Add(int x,int y),定义int Add(int x),编译报错;
  5. static修饰局部变量后认为作用域扩大:错误!作用域仍为局部范围,只是生命周期变长;
  6. extern声明未定义的符号:编译通过,链接会报错。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 5:38:06

看完就想试!BSHM打造的专业级抠图效果展示

看完就想试&#xff01;BSHM打造的专业级抠图效果展示 1. 这不是普通抠图&#xff0c;是能直接用在商业项目里的精细人像分离 你有没有遇到过这些场景&#xff1a; 电商运营要连夜赶制50张商品主图&#xff0c;每张都要把模特从原图里干净利落地抠出来换背景&#xff1b;设计…

作者头像 李华
网站建设 2026/4/18 8:28:19

解决Windows PDF处理难题:3步掌握Poppler高效全功能PDF处理环境

解决Windows PDF处理难题&#xff1a;3步掌握Poppler高效全功能PDF处理环境 【免费下载链接】poppler-windows Download Poppler binaries packaged for Windows with dependencies 项目地址: https://gitcode.com/gh_mirrors/po/poppler-windows 问题导入 在Windows平…

作者头像 李华
网站建设 2026/4/18 10:08:18

Nano-Banana Studio部署案例:SDXL爆炸图生成镜像免配置快速上手

Nano-Banana Studio部署案例&#xff1a;SDXL爆炸图生成镜像免配置快速上手 1. 工具介绍&#xff1a;一键生成专业拆解图 Nano-Banana Studio 是一款基于 Stable Diffusion XL (SDXL) 技术的 AI 图像生成工具&#xff0c;专门用于将各种物体&#xff08;尤其是服装与工业产品…

作者头像 李华
网站建设 2026/4/18 9:37:23

超级Minecraft启动器PCL2-CE:打造专属游戏体验的终极方案

超级Minecraft启动器PCL2-CE&#xff1a;打造专属游戏体验的终极方案 【免费下载链接】PCL2-CE PCL2 社区版&#xff0c;可体验上游暂未合并的功能 项目地址: https://gitcode.com/gh_mirrors/pc/PCL2-CE 还在为Minecraft启动器频繁崩溃而烦恼&#xff1f;尝试过多个启动…

作者头像 李华
网站建设 2026/4/18 3:23:45

训练中断怎么办?Qwen2.5-7B微调常见问题解决方案

训练中断怎么办&#xff1f;Qwen2.5-7B微调常见问题解决方案 在单卡环境下完成大模型微调&#xff0c;尤其是像Qwen2.5-7B这样参数量达70亿的模型&#xff0c;看似简单——镜像已预置、命令已写好、数据已备齐。但实际操作中&#xff0c;你很可能刚敲下回车&#xff0c;就遭遇…

作者头像 李华