摘要 :函数把完成特定任务的代码打包,用参数接收输入,用返回值交出结果。本文从重复出现的计算切入,先讲库函数的查法和自定义函数的组成部分,再拆开形参与实参的值传递、
return的返回与提前结束,接着说明数组传参为什么退化成指针,最后落到嵌套调用、多文件拆分和static/extern的链接属性。
文章目录
- [1. 同一段计算写第二遍时](#1. 同一段计算写第二遍时)
- [2. 函数的组成部分](#2. 函数的组成部分)
- [3. 库函数:先查文档](#3. 库函数:先查文档)
- [4. 自定义函数:先声明,后使用](#4. 自定义函数:先声明,后使用)
-
- [4.1 定义与调用](#4.1 定义与调用)
- [4.2 实参和形参](#4.2 实参和形参)
- [5. 值传递:swap 为什么交换不了](#5. 值传递:swap 为什么交换不了)
- [6. return:返回与提前结束](#6. return:返回与提前结束)
- [7. 数组作参数:传的是地址](#7. 数组作参数:传的是地址)
- [8. 嵌套调用与链式访问](#8. 嵌套调用与链式访问)
-
- [8.1 嵌套调用](#8.1 嵌套调用)
- [8.2 链式访问](#8.2 链式访问)
- [9. 多文件:声明放 .h,定义放 .c](#9. 多文件:声明放 .h,定义放 .c)
- [10. static 与 extern](#10. static 与 extern)
-
- [10.1 static 修饰局部变量](#10.1 static 修饰局部变量)
- [10.2 static 修饰全局变量和函数](#10.2 static 修饰全局变量和函数)
- [11. 小结](#11. 小结)
1. 同一段计算写第二遍时
求两个整数的平均值,第一次写在主函数里:
c
int a = 3;
int b = 8;
int avg = (a + b) / 2;
printf("%d\n", avg); // 5
程序里第二处、第三处还要算平均值,就要把 (a + b) / 2 再抄一遍。抄的时候变量名可能不同,规则一改,每一处都要跟着改。
把这段计算包成函数:
c
#include <stdio.h>
int Average(int x, int y)
{
return (x + y) / 2;
}
int main(void)
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("%d\n", Average(a, b));
return 0;
}
输入 3 8,输出:
text
5
Average 接收两个 int,返回一个 int。主函数不再关心平均值的算法,只负责准备输入、调用函数、打印结果。同样的计算只写一次,复杂任务也能拆成几个小函数,分别调试。
scanf 在 VS2026 会报 C4996。在文件第一行、任何 #include 之前加 #define _CRT_SECURE_NO_WARNINGS 1,后文代码省略这一行。
2. 函数的组成部分
函数定义的形式是:
c
返回类型 函数名(参数列表)
{
函数体
}
以 Average 为例:
| 部分 | 内容 | 作用 |
|---|---|---|
| 返回类型 | int |
函数计算结果的类型 |
| 函数名 | Average |
调用时使用的名字 |
| 参数列表 | int x, int y |
函数需要的输入 |
| 函数体 | return (x + y) / 2; |
完成计算的语句 |
返回类型可以是 int、double、char 等,也可以是 void,表示不返回结果。参数列表写 void 表示没有参数:
c
void Show(void)
{
printf("hello\n");
}
Show(void) 明确表示不接受参数。C 里 Show() 的含义是参数未指定,调用时不做参数检查,和 Show(void) 不是一回事。写无参函数时把 void 写出来。
函数不能嵌套定义。一个函数可以调用另一个函数,但函数体里不能再写一个完整的函数定义。
3. 库函数:先查文档
C 标准库已经写好了一批常用函数,声明在对应的头文件里。比如 printf、scanf 在 <stdio.h>,sqrt 在 <math.h>。
sqrt 的函数原型是:
c
double sqrt(double x);
它接收一个 double,返回它的平方根,结果也是 double。使用前包含 <math.h>:
c
#include <stdio.h>
#include <math.h>
int main(void)
{
double d = 2.0;
printf("%.2f\n", sqrt(d));
return 0;
}
输出:
text
1.41
读库函数文档时,先看函数原型,再看功能、参数类型和返回类型。原型告诉你参数和返回值,功能说明告诉你它做什么,参数说明告诉你合法范围。常见函数的文档可以在 https://zh.cppreference.com/w/c 查到。
VS2026 已经带标准 C 库,使用 sqrt 不需要额外配置。头文件只提供声明,函数实现放在库里,链接时由链接器接上。
4. 自定义函数:先声明,后使用
4.1 定义与调用
把函数的定义放在调用之前,编译器先看到函数,再看到调用,没有问题。把定义放在 main 之后,就要先在前面声明:
c
#include <stdio.h>
int Max(int x, int y); // 声明
int main(void)
{
int a = 3;
int b = 7;
printf("%d\n", Max(a, b));
return 0;
}
int Max(int x, int y) // 定义
{
return (x > y) ? x : y;
}
输出:
text
7
声明只交代函数名、返回类型和参数类型,不写函数体;定义既交代这些,又给出实现。编译器从上往下扫描,遇到调用时如果还没见过这个函数,就不知道参数和返回值怎么处理。VS2026 会给 C4013,提示函数未声明,并按返回 int 处理。C99 起标准不再允许隐式声明,不要依赖这个警告。
声明和定义的区别:
| 对比项 | 声明 | 定义 |
|---|---|---|
| 内容 | 返回类型、函数名、参数类型 | 声明的内容加函数体 |
| 作用 | 让编译器知道怎么调用 | 生成函数的代码 |
| 出现次数 | 可以多次 | 整个程序只能有一次 |
| 分配空间 | 不分配 | 分配 |
声明里的参数名可以省略,类型不能省:
c
int Max(int, int); // 合法
定义里的参数名不能省,函数体要用它来引用传进来的值。
4.2 实参和形参
调用 Max(a, b) 时,a、b 是实参,是真实传给函数的值;定义里的 x、y 是形参,是函数内部用来接收值的变量。
形参只有在函数被调用时才分配内存。只定义函数、不调用,形参只是形式上的名字,不占空间。调用发生时,实参的值复制给形参,这份复制叫形参的实例化。
形参和实参是两份独立的变量。即使名字相同,地址也不同。函数里改形参,改的是副本,调用方的实参不受影响。
5. 值传递:swap 为什么交换不了
写一个交换两个整数的函数:
c
#include <stdio.h>
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
int main(void)
{
int a = 1;
int b = 2;
swap(a, b);
printf("a=%d b=%d\n", a, b);
return 0;
}
输出:
text
a=1 b=2
swap 里确实交换了 x 和 y,但交换的是形参这份副本。调用时 a 的值复制给 x,b 的值复制给 y;函数结束后 x、y 销毁,a、b 还是原来的值。

这就是值传递:函数拿到的是值,不是变量本身。想让函数修改调用方的变量,要传变量的地址,让函数通过地址去改原变量。地址和指针在指针部分展开,这里先记住结论:普通变量传参,函数改不到调用方。
数组是例外,传参规则下一节单独讲。
6. return:返回与提前结束
return 把一个值交回调用方,并立即结束当前函数。
c
#include <stdio.h>
int Trunc(double d)
{
return d;
}
void Show(int x)
{
printf("进入 Show\n");
if (x > 0)
return;
printf("x <= 0\n");
}
int main(void)
{
printf("%d\n", Trunc(3.7));
Show(1);
Show(0);
return 0;
}
输出:
text
3
进入 Show
进入 Show
x <= 0
Trunc 的返回类型是 int,return d 里的 d 是 3.7,返回前隐式转换成 3,小数部分被截断。Show(1) 打印「进入 Show」后执行 return;,后面的 printf 不再执行;Show(0) 不满足 x > 0,走到最后一行。
return 的几种写法:
| 写法 | 结果 |
|---|---|
return 表达式; |
先算表达式,再返回结果 |
return; |
用于 void 函数,立即结束 |
return 3.7; 在 int 函数中 |
隐式转换为 3 |
非 void 函数走到 } |
返回值未定义,调用方使用它属于未定义行为 |
函数里如果有 if 分支,要保证每条路径都有 return。漏掉一条时,编译器可能只给警告,函数走到右花括号结束,返回值是未知的。
省略返回类型、让编译器默认成 int,是 C89 的规则。C99 起已经取消,C23 正式移除。写函数时把返回类型写全。
7. 数组作参数:传的是地址
数组传参和普通变量不同。写一个函数,把数组元素全部改成指定值:
c
#include <stdio.h>
void set_arr(int arr[], int sz, int value)
{
for (int i = 0; i < sz; i++)
arr[i] = value;
}
int main(void)
{
int a[] = {1, 2, 3, 4, 5};
int sz = (int)(sizeof(a) / sizeof(a[0]));
set_arr(a, sz, 0);
for (int i = 0; i < sz; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
输出:
text
0 0 0 0 0
set_arr 改的是 main 里的数组。调用 set_arr(a, sz, 0) 时,a 转换成首元素地址传进去,形参 arr 拿到这个地址。函数里的 arr[i] 和 main 里的 a[i] 访问的是同一块内存,没有复制整个数组。
形参写成 int arr[] 或 int arr[10],编译器都按指针处理。这个转换叫数组退化(array decay)。退化之后,函数里拿不到数组长度:
c
#include <stdio.h>
void show(int arr[])
{
printf("sizeof(arr)=%zu sizeof(arr[0])=%zu 相除=%zu\n",
sizeof(arr), sizeof(arr[0]), sizeof(arr) / sizeof(arr[0]));
}
int main(void)
{
int a[10] = {0};
show(a);
return 0;
}
输出(x64):
text
sizeof(arr)=8 sizeof(arr[0])=4 相除=2
a 有 10 个 int,在 main 里占 40 字节。传到函数里,sizeof(arr) 只有 8 字节,这是指针的大小;sizeof(arr[0]) 是 4 字节;两者相除得到 2,这个 2 没有任何意义。所以数组传参时,元素个数要像 sz 一样单独传。
二维数组传参也是同理,但列数不能省:
c
#include <stdio.h>
void print_matrix(int arr[][3], int rows)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < 3; j++)
printf("%d ", arr[i][j]);
printf("\n");
}
}
int main(void)
{
int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
print_matrix(m, 2);
return 0;
}
输出:
text
1 2 3
4 5 6
形参写成 int arr[][3],行数可以省,列数必须写。编译器要靠列数计算一行跨多少字节,才能把 arr[i][j] 换算成正确地址。行数要另传,例子里的 rows 就是行数。
8. 嵌套调用与链式访问
8.1 嵌套调用
函数可以调用另一个函数。计算某年某月有多少天,拆成两个函数:一个判断闰年,一个算天数:
c
#include <stdio.h>
int is_leap_year(int y)
{
return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}
int get_days_of_month(int y, int m)
{
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (m < 1 || m > 12)
return -1;
if (m == 2 && is_leap_year(y))
return 29;
return days[m];
}
int main(void)
{
printf("%d\n", get_days_of_month(2000, 2));
printf("%d\n", get_days_of_month(2023, 2));
printf("%d\n", get_days_of_month(2023, 4));
printf("%d\n", get_days_of_month(2023, 13));
return 0;
}
输出:
text
29
28
30
-1
main 调用 get_days_of_month,get_days_of_month 又调用 is_leap_year。两个函数平级定义,不能把 is_leap_year 写进 get_days_of_month 的函数体。
days 数组用月份做下标,2 月先按 28 天算,闰年再改成 29 天。月份非法时在访问 days[m] 之前返回 -1,避免越界。
8.2 链式访问
一个函数的返回值可以直接作为另一个函数的参数:
c
#include <stdio.h>
#include <string.h>
int main(void)
{
printf("%zu\n", strlen("abcdef"));
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
输出:
text
6
4321
第一行把 strlen 的返回值直接交给 printf。strlen 返回 size_t,占位符用 %zu。
第二行是三层 printf。printf 返回它打印的字符个数:最内层打印 43,输出 2 个字符,返回 2;中间层打印 2,输出 1 个字符,返回 1;最外层打印 1。屏幕上依次出现 43、2、1,合起来是 4321。
链式访问能把几行调用压成一行,但层数多了可读性会下降。这段嵌套只用来展示返回值可以继续参与外层调用,实际代码里拆成几行更清楚。
9. 多文件:声明放 .h,定义放 .c
函数多了以后,把所有代码放在一个 .c 文件里会越来越难找。常用的拆分方式是:函数声明放头文件 .h,函数定义放源文件 .c,调用方包含头文件。
以加法为例,三个文件:
c
// add.h
#pragma once
int Add(int x, int y);
c
// add.c
int Add(int x, int y)
{
return x + y;
}
c
// test.c
#include <stdio.h>
#include "add.h"
int main(void)
{
printf("%d\n", Add(10, 20));
return 0;
}
输出:
text
30
add.h 里只有声明,没有函数体;add.c 提供定义;test.c 用 #include "add.h" 把声明展开进来。自己写的头文件用双引号,标准库头文件用尖括号。
#pragma once 让同一个头文件在一个源文件里只展开一次,避免重复包含。头文件只放声明,不要把函数定义写进去:定义被多个 .c 包含时,链接器会看到多份同名函数,报重复定义。
在 VS2026 里新建空项目,把 add.h、add.c、test.c 都添加到项目,按 Ctrl + F5 运行。VS 会编译每个 .c,再把生成的目标文件链接成 .exe。

头文件和 .c 文件不会自动关联。test.c 能调用 Add,是因为它包含了声明;链接器能找到 Add 的实现,是因为 add.c 也参与了编译和链接。少放一个 .c 进项目,链接时就会报找不到符号。
10. static 与 extern
10.1 static 修饰局部变量
普通局部变量在函数每次进入时重新创建,函数返回时销毁。加 static 后,变量只创建一次,函数返回后仍然存在:
c
#include <stdio.h>
void test(void)
{
int i = 0;
i++;
printf("%d ", i);
}
void test_static(void)
{
static int i = 0;
i++;
printf("%d ", i);
}
int main(void)
{
for (int i = 0; i < 5; i++)
test();
printf("\n");
for (int i = 0; i < 5; i++)
test_static();
printf("\n");
return 0;
}
输出:
text
1 1 1 1 1
1 2 3 4 5
test 里的 i 每次进函数都重新初始化为 0,所以每次都打印 1。test_static 里的 static int i = 0 只在程序启动前初始化一次,之后每次调用都沿用上一次的值。
static 修饰局部变量,改变的是生命周期,不是作用域。i 仍然只能在 test_static 里使用,离开函数就看不见;但它的存储从栈区移到了静态区,程序结束才销毁。
| 变量 | 存放位置 | 生命周期 | 作用域 |
|---|---|---|---|
| 普通局部变量 | 栈区 | 函数返回即销毁 | 函数内部 |
static 局部变量 |
静态区 | 程序结束才销毁 | 函数内部 |

10.2 static 修饰全局变量和函数
全局变量和函数默认具有外部链接属性,其他源文件声明后就能使用。加 static 后,链接属性变成内部链接,只能在本源文件使用。
先看默认情况。add.c 里定义全局变量:
c
// add.c
int g_val = 2018;
test.c 里用 extern 声明它:
c
// test.c
#include <stdio.h>
extern int g_val;
int main(void)
{
printf("%d\n", g_val);
return 0;
}
两个文件一起编译链接,输出:
text
2018
extern 表示这个变量定义在别的文件里,这里只声明,不分配空间。链接器在 add.c 里找到 g_val 的定义,把引用接上。
把 add.c 里的定义改成 static int g_val = 2018;,同样的 test.c 就链接失败,VS2026 报 LNK2019,提示无法解析的外部符号 g_val。static 把 g_val 限制在 add.c 内部,test.c 看不到它。
static 修饰函数同理。add.c 里写 static int Add(...),其他文件即使声明了 Add,链接时也找不到这个符号。
| 修饰对象 | 默认链接属性 | 加 static 后 |
|---|---|---|
| 全局变量 | 外部链接,其他源文件可 extern |
内部链接,只能本文件使用 |
| 函数 | 外部链接,其他源文件可调用 | 内部链接,只能本文件调用 |
注意 static 对两类对象的作用不同:修饰局部变量时,改变的是生命周期;修饰全局变量和函数时,改变的是链接属性。全局变量本来就在静态区,生命周期已经是整个程序,static 不会让它活得更久。
什么时候用 static:局部变量要在两次调用之间保留值时,用 static 局部变量;某个全局变量或函数只给本文件用,不想被其他文件看到时,加 static。extern 用来声明其他文件里的符号,但跨文件共享全局变量会让依赖关系变隐蔽,能用参数传递就优先用参数。
11. 小结
- 函数由返回类型、函数名、参数列表和函数体组成;
void表示不返回结果,(void)表示没有参数。 - 库函数先查文档,包含对应头文件;
sqrt的原型是double sqrt(double),需要<math.h>。 - 函数调用要先声明后使用;定义在调用之后时,必须在前面写声明,否则 VS2026 报 C4013。
- 普通变量按值传递,形参是实参的副本;
swap交换的是副本,调用方的变量不变。 return立即结束函数,返回值会隐式转换成返回类型;非void函数的每条路径都要有返回值。- 数组传参传首元素地址,形参退化为指针,函数内
sizeof得到指针大小;元素个数要另传,二维数组的列数不能省。 - 多文件编程:
.h放声明,.c放定义;static修饰局部变量延长生命周期,修饰全局变量和函数则改为内部链接;extern声明其他文件中的符号。