【C语言】函数

摘要 :函数把完成特定任务的代码打包,用参数接收输入,用返回值交出结果。本文从重复出现的计算切入,先讲库函数的查法和自定义函数的组成部分,再拆开形参与实参的值传递、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 声明其他文件中的符号。
相关推荐
zwc321043205543 分钟前
Day 2:图像内存、数据类型与stride
c++·人工智能·opencv·算法·计算机视觉
重生之小比特1 小时前
【算法拾光】模拟|提莫攻击 + N 字形变换
算法·模拟
東隅已逝,桑榆非晚1 小时前
AVL数实现
c++·笔记·学习·算法
驭渊的小故事2 小时前
LeetCode 算法题解:滑动窗口与数组合并的实战应用
算法·leetcode·职场和发展
.YM.Z2 小时前
优选算法——【双指针算法2】C++实现
开发语言·c++·算法
zwc32104320553 小时前
第四周第五天——重构成类
c++·人工智能·opencv·算法·计算机视觉
Navigator_Z3 小时前
LeetCode //C - 1311. Get Watched Videos by Your Friends
c语言·算法·leetcode
云析赢指标公式网43 小时前
通达信ATAN均线角度公式源码笔记:四周期角度计算与DRAWBAND色带实现
笔记·算法
21岁被迫秃头4 小时前
SVT-AV1 Mini-Gop决策算法详解+源码分析.md
算法·av1·svt-av1·源码详解·mini-gop决策