系列文章目录
学习系列文章:
【初识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 语言的函数分为两类:
- 库函数:编译器自带的 “现成工具”(如
printf、scanf、strlen),无需自己实现,只需包含对应头文件即可使用; - 自定义函数:根据需求自己编写的函数,灵活度高,是编程的核心。
二、函数基础:从使用到定义
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:返回值类型(如int、double,无返回值用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)中的a、b) | 有具体值,占用内存 |
| 形参 | 函数定义时的参数(如Add(int x,int y)中的x、y) | 仅在函数调用时创建内存,是实参的拷贝 |
举例证明:形参和实参的内存是独立的
#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;}
原因:x和y是a和b的拷贝,修改x、y不会影响原变量。若要实现真正交换,需用指针(跳转->指针详解)。
4. return 语句:函数的 “出口”
return 语句的核心规则(必记):
- 可返回数值或表达式(如
return x+y),表达式会先计算再返回; - 无返回值(
ret_type为void)时,return可不写; return执行后,函数立即退出,后续代码不再执行;- 若函数有返回类型(如
int),必须保证所有分支都有return(否则返回值未知); - 返回值类型与函数声明不一致时,编译器会自动隐式转换(如返回
3.14给int函数,会转为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:控制函数 / 变量的 “访问权限”
要理解static和extern,先明确两个概念:
- 作用域:变量 / 函数能被访问的代码范围(局部变量→局部范围,全局变量→整个工程);
- 生命周期:变量从创建(申请内存)到销毁(释放内存)的时间段(局部变量→进入作用域创建,出作用域销毁;全局变量→整个程序运行期间)。
(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. 递归的两个必要条件(缺一不可)
- 存在终止条件(满足条件时递归停止);
- 每次递归调用后,越来越接近终止条件。
反例:死递归(无终止条件)
#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=1203. 递归举例 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 44. 递归与迭代:该怎么选?
迭代就是 “循环”,很多递归问题都能用迭代实现。两者对比:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 递归 | 代码简洁,逻辑清晰 | 栈溢出风险,效率低(栈帧开销) | 问题复杂、迭代难实现(如汉诺塔) |
| 迭代 | 效率高,无栈溢出风险 | 代码逻辑可能更复杂 | 简单问题(如阶乘、斐波那契数) |
反例:递归求斐波那契数(低效!)
斐波那契公式:F(n) = F(n-1) + F(n-2)(n<=2时F(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;}因此,实际开发中,我们需要根据实际情况选择递归或迭代。
四、函数避坑指南(高频踩坑点)
- 数组传参时用
sizeof求个数:错误!函数内arr是指针,sizeof(arr)是指针大小(4/8 字节),需手动传递元素个数sz; - 形参修改影响实参:错误!形参是临时拷贝,需用指针才能修改实参;
- 递归无终止条件或不接近终止条件:会导致栈溢出;
- 函数声明与定义不一致:如声明
int Add(int x,int y),定义int Add(int x),编译报错; static修饰局部变量后认为作用域扩大:错误!作用域仍为局部范围,只是生命周期变长;extern声明未定义的符号:编译通过,链接会报错。