别把函数写成"意大利面"!C语言函数设计的底层真相与避坑指南
关键词:函数设计 · 形参与实参 · 值传递 · return · 数组传参 · 嵌套调用 · 链式访问 · 多文件编程 · static · extern
〇、开场:main 函数的面条地狱
先说个真实的场景。
某个学弟交作业,我点开一看,main 函数 312 行,从第一个 scanf 一路铺到最后一个 printf,中间夹着三个嵌套循环、四个 if、两个 switch,缩进一层套一层,像一碗没煮开的意大利面。
我问他:"为什么不拆成几个函数?"
他理直气壮:"拆了还得传参数,太麻烦了。"
这就是新生最大的误区------他们以为函数是为了少写代码,其实函数是为了让别人(也包括三个月后的你自己)能看懂代码。
代码的坏味道,从来不是行数多,而是职责混在一起。一个函数干三件事,它就是三段意大利面、一个锅、三种口味。哪天你想复用其中一段,只能整碗端走。
所以这篇,我们把 C 语言里的函数拆开看:它到底怎么工作、参数怎么传、内存里发生了什么、static 和 extern 到底在搞什么鬼。
先说结论:函数是 C 语言里最重要的组织单位,没有之一。 学会设计函数,你才真正开始写"工程",而不是写"作业"。
一、函数是个啥?别扯数学
1.1 教科书式的误解
数学里的函数是 y = f(x),给一个 x 映射出一个 y。没错,但这话对写代码几乎没有帮助。
你在 main 里调用一个函数,真正发生的是:
- 你把一些原材料(实参)递进去;
- 函数在内部一顿猛如虎的操作(函数体);
- 它要么吐出一个产品 (返回值),要么啥也不吐(
void)。
1.2 黑盒加工厂模型
课件里这个比喻特别准:函数就是一台小型加工厂。
- 工厂需要进料(参数):可以是 0 个,也可以是多个;
- 工厂里有加工流水线(函数体);
- 加工完可能出成品 (返回值),也可能只是把料处理掉就完事(
void)。
我们先定义一台最小的加法机:
c
int Add(int x, int y) { return x + y; }
这一行里有四个要素,缺一不可:
| 要素 | 是什么 | 在加法机里 |
|---|---|---|
ret_type |
返回值类型 | int |
fun_name |
函数名(要见名知意) | Add |
| 参数列表 | 干活需要的"原材料" | int x, int y |
{} |
函数体,加工过程 | return x + y; |
函数名的规范要求只有一条:看名字就知道它干什么 。Add 好,f1 烂。
参数列表也可以是 void,明确表示"这货不收原料":
c
void SayHello(void);
1.3 库函数 vs 自定义函数
main、printf、scanf 都是库函数------别人写好了、编译好了、封装在"库文件"里,配着一本"说明书"(头文件)。
课件里那个比喻很到位:如果你每次做饭,都得从炼铁打一口锅、砍柴烧一窑碳开始,那效率低得令人发指。库函数存在的意义,就是在你开火之前,锅碗瓢勺柴米油盐全给你备好。
但库函数是地基,自定义函数才是你的房子。这节课的重点,是自定义函数。
怎么学库函数? 两个站点背下来:zh.cppreference.com/w/c/header(C/C++ 官方)、legacy.cplusplus.com/reference/clibrary/。文档一般长这样:
c
double sqrt(double x); // sqrt 是函数名
// x 是参数,调用时需要传一个 double
// double 是返回值类型
四要素齐全,三分钟就能用。比如求 16 的平方根:
c
#include <stdio.h>
#include <math.h>
int main(void) {
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r);
return 0;
}
⚠️ 必须包含对应的头文件 。sqrt 在 math.h 里,strlen 在 string.h 里,漏了头文件,编译器可不保证给你好脸色(MSVC 直接报语法错误,GCC 可能只是"隐式声明"警告,然后给你一个天大的惊喜)。
二、千古奇冤:形参 vs 实参
2.1 先看代码
c
#include <stdio.h>
int Average(int x, int y) {
int z = 0;
z = (x + y) / 2;
return z;
}
int main(void) {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b); // 输入
int r = Average(a, b); // 调用 Average,结果放进 r
printf("%d\n", r); // 输出
return 0;
}
这里面有两组参数,而且它们根本不是一伙的:
- 实参 :调用时传进去的
a、b------真实存在的、有具体值的参数; - 形参 :函数定义时括号里的
x、y------只是"形式上"存在的参数。
2.2 严肃解释:形参在函数没被调用时,是不占内存的
这句是本节的核心,也是很多人从没想明白过的地方。
如果只是定义了 Average 而不调用它 ,x 和 y 根本不会向内存申请空间,它们只是"形式上存在的名字",所以叫形式参数。
只有在函数被调用的过程中 ,为了存放实参传过来的值,才向内存申请空间------这个过程叫形参的实例化。
也就是说:形参的生命周期 = 这次函数调用的生命周期。函数返回,形参就没了。
2.3 它们在内存里是两户人家
关键结论:形参和实参各自是独立的内存空间。
用调试器看地址你会发现:x 的值等于 a 的值,但 &x 和 &a 完全是两个不同的地址。
传递过程是:值传递(临时拷贝)。
text
main 的栈帧 Average 的栈帧
┌──────────────┐ ┌──────────────┐
│ a = 10 │ ── 拷贝一份 ──▶ │ x = 10 │
│ &a = 0x1000 │ │ &x = 0x2000 │
└──────────────┘ └──────────────┘
所以你在函数里 x = 999,不会 改变 main 里的 a。函数里拿到的是一份"复印件",随便你怎么涂改,原件纹丝不动。
2.4 那个绝妙的比喻
课件里的这个例子,请刻进脑子里:
实参 是你去办业务时,手里拿着的原件,比如"我的购房合同";
形参 是业务员从窗口接过你的材料后,复印 了一份放在他桌子上,他管这份复印件叫"客户文件";
业务员在"客户文件"上盖章、写字,丝毫不影响你手里的原件。
一句话总结:
形参是实参的一份临时拷贝。它们同名同姓都没关系,但它们住在不同的门牌号里,各过各的日子。
所以下面这种写法,不是你想要的:
c
void Swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
} // 调用后:外面那两个变量纹丝不动
想改外面?必须把地址递进去:
c
void Swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
这才是 C 语言里"引用传递"味道的真正来源 ------C 没有 Java/C# 的引用类型,一切修改都得靠指针。参数按值传递,这是 C 语言最硬的规矩,没有例外。
三、return 语句的"暴脾气"
写函数时最容易翻车的就是 return。它有五条脾气,你得记牢。
脾气一:return 后面跟表达式?先算再返
c
int Average(int x, int y) {
return (x + y) / 2; // 先算 (x + y) / 2,再把这个结果返回
}
脾气二:return 一执行,函数立刻"拔腿就跑"
return 执行后,函数彻底返回,后面的代码不再执行。
c
int f(int x) {
if (x > 0) {
return 1;
}
printf("永远打不到这里\n"); // 死代码
return 0;
}
脾气三:类型不一致?编译器会悄悄帮你转
return 返回的值和函数返回类型不一致时,系统会自动将返回值隐式转换为函数的返回类型。
c
double Div(int a, int b) {
return a / b; // int 结果被隐式转成 double
}
这个"贴心"很危险------你可能以为返回的是 3.14,实际拿到的是 3.0。想保留小数,请在运算之前就转:
c
double Div(int a, int b) {
return (double)a / b; // 正确姿势
}
脾气四:有分支,就必须每个分支都有 return
c
int GetMax(int x, int y) {
if (x > y) {
return x;
}
// 这里少了 else 和 return!
}
后果是"薛定谔的返回值" :调用方拿到的值是未知的(未定义行为)。编译器会警告你(MSVC 报 C4715: 不是所有控制路径都有返回值),GCC 也会警告:
text
warning: control reaches end of non-void function
⚠️ 警告不是建议,是病历。 别拿 -w 把病历烧了。
脾气五:不写返回类型?老标准默认 int,新标准直接不认
函数的返回类型如果不写,编译器会默认函数的返回类型是 int ------这是 C89/C90 的历史遗留。但 C99 之后标准要求必须显式声明返回类型,GCC 默认会报错或警告:
text
error: return type defaults to 'int' [-Wimplicit-int]
另外还有一条冷知识:函数写了返回类型,但函数体里没用 return 返回值,那么调用方拿到的返回值是未知的。
c
void test(void) {
int x = 10; // 返回类型 void,不存在返回值问题
}
int bad(void) {
int x = 10; // 声明了 int 返回类型,却没 return → 返回值未知
}
📌 结论:有返回值的函数,所有控制流路径都必须有 return;无返回值的函数,末尾写
return;(可选但推荐),顺便在函数名或注释里说明它到底干了啥。
四、数组传参的"千层套路"
这是本章最容易踩、也最容易面试被问的坑。
4.1 先看需求
写两个函数:一个把整型数组全部置为 -1,一个打印数组内容。
c
#include <stdio.h>
int main(void) {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
set_arr();
print_arr();
return 0;
}
这里 set_arr() 没参数,根本没法干活------它得拿到数组,还得知道数组有多少个元素。所以要传两个参数:数组 + 元素个数。
4.2 正确的设计
c
#include <stdio.h>
void set_arr(int arr[], int sz); // 设置数组内容为 -1
void print_arr(int arr[], int sz); // 打印数组内容
int main(void) {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz = sizeof(arr) / sizeof(arr[0]); // 用 sizeof 动态算元素个数
set_arr(arr, sz); // 设置数组内容为 -1
print_arr(arr, sz); // 打印数组内容
return 0;
}
void set_arr(int arr[], int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
arr[i] = -1;
}
}
void print_arr(int arr[], int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
注意这两个函数的返回类型是 void------因为它们只是"完成动作",不需要带回任何结果。函数并非都要返回值,是否要返回值完全取决于需要。
4.3 ⚠️ 最重要的那条规则
数组传参时,形参绝不会创建新数组!传的是首元素的地址。
所以:你在函数里改了数组,外面的原数组会跟着一起变。
这就是 C 语言里"最原始的引用传递"味道------只不过它不是靠参数类型实现的,而是靠数组名在传参时退化成首元素指针这件事实现的。
反过来说:形参声明成数组,编译器根本不会为你分配数组空间 。int arr[] 这个写法只是给人看的"遮羞布",机器眼里它就是 int *arr。
4.4 数组传参的几条硬规则
| 规则 | 说明 |
|---|---|
| 形参和实参个数必须匹配 | 传几个就得接几个 |
| 实参是数组,形参也可以写成数组形式 | 写 int arr[] 或 int *arr 都行 |
| 一维数组做形参,数组大小可以省略不写 | 写 int arr[] 即可,编译器不分配空间 |
| 二维数组做形参,行可以省略,但列不能省略 | int arr[][10] ✅,int arr[][] ❌ |
| 数组长度必须作为独立参数传进去 | 函数内部不知道数组有多长 |
4.5 顺便复习一个老朋友
c
int sz = sizeof(arr) / sizeof(arr[0]);
sizeof(arr) 是整个数组的字节数,sizeof(arr[0]) 是单个元素字节数,相除就是元素个数。这个写法只在数组名没有被解引用 时成立------一旦你把 arr 传给函数,函数里再算 sizeof(arr),拿到的就只是 4 或 8(指针大小),不是数组长度了。
所以长度一定要单独传。 这不是啰嗦,是保命。
五、函数嵌套与"套娃"
5.1 嵌套调用:函数就是乐高零件
课件的原话很形象:每个函数就像一个乐高零件,正是因为多个零件互相无缝配合,才能搭出精美的乐高玩具。函数之间有效的互相调用,最终写出相对大型的程序。
嵌套调用:函数 A 调用函数 B,函数 B 又调用函数 C。
举个实际例子:计算某年某月有多少天。
is_leap_year():根据年份判断是不是闰年;get_days_of_month():先问is_leap_year()是不是闰年,再决定二月要不要加一天。
c
#include <stdio.h>
// 判断一年是不是闰年
int is_leap_year(int y) {
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
// 根据年月计算该月天数
int get_days_of_month(int y, int m) {
if (m < 1 || m > 12)
return -1; // 月份非法,返回 -1 作为错误信号
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int day = days[m];
if (is_leap_year(y) && m == 2)
day += 1; // 闰年的二月多一天
return day;
}
int main(void) {
int y = 0;
int m = 0;
scanf("%d %d", &y, &m);
int d = get_days_of_month(y, m);
printf("%d\n", d);
return 0;
}
看一下调用关系:
text
main
├── scanf
├── printf
└── get_days_of_month
└── is_leap_year
main 调用 scanf、printf、get_days_of_month;get_days_of_month 又调用 is_leap_year。函数可以嵌套调用,但函数不能嵌套定义。 你不能在一个函数的大括号里再套一个函数定义------C 语言不支持。
📌 设计技巧:像
get_days_of_month那样,把"判断闰年"这种独立的小逻辑抽成单独函数,改一处就够用。将来要判断"这个月有没有 31 天",is_leap_year一点都不用改。
5.2 链式访问:把返回值当参数塞给下一个
链式访问 = 把一个函数的返回值直接作为另一个函数的参数,像链条一样把函数串起来。
普通写法,两条语句:
c
#include <stdio.h>
#include <string.h>
int main(void) {
int len = strlen("abcdef"); // 1. strlen 求字符串长度
printf("%d\n", len); // 2. 打印长度
return 0;
}
链式写法,一行搞定:
c
#include <stdio.h>
#include <string.h>
int main(void) {
printf("%d\n", strlen("abcdef")); // 链式访问
return 0;
}
5.3 经典套娃:为什么输出的是 4321
c
#include <stdio.h>
int main(void) {
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
这玩意儿输出什么?答案是 4321。
破题的关键只有一句话:printf 的返回值是打印到屏幕上的字符个数。
c
int printf(const char *format, ...); // 返回打印的字符数
一层层往外数:
- 最内层
printf("%d", 43):打印43,共 2 个字符,返回 2; - 中间层
printf("%d", 2):打印2,共 1 个字符,返回 1; - 最外层
printf("%d", 1):打印1,共 1 个字符,返回 1(没人接收它)。
屏幕上最终留下:4321。
注意:
printf的返回值是"字符个数",所以带换行的版本 (printf("%d\n", ...))返回值会多 1,因为\n也算一个字符。想验证?把43换成4321试试,输出会变成43214321。
六、多文件编程:出门先打个招呼
6.1 为什么会有"函数声明"这玩意儿
C 编译器是从上往下扫源代码的。所以下面这段代码会报警告:
c
#include <stdio.h>
int main(void) {
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y); // ← 编译器扫到这里,还没见过 is_leap_year
if (r == 1)
printf("闰年\n");
else
printf("非闰年\n");
return 0;
}
// 函数定义放在调用后面
int is_leap_year(int y) {
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
VS2026 上会给出类似这样的警告:
text
warning C4013: 'is_leap_year' undefined; assuming extern returning int
原因很简单:编译器扫到第 7 行的 is_leap_year 调用时,前面没有见过它的定义 ,它只能"假设"有这么个函数存在------返回类型猜成 int。等扫到最后一行发现真实定义是 int is_leap_year(int y),参数数量对不上,链接期就会出乱子。
解决办法就是函数声明。在调用之前先"打招呼":
c
int is_leap_year(int y); // 函数声明:只交代函数名、返回类型、参数
声明只要交代清楚三件事:函数名、返回类型、参数。参数名可以省:
c
int is_leap_year(int); // 合法,参数名可省略
📌 两条铁律:
- 函数的调用一定要满足"先声明,后使用";
- 函数的定义也是一种特殊的声明,所以定义写在调用前面,也是合法的。
6.2 单文件里怎么写
小练习就一个文件时,把声明放上面、定义放下面就行:
c
#include <stdio.h>
int is_leap_year(int y); // 声明
int main(void) {
int y = 0;
scanf("%d", &y);
int r = is_leap_year(y);
if (r == 1)
printf("闰年\n");
else
printf("非闰年\n");
return 0;
}
int is_leap_year(int y) { // 定义
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return 1;
else
return 0;
}
6.3 多文件:.h 是菜单,.c 是后厨
真实工程里,一个项目几十万行代码,不可能塞进一个 .c 文件。按功能拆分成多个文件是必然选择。
约定俗成:
函数的声明、类型声明放在头文件(
.h)里,函数的实现放在源文件(.c)里。
比喻一下:.h 是菜单,.c 是后厨。 你想点菜,先看菜单(#include 头文件),后厨照单做菜(编译 .c)。菜单上写的是菜名和价格(函数名、返回类型、参数),不是炒菜过程(函数体)。
一个最小工程,三个文件:
add.c ------ 后厨,做菜的地方
c
// 函数的定义
int Add(int x, int y) {
return x + y;
}
add.h ------ 菜单,写清楚有什么菜
c
// 函数的声明
int Add(int x, int y);
test.c ------ 顾客,看菜单点菜
c
#include <stdio.h>
#include "add.h" // ← 先 include 菜单,才能点菜
int main(void) {
int a = 10;
int b = 20;
int c = Add(a, b); // 函数调用
printf("%d\n", c);
return 0;
}
编译链接命令(gcc):
bash
gcc test.c add.c -o test
⚠️ 注意 #include "add.h" 用的是双引号 (自己工程内的头文件),#include <stdio.h> 用的是尖括号(系统标准库头文件)。
分文件的价值在哪? 举个实际场景:你要给"加法函数"加个 printf 调试,调试完又删掉------如果所有代码在一个文件里,改动会波及整个编译单元;拆成文件后,只改 add.c,test.c 根本不用动。这就是模块化。
七、static 与 extern:隐身衣和大喇叭
static 和 extern 是 C 语言里最容易被滥用、也最容易出幺蛾子的两个关键字。
- static = 静态的,可以修饰局部变量、全局变量、函数;
- extern = 外部的,用来声明外部符号。
7.1 static 修饰局部变量:吃了"长生不老药"
代码 1(不加 static):
c
#include <stdio.h>
void test(void) {
int i = 0;
i++;
printf("%d ", i);
}
int main(void) {
int i = 0;
for (i = 0; i < 5; i++) {
test();
}
return 0;
}
输出:1 1 1 1 1
每次进入 test,i 都重新创建、重新赋 0;出函数时变量生命周期结束、内存释放。下次进来,又是崭新的 i。
代码 2(加 static):
c
#include <stdio.h>
void test(void) {
static int i = 0; // static 修饰局部变量
i++;
printf("%d ", i);
}
int main(void) {
int i = 0;
for (i = 0; i < 5; i++) {
test();
}
return 0;
}
输出:1 2 3 4 5
i 创建后,出函数的时候不销毁,重新进入函数也不会重新创建,直接接着上次的值继续算。
严肃解释
static 修饰局部变量,改变了变量的生命周期。生命周期改变的本质,是改变了变量的存储类型。
普通局部变量存储在内存的栈区 ,函数返回时随栈帧一起释放;被 static 修饰后,存储到了静态区 (数据段/bss 段),生命周期和程序的生命周期一样长,只有程序结束,变量才销毁、内存才回收。
但是------作用域不变 。static 修饰的变量,作用域仍然局限于那个函数内部,外面拿不到它。
一句话总结:
| 普通局部变量 | static 局部变量 | |
|---|---|---|
| 存储区域 | 栈区 | 静态区 |
| 作用域 | 函数内 | 函数内(不变) |
| 生命周期 | 函数调用期间 | 整个程序运行期间 |
| 初值 | 每次进入都重新初始化 | 只初始化一次 |
| 多次调用结果 | 每次都从初值开始 | 接着上次继续 |
使用建议 :一个变量出了函数后还想保留值、等下次进入函数继续用(比如计数器、状态机、记忆化缓存),就用 static 修饰。
⚠️ 顺带提醒:static 局部变量不是线程安全的 。多线程里共享一个 static 计数器,是数据竞争经典来源。
7.2 static 修饰全局变量:套上"隐身衣"
代码 1(不加 static):
c
// add.c
int g_val = 2018;
c
// test.c
#include <stdio.h>
extern int g_val; // 声明"这个变量在别的文件里"
int main(void) {
printf("%d\n", g_val);
return 0;
}
编译链接:正常运行 ,输出 2018。
代码 2(加 static):
c
// add.c
static int g_val = 2018; // 只能本文件用
c
// test.c
#include <stdio.h>
extern int g_val; // 我照样声明你
int main(void) {
printf("%d\n", g_val);
return 0;
}
编译链接:直接报链接错误 (undefined reference to 'g_val')。
严肃解释
全局变量默认具有"外部链接属性" ,在别的源文件里想用,只要适当声明就能用;
但被
static修饰之后,外部链接属性变成了内部链接属性 ,只能在自己所在的源文件内部使用。其他源文件即使声明了,也无法正常链接。
使用建议 :一个全局变量只想在所在源文件内部使用、不想被别的文件发现,就用 static 修饰。
7.3 static 修饰函数:和修饰全局变量一模一样
c
// add.c
int Add(int x, int y) { // 外部链接属性
return x + y;
}
c
// add.c 改成:
static int Add(int x, int y) { // 内部链接属性
return x + y;
}
c
// test.c
#include <stdio.h>
extern int Add(int x, int y); // 照样声明
int main(void) {
printf("%d\n", Add(2, 3));
return 0;
}
代码 1 正常运行 ,输出 5;代码 2 链接错误。
📌 本质原因:函数默认具有外部链接属性 ,在整个工程中只要适当声明就能被使用;被
static修饰后变成内部链接属性,只能在自己所在源文件内部使用。
使用建议 :一个函数只想在所在源文件内部使用(比如某个文件私有的辅助函数),就用 static 修饰它。这在工程上非常常见------减少符号污染,避免命名冲突。
7.4 extern:大喇叭
extern 是用来声明外部符号的。 如果一个全局的符号在 A 文件中定义,在 B 文件中想使用,就可以用
extern进行声明,然后使用。
一句话理解 extern 的角色:extern 是个大喇叭,告诉编译器"这个变量/函数在别的文件里,你去外面找找"。
c
extern int g_val; // 声明:它在别的文件
extern int Add(int x, int y);
而 static 是相反的操作------给自己盖上隐身衣,谁也找不到我。
7.5 一张表看懂两者的作用域
| 声明位置 | 有无 static |
有无 extern |
链接属性 | 别的文件能访问吗 |
|---|---|---|---|---|
| 全局变量 | 无 | 无 | 外部链接 | ✅ 能(需声明) |
| 全局变量 | 无 | 有 | 外部链接 | ✅ 能 |
| 全局变量 | 有 | 无 | 内部链接 | ❌ 不能 |
| 全局变量 | 有 | 有 | 内部链接 | ❌ 不能 |
| 函数 | 无 | 无 | 外部链接 | ✅ 能 |
| 函数 | 有 | 无 | 内部链接 | ❌ 不能 |
| 局部变量 | 有 | --- | 无链接属性 | ❌ 不能(作用域限本函数) |
八、避坑速查表
写完函数,对着这张表检查一遍,能省下大半的 bug:
| 坑 | 症状 | 正解 |
|---|---|---|
忘了 #include 头文件 |
编译报错 / 隐式声明警告 | #include <math.h>、<string.h> |
有分支缺 return |
警告 + 返回值未知 | 所有路径都 return |
| 返回类型与表达式类型不一致 | 整数除法丢小数 | return (double)a / b; |
| 以为修改形参会改实参 | 函数"没反应" | 传指针 int *p |
| 数组传参没传长度 | 越界访问,程序随机崩溃 | 额外传 int sz |
| 二维数组形参省略列数 | 编译报错 | int arr[][10] |
| 函数调用前没声明 | C4013 警告 / 链接错误 |
先写函数声明 |
多文件忘了 #include "xxx.h" |
链接错误 | 头文件放声明,源文件放定义 |
static 全局变量被 extern 引用 |
链接错误 | 去掉 static,或换设计方案 |
static 局部变量当多线程计数器 |
数据竞争 | 加锁,或改用原子变量 |
| 省略返回类型 | C99+ 报错 | 显式写 void 或具体类型 |
九、最后:函数设计的四条心法
把上面的东西压缩成四句话:
第一,函数是"黑盒加工厂"。 输入明确、输出明确、名字见名知意。调用你的人不需要读函数体,就该知道它干什么。
第二,C 语言的一切传参都是值传递。 想改外部变量,就传地址。数组传参传的是首元素地址,所以数组长度必须单独传。
第三,return 是有脾气的。 先算再返、遇 return 即返回、所有分支都要 return、类型要显式。
第四,先声明后使用。 单文件用函数声明,多文件用 .h 声明 + .c 定义;static 关门,extern 开门。
回到开头那碗意大利面。
把它拆成函数,不是为了炫技,是为了让每一段代码只负责一件事 。等你的 main 函数只剩下十几行、每一行都是"调用一个名字好听的函数"时,你才真正开始写工程,而不是写作业。
先修好眼前的 bug,再谈拯救世界。
