本文Gitee链接———2025.5.29、5.30.static关键字————Blog code: 本仓库仅用于存放博客上的代码
我们早在数学中见过函数的概念:⼀次函数 y = kx + b ,k 和 b 都是常数,给⼀个任意的 x,就得到⼀个 y 值。
函数一词源于清代,中国数学家 李善兰 在19 世纪中叶翻译英国数学家乔治・皮科克(George Peacock)的《代数学》时,将 “function” 译为 “函数”。
“函” 字在汉语中有 “匣子”“容器” 之意(如 “函套”),李善兰取 “包含”“对应” 之义,用 “函数” 表示一个量(自变量)通过某种规则 “包含” 或 “决定” 另一个量(因变量),即 “凡此变数中函彼变数者,则此为彼之函数”(《代数学》序)。
一、函数:编程世界的 “模块化工厂”
核心类比
- 函数 = 工厂:工厂接收原材料(输入参数),通过生产线加工(函数体),产出产品(返回值)。
- 程序 = 工厂集群:复杂任务分解为多个小工厂协作,每个工厂专注解决一个具体问题(如计算、数据处理),提升效率和复用性。
1. 库函数(预制工厂)
定义:由 C 语言标准规定、编译器厂商实现的 “现成工厂”,如printf
(打印工厂)、sqrt
(平方根工厂)。
特点:无需自己搭建,直接调用,但需知道 “工厂地址”(包含对应头文件,如#include <math.h>
)。
使用步骤:
- 查文档(如cppreference
https://zh.cppreference.com/w/c/header这个有搜索功能,上面的没有
https://legacy.cplusplus.com/reference/clibrary/)
- 了解工厂功能(函数功能)、原料类型(参数)、产品类型(返回值)。
栗子sqrt:
图中的小绿字:
double sqrt (double x);是函数原型,
sqrt指的是函数名,x是参数,表示调用sqrt函数需要传递一个double类型的值,
第一个double指的是返回值的类型,第二个double指的是参数的类型
而我们点击上面的链接进行学习了解库函数时,这些文档从上往下的一般格式是:
- 函数原型
- 函数功能介绍
- 参数返回类型
- 代码栗子和运行结果
- 相关相似知识链接
我们使用上面的两个链接时,可以在第二个链接查找,知道了这个库函数的头文件之后去第一个链接浏览(因为一般的网页都会有翻译的提示,点击一下就全翻译为中文了)
如果你觉得有点麻烦,那就对了,根本不用点击那两个网站直接问一下AI就行了
例如我接下来将链式访问的时候,就发生了错误,因为英文水平不够,理解错了,哈哈哈,所以还是AI好,啥年代了,想知道一个问题还要自己查?直接让AI去查就行了,我学校的企业微信都用AI了,再说你英语还不一定有现在的我好呢,我好歹高考英语也考了130呢,你也别被什么英语不好,将来看不懂国外的技术文件什么的吓着了,直接让AI翻译就行了,点一下的事儿,而且你以后需不需要看那些东西还不一定呢
2. 自定义函数(自建工厂)
定义:根据需求自己设计的工厂,语法结构:
产品类型 工厂名(原料类型1 原料名1, 原料类型2 原料名2)
{
函数体逻辑; ———— 加工步骤
return 最终产品; ———— 没有产品时用void
}
栗子:设计加法工厂Add
int Add(int a, int b)————接收两个整数原料
{
return a + b;————加工后返回和
}
调用:int sum = Add(3, 5); ————输入3和5,得到8
设计不需要返回值的函数:
void dayin (int a)
{
printf("%d\n",a);
return ;
}
设计函数需要设计这四个部分:
- 原材料(输入参数)参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥
- 工厂名(函数名)
- 生产线加工(函数体)
- 产出的产品(返回值)
二、函数的形参与实参
先来个栗子:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a;
int b;
scanf("%d %d", &a, &b);
int c = Add(a, b);
printf("%d",c);
return 0;
}
核心类比(当“原料”不是数组时)
- 实参 = 原件:调用函数时传递的真实数据(如变量
a
的值)。 - 形参 = 复印件:函数内部用于临时存储数据的 “副本”,与原件独立存放。在没有调用函数时,不会向内存申请空间,不会真实存在,只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间
- 关键点:修改复印件不影响原件,不调用函数时不需要复印件,因此不会向内存申请空间。
栗子:
void Change(int x)————x是复印件
{
x = 100;————仅修改复印件
}
int main() {
int a = 5;————原件
Change(a);————传递原件,函数内生成x=5的复印件
printf("%d", a);————输出5(原件未改变)
return 0;
}

当“原料”是数组时
首先我们要了解一下数组名字的本质:数组名的本质是数组首个元素的地址
上面的案例中“修改复印件不影响原件(如函数内改变形参x
的值,实参a
不变)”,但当数组作为参数时,形参和实参操作的是同一个数组。
核心问题:如何向函数传递一大箱原料(数组)?
方法:传递箱子地址(数组名本质是首元素地址)+ 箱子容量(元素个数)。
语法特点:
函数形参可写为数组形式int arr[]
,但本质是指针(无需创建新箱子,直接操作原箱)。
二维数组需指定列数(如int arr[][5]
),否则无法确定每层箱子的结构。
栗子:
void ClearArray(int arr[], int size) // 接收数组地址和长度
{
for (int i = 0; i < size; i++)
{
arr[i] = -1; // 直接修改原数组
}
}
int main()
{
int data[5] = {1, 2, 3};
ClearArray(data, 5); // 传递数组地址和长度
for (int i = 0; i < 5; i++)
{
printf("%d",data[i]);
}
return 0;
}
运行结果:
-1-1-1-1-1
可以看到,不同于上面的修改复印件不影响原件,ClearArray函数操作完之后,main函数中printf函数打印的结果是五个-1,因此可知:当数组作为参数时,形参和实参操作的是同一个数组
总结一下:
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参的数组形式也是可以使用同样的数组名(不建议实参和形参使用相同名称,可能让你误以为函数会接收完整数组,而实际上接收的是指针。)
- 形参如果是一维数组,数组大小可以省略不写
- 形参如果是二维数组,行可以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的,形参操作的数组和实参的数组是同一个数组
三、函数的 “生产线”:返回值与return
核心类比
return
= 工厂出货口:
作用:结束生产线(函数执行),并将产品(返回值)传递给调用者。
规则:
有产品时需匹配类型(如工厂声明产int
,return
需是整数或可转换为整数的值)。
无产品时用void
(如打印工厂printf
,只执行动作不返回具体数据)。
注意事项
1. return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达的结果。如:先计算3+5,后返回
int Add(int a, int b)————接收两个整数原料
{
return a + b;————加工后返回和
}
调用:int sum = Add(3, 5); ————输入3和5,得到8
2. return语句执行后,函数就彻底返回,后边的代码不再执行。
void ClearArray(int arr[], int size) // 接收数组地址和长度
{
return;
for (int i = 0; i < size; i++)
{
arr[i] = -1; // 直接修改原数组
}
return;
}
int main()
{
int data[5] = {1, 2, 3};
ClearArray(data, 5); // 传递数组地址和长度
for (int i = 0; i < 5; i++)
{
printf("%d",data[i]);
}
return 0;
}
运行结果:
12300
3. void函数的return规则
-
允许省略return
当void函数执行到函数体末尾时,编译器会自动生成隐式返回操作,无需显式添加return
void Change(int x)————x是复印件 { x = 100;————仅修改复印件 //到这里编译器会自动生成return;只是把它隐藏起来了 }
-
选择性使用return
若需要提前终止函数执行,可显式使用return;
(不带返回值)。例如上面的栗子中在for循环前面加上一句return;运行结果显示数组未被改变void ClearArray(int arr[], int size) // 接收数组地址和长度 { return; for (int i = 0; i < size; i++) { arr[i] = -1; // 直接修改原数组 } }
4. 如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
需要注意的是,void函数的末尾return;可以省略,但是因为其生成了隐式return;所以同样符合每种情况下都有return返回的规则,栗子:
#include <stdio.h>
void ClearArray(int arr[], int size)// 接收数组地址和长度
{
int a=0;
scanf("%d",&a);
if (a > 0)
{
for (int i = 0; i < size; i++)
{
arr[i] = -1; //直接修改原数组
}
return;
}
}
int main()
{
int data[5] = { 1,2,3 };
ClearArray(data, 5);//传递数组地址和长度
for (int i = 0; i < 5; i++)
printf("%d", data[i]);
return 0;
}
5. 其他
return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。
函数的返回类型如果不写,编译器会默认函数的返回类型是int。
函数写了返回类型,但是函数中没有使用return返回值(包括隐式return),那么函数的返回值是未知的。
四. 链式访问和嵌套调用
1. 链式访问
翻译:返回值成功时,返回写入的字符总数。(但是这是不对的,以为我们的英文理解出了问题,所以想了解一个函数,最好还是问一下AI哈哈)来看一下AI的解答:
因此我们可以知道该函数printf("%d", 43);的返回值是2,因为输出了两个字符,来看看这个链式访问的结果:
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
分析一下:第一个printf函数打印的结果是第二个printf函数的返回值,而第二个printf函数打印的是printf("%d", 43);的返回值,刚才我们说了printf("%d", 43);的返回值是2(打印一个43),因此第二个printf函数打印数字2(打印一个2),因此第一个printf函数打印数字1(打印一个1)
故最终打印的结果是:4321
将一个表达式的返回值最为另一个函数的参数,像链条一样将函数链接起来,这个就叫做链式访问。
2. 嵌套调用
顾名思义,就是在使用一个函数的时候嵌套着另一个函数
类比:组装汽车工厂时 1.调用轮胎工厂生产轮胎 2. 调用发动机工厂生产发动机。
还记得我们之前的猜数字游戏(一分钟内猜五次,猜不对就强制关机)吗?当时我们没有讲解怎么把它的函数分装,就是留到现在讲解,怎么样,有没有感觉到我强烈的设计感?
首先回顾一下这个猜数字游戏的代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand((unsigned int)time(NULL));//随机数生成
int 随机数 = rand() % 100 + 1;
int 输入值 = 0;
int 次数=5;
int 选择;
printf("**********************************\n");
printf("***********1.play ****************\n");
printf("***********0.exit ****************\n");//菜单打印
printf("**********************************\n");
scanf("%d", &选择);
switch (选择) //Switch选择情况
{
case 1:
printf("游戏开始\n");
system("shutdown -s -t 60");
printf("请注意,你还有%d次机会,如果你不能在1分钟内回答正确,就会关机\n", 次数);
while (1)
{
printf("请猜数字:\n");
scanf("%d", &输入值);//主体while循环
if (输入值 < 随机数)
printf("猜小了\n");
else if (输入值 > 随机数)
printf("猜大了\n");
else
{
printf("猜对了,游戏结束\n");
printf("骗你的,我根本就没写取消关机的程序,哎~,我是出生~准备关机吧哈哈哈哈哈\n");
//system("shutdown -a");
printf("骗你的,其实我写了\n");
printf("但是我注释掉了,如果你没认真看我的代码就不会把它取消注释\n");
printf("这是对你不认真看代码的惩罚,\n哎~~~,我是出生~\n准备关机吧哈哈\n哈哈哈\n");
break;
}
次数--;
if (次数 == 0)
{
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
break;
}
}
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误");
break;
}
return 0;
}
我们浏览代码可以看到,代码运行的逻辑很简单:首先打印游戏菜单,然后利用switch语句进行菜单选项的选择,其中的case 1;语句控制游戏运行。
首先我们可以先将菜单打印分装成一个函数:这个菜单函数不需要参数
void menu()
{
printf("**********************************\n");
printf("***********1.play ****************\n");
printf("***********0.exit ****************\n");//菜单打印
printf("**********************************\n");
}
将其分装之后,我们只需要在原来打印菜单的部分调用一下这个函数就可以了:
void menu()
{
printf("**********************************\n");
printf("***********1.play ****************\n");
printf("***********0.exit ****************\n");//菜单打印
printf("**********************************\n");
}
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand((unsigned int)time(NULL));//随机数生成
int 随机数 = rand() % 100 + 1;
int 输入值 = 0;
int 次数 = 5;
int 选择;
menu();
scanf("%d", &选择);
switch (选择) //Switch选择情况
{
case 1:
printf("游戏开始\n");
system("shutdown -s -t 60");
printf("请注意,你还有%d次机会,如果你不能在1分钟内回答正确,就会关机\n", 次数);
while (1)
{
printf("请猜数字:\n");
scanf("%d", &输入值);//主体while循环
if (输入值 < 随机数)
printf("猜小了\n");
else if (输入值 > 随机数)
printf("猜大了\n");
else
{
printf("猜对了,游戏结束\n");
printf("骗你的,我根本就没写取消关机的程序,哎~,我是出生~准备关机吧哈哈哈哈哈\n");
system("shutdown -a");
printf("骗你的,其实我写了\n");
printf("但是我注释掉了,如果你没认真看我的代码就不会把它取消注释\n");
printf("这是对你不认真看代码的惩罚,\n哎~~~,我是出生~\n准备关机吧哈哈\n哈哈哈\n");
break;
}
次数--;
if (次数 == 0)
{
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
break;
}
}
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误");
break;
}
return 0;
}
接下来,我们将游戏主体分装一下:因为游戏主题部分需要比较输入值和随机数的大小并打印还有几次机会,因此调用时需要将这三个参数传递给game函数,需要注意的是,包含system函数的头文件的语句要提前到game函数之前,不然会报错。
#include <stdlib.h>
void game(int 次数, int 输入值, int 随机数)
{
printf("游戏开始\n");
system("shutdown -s -t 60");
printf("请注意,你还有%d次机会,如果你不能在1分钟内回答正确,就会关机\n", 次数);
while (1)
{
printf("请猜数字:\n");
scanf("%d", &输入值);//主体while循环
if (输入值 < 随机数)
printf("猜小了\n");
else if (输入值 > 随机数)
printf("猜大了\n");
else
{
printf("猜对了,游戏结束\n");
printf("骗你的,我根本就没写取消关机的程序,哎~,我是出生~准备关机吧哈哈哈哈哈\n");
system("shutdown -a");
printf("骗你的,其实我写了\n");
printf("但是我注释掉了,如果你没认真看我的代码就不会把它取消注释\n");
printf("这是对你不认真看代码的惩罚,\n哎~~~,我是出生~\n准备关机吧哈哈\n哈哈哈\n");
break;
}
次数--;
if (次数 == 0)
{
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
break;
}
}
}
将其分装之后,我们只需要在原来case 1 ;语句后面调用一下这个函数就可以了
void menu()
{
printf("**********************************\n");
printf("***********1.play ****************\n");
printf("***********0.exit ****************\n");//菜单打印
printf("**********************************\n");
}
#include <stdlib.h>
void game(int 次数, int 输入值, int 随机数)
{
printf("游戏开始\n");
system("shutdown -s -t 60");
printf("请注意,你还有%d次机会,如果你不能在1分钟内回答正确,就会关机\n", 次数);
while (1)
{
printf("请猜数字:\n");
scanf("%d", &输入值);//主体while循环
if (输入值 < 随机数)
printf("猜小了\n");
else if (输入值 > 随机数)
printf("猜大了\n");
else
{
printf("猜对了,游戏结束\n");
printf("骗你的,我根本就没写取消关机的程序,哎~,我是出生~准备关机吧哈哈哈哈哈\n");
system("shutdown -a");
printf("骗你的,其实我写了\n");
printf("但是我注释掉了,如果你没认真看我的代码就不会把它取消注释\n");
printf("这是对你不认真看代码的惩罚,\n哎~~~,我是出生~\n准备关机吧哈哈\n哈哈哈\n");
break;
}
次数--;
if (次数 == 0)
{
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
break;
}
}
}
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h>
int main()
{
srand((unsigned int)time(NULL));//随机数生成
int 随机数 = rand() % 100 + 1;
int 输入值 = 0;
int 次数 = 5;
int 选择;
menu();
scanf("%d", &选择);
switch (选择) //Switch选择情况
{
case 1:
game(次数,输入值,随机数);
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误");
break;
}
return 0;
}
至此,猜数字游戏的分装完成,我们会看这个程序,在main函数中调用了菜单menu函数和游戏game函数,这个就叫做嵌套调用。
五. 函数的声明和定义
核心逻辑
- 定义:工厂的具体实现(画出生产线图纸,如上面栗子中的game函数的创建)。
- 声明:提前告知存在(如在医院挂号处登记科室名称),让其他模块知道可以调用。
- 场景:
- 单文件:定义在调用前,可省略声明(相当于直接看到图纸)。
- 多文件:在头文件(
.h
)中声明,源文件(.c
)中定义(如医院的挂号单和科室实际位置分离)。
单文件
函数的声明就是告诉编译器,有一个函数:名字是什么,参数是什么,返回类型是什么,函数声明中参数只保留类型,省略掉名字也是可以的。
函数的使用规则就是先声明后调用,就像你去医院看病,就要先挂号再看病
函数的定义就是一种特殊是声明,这个很容易理解,我不仅告诉了编译器一个函数:名字是什么,参数是什么,返回类型是什么,我连这个函数怎么实现的都告诉你了,已经把内裤都交代清楚了,这还不算声明吗?
以上面的猜数字游戏最终版的栗子来说
在main函数前面定义了menu函数和game函数,声明了两个函数,所以后面就能够直接调用,现在我们把这两个函数的定义放到main函数后面,此时VS就会报错
因为我们违反了先声明后调用的使用规则,所以我们需要再调用之前将这两个函数声明一下,函数的声明就是告诉编译器,有一个函数:名字是什么,参数是什么,返回类型是什么
因此,对于菜单menu函数,声明的语句为:void menu();
对于game函数,声明的语句为:void game(int,int,int);
不过其实声明的时候根本不需要那么麻烦,只要把定义函数时的第一行复制一下就行了,别忘了加上;
void menu();
void game(int ,int,int);
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int main()
{
srand((unsigned int)time(NULL));//随机数生成
int 随机数 = rand() % 100 + 1;
int 输入值 = 0;
int 次数 = 5;
int 选择;
menu();
scanf("%d", &选择);
switch (选择) //Switch选择情况
{
case 1:
game(次数, 输入值, 随机数);
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误");
break;
}
return 0;
}
void menu()
{
printf("**********************************\n");
printf("***********1.play ****************\n");
printf("***********0.exit ****************\n");//菜单打印
printf("**********************************\n");
}
void game(int 次数, int 输入值, int 随机数)
{
printf("游戏开始\n");
system("shutdown -s -t 60");
printf("请注意,你还有%d次机会,如果你不能在1分钟内回答正确,就会关机\n", 次数);
while (1)
{
printf("请猜数字:\n");
scanf("%d", &输入值);//主体while循环
if (输入值 < 随机数)
printf("猜小了\n");
else if (输入值 > 随机数)
printf("猜大了\n");
else
{
printf("猜对了,游戏结束\n");
printf("骗你的,我根本就没写取消关机的程序,哎~,我是出生~准备关机吧哈哈哈哈哈\n");
system("shutdown -a");
printf("骗你的,其实我写了\n");
printf("但是我注释掉了,如果你没认真看我的代码就不会把它取消注释\n");
printf("这是对你不认真看代码的惩罚,\n哎~~~,我是出生~\n准备关机吧哈哈\n哈哈哈\n");
break;
}
次数--;
if (次数 == 0)
{
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
printf("五次机会已用尽,准备关机哈哈哈哈哈\n");
break;
}
}
}
需要注意的是stdlib.h需要包含在main函数之前,因为srand和rand函数的头文件也是stdlib.h
多文件
在企业工作中多文件才是正常的,单文件估计只有学习的时候会拿它举例子,谁用多文件的模式有以下好处:
1. 模块化和组织清晰
将相关功能的函数和数据放在同一个文件中,形成独立模块。例如把计算相关函数放在math.c
,输入输出相关放在io.c
,项目结构就会很清晰,代码逻辑一目了然,理解起来轻松,后续查找和修改特定功能代码也更方便 。
2. 方便代码复用
一个.c
文件及其配套.h
文件实现的功能,可轻松复用到不同项目。像自己编写的字符串处理函数放在string_utils.c
及string_utils.h
中,之后新项目若有字符串处理需求,直接引入这俩文件就能用,减少重复开发工作量 。
3. 提高编译效率
大型项目中,若所有代码在一个文件,每次修改一点内容都要重新编译整个文件。而多文件结构下,仅修改少数.c
文件时,编译器只需重新编译被修改的,其他未改动文件的编译结果可直接使用,大大缩短编译时间 。
4. 便于团队协作
多个开发者能同时在不同文件上工作,比如开发者 A 负责module1.c
,开发者 B 负责module2.c
,大家工作相对独立,减少代码冲突,提升开发效率,也便于项目管理和分工 。
5. 实现信息隐藏与封装
头文件只暴露函数接口(声明),函数具体实现细节藏在.c
文件里。像调用math.c
里的函数,只需看math.h
了解函数名、参数和返回值类型,不用知道内部计算过程,提高代码封装性,保护内部实现逻辑,也让代码对外接口更简洁明了 。
⼀般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在源文件(.c)⽂件中。
add.h
(声明):int Add(int a, int b);
add.c
(定义):int Add(int a, int b) { return a + b; }
main.c
(调用):#include "add.h" ————引入声明 int result = Add(3, 5); ————直接调用
这里需要特别注意的是,在包含我们自己编写的头文件时,应使用双引号(
""
),而不是尖括号(<>
) 。
我们之前使用库函数的时候,就要包括其所在的头文件,实际上,头文件的本质就是对函数的声明
六. static和extern
在讲解这static这个关键字之前要先来了解两个概念并阅读一个类比的故事:
作用域:变量的可见范围(全局 / 函数 / 块级)。
- 全局作用域:函数外部定义的变量,文件内所有函数可见(其他文件需
extern
声明)。 - 局部作用域:函数 / 代码块内定义的变量,仅在该区域可见。
- 函数原型作用域:函数声明中的参数名仅在原型中有效。
- 文件作用域:
static
修饰的全局变量仅限当前文件访问。
生命周期:变量的存在时间(创建→销毁,如函数结束释放局部变量)。
- 静态存储期:全局变量、
static
变量,程序启动时创建,结束时销毁。 - 自动存储期:局部变量(默认),函数 / 代码块执行时创建,结束时销毁。
- 动态存储期:
malloc
/calloc
分配的内存,需手动free
释放。
接下来的故事就围绕这张图片进行讲解:
在编程职场中,static
就像一张具有神奇魔力的 “特权卡”,为职场 “打工人”—— 变量和函数,开辟出两条截然不同的进阶之路。
一、从 “临时工” 到 “资深老员工”:存储与生命的蜕变
想象有一家名叫 “函数小作坊” 的公司,这里原本充斥着大量 “临时工” 性质的局部变量员工。这些临时工每天随着公司业务启动(函数调用)才急急忙忙入职,业务结束(函数返回)就立马卷铺盖走人。每次入职都得重新熟悉工作流程(初始化),之前积累的工作成果也不翼而飞,下次来还得从头开始。
直到有一天,公司推行了 “static
特权卡” 计划。拿到这张卡的局部变量员工,就像开启了 “职场飞升” 通道。他们瞬间从临时工摇身一变,成为了公司的 “资深老员工”。这些老员工在公司初创(程序首次执行到变量定义处)时就入职,并且会一直坚守岗位,直到公司彻底关门(程序结束)才离开。而且,他们只在入职时经历一次全面的培训(初始化),之后不管公司业务怎么折腾(函数如何频繁调用),他们都能凭借之前积累的经验继续发光发热,每次工作的成果也会保留下来,为下一次任务做好铺垫。
就好比小作坊里负责统计订单数量的员工,以前用临时工,每次计数都得重新开始。用上 “static
特权卡” 转正后,就能准确累计每个周期的订单数,再也不会出现计数混乱的情况。这背后的原理就是static
改变了局部变量的存储位置,从原来临时工聚集的 “栈区临时工棚” 搬到了正式员工专属的 “静态区豪华办公室”,进而彻底改变了其生命周期。
二、从 “开放式办公” 到 “独立私密隔间”:链接属性的变革
再看编程职场里的大型集团公司,旗下有多个部门(源文件)。各个部门都有自己的员工(函数、全局变量)。在默认情况下,这些员工处于 “开放式办公” 模式,部门之间交流频繁,其他部门可以通过喊名字(extern
声明)的方式,轻松借用别的部门员工来协助工作,这就是所谓的 “外部链接” 属性。
但随着公司业务逐渐复杂,部门间经常出现人员名字重复(命名冲突)导致工作混乱的情况,而且有些部门的核心工作也不希望被随意打扰。这时,“static
特权卡” 再次闪亮登场。当部门内的某些员工拿到这张卡后,就如同被分配到了独立私密的隔间办公,他们从 “开放式办公” 切换到了 “内部链接” 模式。其他部门就算扯着嗓子喊他们的名字(extern
声明),也无法与这些持有特权卡的员工取得联系。
比如集团里两个部门都有个叫 “财务助手” 的员工,负责不同的财务核算工作。没使用 “static
特权卡” 时,常常会因为名字一样而在工作协调上出乱子。而给各自部门的 “财务助手” 都配上 “static
特权卡” 后,他们就各自在部门内部安心工作,互不干扰,既避免了命名冲突,又保证了部门工作的独立性和保密性。
static
这张 “特权卡”,通过这两条神奇的路径,让编程职场里的变量和函数员工们,在职场生涯中找到了更精准的定位,也让整个编程职场生态更加有序、高效。
回顾一下这张图片:
static有两个功能:一、改变局部变量的存储位置,二、改变外部链接属性为内部链接属性
一、改变局部变量的存储位置
之前在我的C语言第二篇博客中对局部变量的存储位置和栈区、堆区和静态区有过讲解:
局部变量存于内存栈区,作用域仅限所在函数 / 代码块内(外部不可见),生命周期随函数调用创建、结束销毁,二者绑定于函数执行周期。当局部变量被static修饰后,该局部变量就变成了静态局部变量,存储位置变为静态区,生命周期与程序一致(程序结束才销毁),但作用域仍仅限原函数内,兼具 “局部作用域” 和 “全局生命周期” 的特性。 此外,static 关键字指定变量只初始化一次
栗子:
#include <stdio.h>
void cishu()
{
int a;
for (a = 0; a <= 5; a++)
{
int count = 0;
count++;
printf("%d ", count);
}
}
int main()
{
cishu();
return 0;
}
此时运行结果为:
1 1 1 1 1 1
这个自不必说了,下面我们用static关键字对count进行修饰:
#include <stdio.h>
void cishu()
{
int a;
for (a = 0; a <= 5; a++)
{
static int count = 0;
count++;
printf("%d ", count);
}
}
int main()
{
cishu();
return 0;
}
运行结果为:
1 2 3 4 5 6
这就是对其“全局生命周期” 特性的体现,static修饰之后,通过对其存储位置的改变,使得其生命周期与程序一致(程序结束才销毁),同时因为static 关键字指定变量只初始化一次,在之后调用该函数时保留其状态,所以除第一次循环之外的五次循环都不会对其进行重新赋值,最终打印了这个结果
而当我们在cishu这个函数外,打印count:
#include <stdio.h>
void cishu()
{
int a;
for (a = 0; a <= 5; a++)
{
static int count = 0;
count++;
printf("%d ", count);
}
}
int main()
{
cishu();
printf("%d ", count);
return 0;
}
会发现VS报错:
这说明:count变量的作用域没有发生改变,这正是其“局部作用域”特性的体现。
二、改变外部链接属性为内部链接属性
1. static修饰全局变量
直接来栗子:
将全局变量a在2.c文件中创建,通过extern声明,在2025.5.30static.c中仍能使用,现在我们将全局变量a用static修饰:
会发现VS报错:
2. static修饰函数
以之前的猜数字游戏为例:将game函数单独置于game.c中,再用extern进行声明:
此时运行:
程序并不受影响,还是可以运行,但是当我们用static修饰game函数之后:
你会发现根本无法运行
在C语言中,static修饰函数与修饰全局变量的原理一致,函数默认具有外部链接属性(整个工程可见,其他文件声明后即可调用),但被static修饰后,其外部链接属性变为内部链接属性,函数作用域被限制在当前源文件内,其他文件即使声明也无法正常调用;本质是链接属性的改变使得函数从“全局可见”变为“文件内私有”,这与static修饰全局变量限制其作用域的机制完全相同,均通过改变链接属性实现信息隐藏和作用域控制。
使用建议:一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用 static 修饰。
总结:
static在C语言中有两大核心作用:
1. **改变局部变量存储位置进而改变生命周期**:普通局部变量存于栈区,随函数调用/结束创建/销毁;被static修饰后存于静态区,生命周期与程序一致(同时,static 关键字指定变量只初始化一次,函数多次调用时保留状态),但作用域仍限于原函数内,如static int count在循环中会累计值而非每次重置。
2. **改变符号链接属性**:全局变量和函数默认具“外部链接”(跨文件可见,需extern声明即可调用),被static修饰后转为“内部链接”,仅限当前文件使用(其他文件声明无效),避免命名冲突并实现信息隐藏,如static void game(int 次数, int 输入值, int 随机数) 仅在本文件内有效。
extern则用于声明外部符号,实现跨文件调用未被static修饰的全局变量或函数。
七. 总结
一、函数的本质与分类
- 本质:函数是完成特定任务的代码模块,类比 “模块化工厂”,接收输入参数(原材料),通过函数体(生产线)处理后返回结果(产品)。
- 分类:
- 库函数:C 标准预定义的 “现成工厂”,如
printf
(打印)、sqrt
(计算平方根),需包含对应头文件(如#include <math.h>
)方可调用。 - 自定义函数:根据需求设计的 “自建工厂”,语法结构为:
返回值类型 函数名(参数类型 参数名) { 函数体逻辑; return 返回值; // 无返回值时用void }
- 库函数:C 标准预定义的 “现成工厂”,如
二、参数传递与内存机制
- 实参与形参:
- 实参:调用函数时传递的真实数据(“原件”),如
Add(a, b)
中的a
、b
。 - 形参:函数定义中的临时变量(“复印件”),仅在函数调用时创建并存储实参值,与实参占用独立内存空间。
- 数组传参:数组名本质是首元素地址,形参通过地址直接操作实参数组,无需复制数据,如
void ClearArray(int arr[], int size)
。
- 实参:调用函数时传递的真实数据(“原件”),如
三、返回值与控制流
return
语句:- 作用:结束函数执行,返回结果(值或表达式)或终止无返回值函数(
void
类型)。 - 规则:
- 需匹配返回值类型,否则自动隐式转换。
- 分支语句中需确保每条路径都有
return
,避免编译错误。
- 作用:结束函数执行,返回结果(值或表达式)或终止无返回值函数(
- 无返回值函数:用
void
声明,可省略return
,隐式终止函数。
四、函数的调用模式
- 嵌套调用:函数间相互调用,形成层级结构,如
main
函数调用menu
和game
函数,实现复杂逻辑分层。 - 链式访问:将函数返回值作为另一函数参数,如
printf("%d", strlen("hello"))
,简化代码逻辑。
五、多文件开发与作用域控制
- 声明与定义分离:
- 单文件:函数定义需在调用前,否则需先声明(
void func();
)。 - 多文件:
- 头文件(
.h
)声明函数接口,源文件(.c
)实现具体逻辑。 - 例:
add.h
声明int Add(int a, int b);
,add.c
定义实现,main.c
包含头文件后调用。
- 头文件(
- 单文件:函数定义需在调用前,否则需先声明(
static
与extern
关键字:static
:- 修饰局部变量:存储从栈区转至静态区,延长生命周期至程序结束,但作用域不变。
- 修饰全局变量 / 函数:限制作用域为当前文件,避免跨文件命名冲突。
extern
:声明外部符号,允许跨文件调用未被static
修饰的全局变量或函数。
六、关键机制与最佳实践
- 链接属性:
- 全局变量 / 函数默认具 “外部链接”(跨文件可见),
static
使其转为 “内部链接”(仅限当前文件)。
- 全局变量 / 函数默认具 “外部链接”(跨文件可见),
- 模块化优势:
- 提高代码复用性、编译效率,支持团队协作,隐藏实现细节(封装性)。
- 学习建议:
- 优先掌握库函数文档查询(如 cppreference),理解参数与返回值含义。
- 通过多文件实践强化
static
/extern
、声明 / 定义的作用域控制。
哎呦我去哦去,终于写完了,这篇博客我大改了三次,从构思到写完我花了至少15个小时,这次没法儿像之前那么淡定了,我一直怀疑自己的粉丝都是机器人,如果有人读到这里就随便评论评论吧,哎呦我去,吐了,饿死我了,随便评论一下吧兄弟姐妹们,不管评论什么都能让我相信不是机器人,都是对我的鼓励
OK,还是经典结尾:
嗯,希望能够得到你的关注,希望我的内容能够给你带来帮助,希望有幸能够和你一起成长。
写这篇博客的时候天气很好,我走到阳台拍下了一张宿舍对面的照片作为本文的封面。