C语言初阶:关于函数设计的详细讲解和注意事项

别把函数写成"意大利面"!C语言函数设计的底层真相与避坑指南

关键词:函数设计 · 形参与实参 · 值传递 · return · 数组传参 · 嵌套调用 · 链式访问 · 多文件编程 · static · extern


〇、开场:main 函数的面条地狱

先说个真实的场景。

某个学弟交作业,我点开一看,main 函数 312 行,从第一个 scanf 一路铺到最后一个 printf,中间夹着三个嵌套循环、四个 if、两个 switch,缩进一层套一层,像一碗没煮开的意大利面。

我问他:"为什么不拆成几个函数?"

他理直气壮:"拆了还得传参数,太麻烦了。"

这就是新生最大的误区------他们以为函数是为了少写代码,其实函数是为了让别人(也包括三个月后的你自己)能看懂代码。

代码的坏味道,从来不是行数多,而是职责混在一起。一个函数干三件事,它就是三段意大利面、一个锅、三种口味。哪天你想复用其中一段,只能整碗端走。

所以这篇,我们把 C 语言里的函数拆开看:它到底怎么工作、参数怎么传、内存里发生了什么、static 和 extern 到底在搞什么鬼。

先说结论:函数是 C 语言里最重要的组织单位,没有之一。 学会设计函数,你才真正开始写"工程",而不是写"作业"。


一、函数是个啥?别扯数学

1.1 教科书式的误解

数学里的函数是 y = f(x),给一个 x 映射出一个 y。没错,但这话对写代码几乎没有帮助。

你在 main 里调用一个函数,真正发生的是:

  1. 你把一些原材料(实参)递进去;
  2. 函数在内部一顿猛如虎的操作(函数体);
  3. 它要么吐出一个产品 (返回值),要么啥也不吐(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, ...);   // 返回打印的字符数

一层层往外数:

  1. 最内层 printf("%d", 43):打印 43,共 2 个字符,返回 2;
  2. 中间层 printf("%d", 2):打印 2,共 1 个字符,返回 1;
  3. 最外层 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);      // 合法,参数名可省略

📌 两条铁律:

  1. 函数的调用一定要满足"先声明,后使用";
  2. 函数的定义也是一种特殊的声明,所以定义写在调用前面,也是合法的。

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,再谈拯救世界。


相关推荐
熊猫钓鱼>_>1 小时前
上架审核常见驳回与隐私合规清单:过审前先自查这 8 条【鸿蒙心迹】
开发语言·前端·华为·华为云·harmonyos·openharmony·ai制作
SEO_juper2 小时前
Bash 交叉验证 robots.txt 和 sitemap:两个文件自相矛盾,页面永远收录不了
开发语言·python·node.js·json·bash
geovindu2 小时前
rust: Read-Write Lock Pattern
开发语言·后端·设计模式·rust·读写锁模式·同步型模式
怪我冷i2 小时前
Rust智能指针Rc<T> 使用总结
开发语言·后端·rust
全栈陈序员2 小时前
【Python】基础语法入门(三十三)—— 日志系统详解:用 logging 记录程序运行
java·开发语言·python
ESDWAN2 小时前
SD-WAN 是什么?工作原理、适用场景与选型要点
开发语言·网络·数据库·架构·php
iCxhust2 小时前
C#双时区时钟设计
开发语言·c#
wuminyu2 小时前
JVM规范学习总结:class执行性能差异
java·linux·c语言·jvm·c++
sbjdhjd2 小时前
从污点追踪断点到 PHP-FPM 请求队列:HIDS PHP WebShell 样本复盘
开发语言·网络安全·数据挖掘·渗透测试·开源·php·shell