函数
目录
六、static 和 extern:函数与变量的 “访问控制”
✨ 引言:
如果说变量是 C 语言的 “零件”,数组是 “零件收纳盒”,那函数就是 “专用工具模块”—— 把实现特定功能的代码封装起来,需要时直接调用,不用重复写冗余代码。比如计算加法、判断闰年、打印数组,都能做成 “专属工具”,让程序逻辑更清晰、更易维护。
一、先搞懂:函数到底是什么?(核心概念)
函数是 C 语言中 “完成特定任务的独立代码块”,就像生活中的 “专用工具”:
- 🔧 比如 “螺丝刀”(函数)的任务是 “拧螺丝”(功能),“计算器”(函数)的任务是 “计算数值”(功能);
- 📦 C 语言程序由无数个函数组成,main 函数是 “程序入口”(所有程序都必须有且只有一个 main 函数);
- 🎯 核心价值:代码复用(写一次多次调用)、逻辑拆分(复杂问题拆成小任务)、便于维护(修改函数不影响其他代码)。
函数的分类(两大核心类别)
| 类别 | 定义 | 特点 | 示例 |
|---|---|---|---|
| 库函数 | 编译器厂商实现的标准函数 | 直接调用,需包含对应头文件 | printf(打印)、sqrt(平方根) |
| 自定义函数 | 程序员自己编写的函数 | 按需设计,灵活适配需求 | Add(加法)、is_leap_year(判断闰年) |
二、库函数:C 语言自带的 “现成工具”(直接用)
库函数是 C 语言标准规定的 “现成工具”,编译器厂商已经实现,我们只需 “调用”,不用关心内部实现。
2.1 库函数的核心常识
- 📚 标准库与头文件:库函数按功能分类,声明在不同头文件中(比如数学函数在
math.h,输入输出函数在stdio.h); - 📖 查询工具:不确定函数用法时,查这两个网站:
- ⚠️ 注意:调用库函数必须包含对应头文件(比如用
sqrt要加#include <math.h>)。
2.2 常用库函数分类(新手必备)
- IO 函数:输入输出相关(
printf、scanf、getchar、putchar); - 字符串操作函数:字符串处理(
strlen求长度、strcpy复制、strcmp比较); - 数学函数:数值计算(
sqrt平方根、pow幂运算、abs绝对值); - 内存操作函数:内存管理(
malloc申请内存、free释放内存、memcpy拷贝)。
2.3 库函数使用示例(sqrt 求平方根)
#include <stdio.h>
#include <math.h> // 必须包含数学函数头文件
int main()
{
double num = 100.0;
double result = sqrt(num); // 调用库函数sqrt,计算100的平方根
printf("%.2lf\n", result); // 输出:10.00
return 0;
}
三、自定义函数:自己打造的 “专属工具”(核心重点)
库函数不能满足所有需求,自定义函数是 “按需设计的工具”,核心掌握 “语法结构 + 形参实参 + return 语句”。
3.1 自定义函数的语法结构(像搭 “小工厂”)
返回值类型 函数名(形式参数列表)
{
函数体; // 实现功能的代码
return 结果; // 返回计算结果(返回值类型非void时必须有)
}
生活化比喻
- 返回值类型:“工厂产品的类型”(比如生产 “整数” 就用
int,不生产产品就用void); - 函数名:“工厂名字”(便于调用,比如
Add、is_leap_year); - 形式参数:“工厂原材料入口”(接收外部传入的数据,可无参数);
- 函数体:“工厂生产流程”(实现功能的核心代码);
- return:“产品出口”(把结果返回给调用者)。
3.2 自定义函数示例(加法函数)
#include <stdio.h>
// 加法函数:接收两个int型“原材料”,返回int型“产品”
int Add(int x, int y) // x、y是形式参数(形参)
{
return x + y; // 生产流程:求和,返回结果
}
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b); // 修正原笔记笔误:scnf→scanf
// 调用Add函数:a、b是实际参数(实参),传入Add的x、y
int sum = Add(a, b);
printf("sum = %d\n", sum);
return 0;
}
3.3 形参和实参的核心区别(高频易错点)
| 特性 | 形式参数(形参) | 实际参数(实参) |
|---|---|---|
| 本质 | 函数的 “参数占位符” | 传递给函数的 “具体数据” |
| 内存分配 | 函数调用时才分配内存(实例化) | 定义时就分配内存 |
| 关系 | 形参是实参的 “临时拷贝” | 实参的值传递给形参 |
| 修改影响 | 修改形参不影响实参 | 实参的修改可能影响传递结果 |
代码验证(形参是临时拷贝)
#include <stdio.h>
void Swap(int x, int y) // x、y是形参(临时拷贝)
{
int temp = x;
x = y;
y = temp;
// 这里修改的是x、y,不是实参a、b
}
int main()
{
int a = 10, b = 20;
printf("交换前:a=%d, b=%d\n", a, b); // 10, 20
Swap(a, b);
printf("交换后:a=%d, b=%d\n", a, b); // 还是10, 20(形参修改不影响实参)
return 0;
}
✅ 结论:传值调用(普通变量作为实参)时,形参的修改不会影响实参。
3.4 return 语句的 5 个核心规则(必记)
- 返回结果:return 后可跟数值或表达式,表达式会先计算再返回;
int Add(int x, int y) { return x + y; // 先计算x+y,再返回结果 } - 无返回值:返回值类型为
void时,return 后可什么都不写(或省略 return);void PrintHello() { printf("Hello World\n"); return; // 可省略,函数执行完自动返回 } - 类型转换:返回值类型与函数声明不一致时,会自动隐式转换;
int Test() { return 3.14; // 浮点数3.14隐式转为整数3 } int main() { printf("%d\n", Test()); // 输出:3 return 0; } - 强制结束函数:return 执行后,函数立即结束,后续代码不再执行;
void Test(int n) { if (n < 0) return; // n<0时直接结束函数 printf("n = %d\n", n); } - 所有路径必须返回:如果函数有返回值(非 void),所有分支都要包含 return;
// 错误:n为偶数时无返回值,编译报错 int IsOdd(int n) { if (n % 2 == 1) return 1; } // 正确:所有路径都有return int IsOdd(int n) { if (n % 2 == 1) return 1; // 奇数返回1 else return 0; // 偶数返回0 }
四、函数的高级用法(实战必备)
4.1 数组作为函数参数(重点!)
数组作为实参时,传递的是数组首元素的地址(不是整个数组),形参本质是指针,因此形参的修改会影响实参数组。
核心规则
- 一维数组传参:形参数组大小可省略(写了也无效);
- 二维数组传参:行可省略,列不可省略(编译器需知道每行有多少元素);
- 必须传数组大小:形参退化为指针,
sizeof(形参数组)计算的是指针大小(4/8 字节),需手动传大小。
示例:数组初始化 + 打印(修正原笔记)
#include <stdio.h>
// 数组初始化:将数组所有元素设为-1
void SetArray(int arr[], int size) // 形参arr本质是指针,size是数组大小
{
for (int i = 0; i < size; i++)
{
arr[i] = -1; // 修改形参arr,实参数组也会变(同一内存地址)
}
}
// 数组打印:打印数组所有元素
void PrintArray(int arr[], int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = {0};
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小(10)
PrintArray(arr, size); // 输出:0 0 0 0 0 0 0 0 0 0
SetArray(arr, size); // 初始化数组为-1
PrintArray(arr, size); // 输出:-1 -1 -1 -1 -1 -1 -1 -1 -1 -1
return 0;
}
4.2 嵌套调用:函数内部调用函数(模块化核心)
函数可以嵌套调用(但不能嵌套定义),比如 “计算某年某月天数”,可拆分为 “判断闰年” 和 “获取天数” 两个函数。
完整示例(修正原笔记笔误)
#include <stdio.h>
#include <stdbool.h> // _Bool类型头文件
// 函数1:判断是否为闰年(返回true/false)
bool IsLeapYear(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return true; // 闰年返回true(1)
else
return false; // 非闰年返回false(0)
}
// 函数2:获取某年某月的天数(嵌套调用IsLeapYear)
int GetDaysOfMonth(int year, int month)
{
// 月份天数数组:索引0不用,1~12对应1~12月
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[month];
// 嵌套调用:如果是闰年且是2月,天数+1
if (IsLeapYear(year) && month == 2)
day += 1;
return day;
}
int main()
{
int year = 0, month = 0;
scanf("%d %d", &year, &month); // 修正原笔记笔误:scaanf→scanf
int days = GetDaysOfMonth(year, month);
printf("%d年%d月有%d天\n", year, month, days);
return 0;
}
4.3 链式访问:函数返回值作为另一个函数的参数
函数的返回值可以直接作为另一个函数的实参,形成 “链式调用”。
示例 1:strlen 返回值作为 printf 参数
#include <stdio.h>
#include <string.h>
int main()
{
// strlen("abcdef")返回6,作为printf的参数
printf("字符串长度:%d\n", strlen("abcdef")); // 输出:6
return 0;
}
示例 2:printf 的返回值(打印字符个数)
#include <stdio.h>
int main()
{
// 内层printf打印43,返回2(字符个数);中层打印2,返回1;外层打印1
printf("%d\n", printf("%d", printf("%d", 43))); // 输出:4321
return 0;
}
✅ 解析:printf的返回值是 “成功打印的字符个数”,因此内层printf("%d",43)打印 2 个字符,返回 2;中层打印 1 个字符(2),返回 1;外层打印 1。
4.4 函数设计原则:高内聚低耦合(进阶必备)
- 高内聚:一个函数只做一件事(比如
IsLeapYear只判断闰年,不打印结果); - 低耦合:函数不依赖外部代码(比如不直接在函数内打印,而是返回结果让调用者处理)。
反例(低内聚)
#include <stdbool.h>
#include <stdio.h>
// 错误:既判断闰年,又打印结果,耦合度高
bool IsLeapYear(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
{
printf("%d是闰年\n", year);
return true;
}
else
{
printf("%d不是闰年\n", year);
return false;
}
}
正例(高内聚低耦合)
#include <stdbool.h>
#include <stdio.h>
// 正确:只判断闰年,返回结果,不打印(高内聚)
bool IsLeapYear(int year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
int main()
{
int year = 0;
scanf("%d", &year);
bool ret = IsLeapYear(year);
// 打印逻辑由调用者处理(低耦合)
if (ret)
printf("%d是闰年\n", year);
else
printf("%d不是闰年\n", year);
return 0;
}
五、函数的声明与定义(多文件开发基础)
函数的 “声明” 是告诉编译器 “函数存在”,“定义” 是实现函数的具体功能。
5.1 单文件场景(新手入门)
如果函数定义在调用之后,必须先声明函数(否则编译器报错)。
示例(先声明后调用)
#include <stdio.h>
// 函数声明:告诉编译器函数名、参数类型、返回值类型
int IsLeapYear(int year);
int main()
{
int year = 2024;
bool ret = IsLeapYear(year); // 调用前已声明,编译器不报错
printf("%d\n", ret);
return 0;
}
// 函数定义:实现具体功能(定义也是一种声明)
int IsLeapYear(int year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
5.2 多文件场景(项目实战)
大型项目中,函数声明放在.h头文件,定义放在.c源文件,便于管理和多人协作。
步骤 1:创建LeapYear.h(头文件,存放声明)
// 函数声明
bool IsLeapYear(int year);
步骤 2:创建LeapYear.c(源文件,存放定义)
#include "LeapYear.h" // 包含头文件
// 函数定义
bool IsLeapYear(int year)
{
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
步骤 3:创建main.c(主文件,调用函数)
#include <stdio.h>
#include "LeapYear.h" // 包含自定义头文件(用双引号)
int main()
{
int year = 2024;
bool ret = IsLeapYear(year);
printf("%d是%s闰年\n", year, ret ? "" : "不");
return 0;
}
多文件开发优势
- 逻辑清晰:声明和实现分离,便于查找和修改;
- 多人协作:不同开发者负责不同
.c文件,互不干扰; - 代码隐藏:可将
.c文件编译为静态库(.lib),只对外提供头文件,保护源代码。
六、static 和 extern:函数与变量的 “访问控制”
6.1 核心概念:作用域与生命周期
- 作用域:变量 / 函数能被访问的范围(比如局部变量的作用域是代码块内);
- 生命周期:变量从内存分配到释放的时间段(比如局部变量的生命周期是进入到退出代码块)。
6.2 static 的 3 种用法(重点)
(1)修饰局部变量:改变生命周期,不改变作用域
- 普通局部变量:存储在栈区,生命周期 = 代码块内;
- static 局部变量:存储在静态区,生命周期 = 整个程序运行期间。
示例
#include <stdio.h>
void Test()
{
static int n = 0; // static修饰局部变量
n++;
printf("%d ", n);
}
int main()
{
for (int i = 0; i < 5; i++)
{
Test(); // 输出:1 2 3 4 5(n不会被销毁,累加)
}
return 0;
}
(2)修饰全局变量:限制作用域为当前源文件
- 普通全局变量:默认有外部链接属性,可通过
extern在其他文件访问; - static 全局变量:内部链接属性,只能在当前
.c文件访问,其他文件无法访问。
(3)修饰函数:限制作用域为当前源文件
- 普通函数:默认有外部链接属性,可通过
extern在其他文件调用; - static 函数:内部链接属性,只能在当前
.c文件调用,其他文件无法调用。
6.3 extern:声明外部符号
extern用于声明 “定义在其他文件的变量或函数”,告诉编译器 “该符号在其他地方存在,无需报错”。
示例(访问其他文件的全局变量)
// 文件1:global.c
int g_val = 2023; // 普通全局变量(外部链接)
// 文件2:main.c
extern int g_val; // 声明外部变量g_val(定义在global.c)
int main()
{
printf("%d\n", g_val); // 输出:2023(成功访问其他文件的变量)
return 0;
}
6.4 static 与 extern 对比表
| 修饰对象 | static 作用 | extern 作用 |
|---|---|---|
| 局部变量 | 生命周期→整个程序,作用域不变 | 无意义(局部变量作用域已限制) |
| 全局变量 | 作用域→当前源文件,生命周期不变 | 声明外部全局变量,扩展访问范围 |
| 函数 | 作用域→当前源文件 | 声明外部函数,扩展调用范围 |
七、新手必避的 8 个坑(红标警告!)
- 形参实参混淆:修改形参以为能改变实参(传值调用不行,需传地址);
- 数组传参忘传大小:形参退化为指针,
sizeof(arr)算的是指针大小; - return 路径不全:非 void 函数的分支未包含 return,编译报错;
- 函数嵌套定义:函数内部不能定义函数(只能嵌套调用);
- 库函数未包含头文件:比如用
sqrt没加#include <math.h>; - 多文件未声明外部符号:调用其他文件的函数 / 变量,未用
extern声明; - 函数名重复:同一项目中函数名不能重复(static 函数除外);
- 二维数组传参省略列:
void Test(int arr[][])错误,列不可省(int arr[][3]正确)。
📝 总结
函数是 C 语言模块化编程的核心,掌握 “库函数调用 + 自定义函数设计 + 多文件组织”,就能写出逻辑清晰、可复用的代码。新手重点要吃透:形参实参的区别、return 语句规则、数组传参的特性,再通过实战练习(比如闰年判断、月份天数计算)巩固。
如果这篇博客帮你理清了函数的逻辑,欢迎点赞收藏!
1405

被折叠的 条评论
为什么被折叠?



