函数 - function

罗大富 BigRich大约 20 分钟C/C++

这节课我们来学习函数(function)。随着我们的编程水平不断地提高,程序中代码量也在不断扩大,我们就可能会面临这样的问题:

程序中需要在多个地方执行完全相同的操作,例如:重复计算圆的面积

如果我们将重复执行的代码封装在一个函数中,在需要的时候进行调用,这样我们的程序也就会变得更加简洁易懂。

一个个函数独立存在互不打扰,分别操控实现不同的功能,并随时听候我们的调用,我们在调用时也不用去关心其内部的细节。有了函数,可以将复杂问题分解为可管理、可重用的独立单元,不仅大大简化了程序的复杂度,也使程序逻辑更为清晰。

函数 是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main()。C 标准库提供了大量的程序可以调用的内置函数,例如:格式化输出函数 printf()标准输入函数 scanf() 等。

C 语言中的函数定义的一般形式如下:

返回值类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ···){
    // 函数体;
    return 返回值; // 如果返回值类型不是 void,则需要返回一个值
}

让我们分解一下这个结构:

  • 返回值类型:指定函数返回的数据类型,例如 int(整数)、float(浮点数)、void(无返回值)等;
  • 函数名:函数的标识符,用于调用时引用,命名规则与变量相同(字母、数字、下划线组成,首位不能是数字);
  • 参数列表:定义函数接受的输入,可以为空,用括号 () 包裹,多个参数用逗号分隔;
  • 函数体:用大括号 {} 包裹,包含函数的具体逻辑;
  • return 语句:用于返回结果并结束函数执行,如果返回值类型是 void,可以省略。

无返回值无参数函数

C 语言函数的一般形式中,每一块儿都是一个独立的知识点,为了让大家更清晰、更容易地学习 C 语言的函数,我们将先把一般形式拆分开来,分成一块一块的独立部分分别进行讲解,最后再把它们组合成函数的一般形式,到时候大家就十分容易理解。接下来就让我们开始进行函数正式的学习。

我们先来了解 无返回值无参数函数 的最基本格式:

void 函数名 () {
    函数体;
}

与函数的一般格式相比,无返回值无参数函数 只有四个部分分别为:

  • void:用于声明函数无返回值。使用 void 定义函数返回值类型,则函数体下面不能出现 return
  • 函数名:标识函数,用于调用。与变量命名规则一致,都要符合标识符命名规则,同时最好做到见名知意。
  • ():由于没有参数所以在 () 内什么也不填写,只需要用一个空括号表示没有参数。即使没有参数,函数名后的 () 也必不可少。
  • {函数体}:由 {} 包裹起来的实现具体逻辑的代码块。一定要用 {} 包裹起来,在其外面的部分则不属于这个函数中的成分。

比如,我们想要在控制台中打印出一个由 * 组成的 3*3 的矩阵,代码如下:

#include <stdio.h>


int main() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("*");
        }
        printf("\n");
    }
    return 0;
}

如果,我们想要重复在控制台中打印这个矩阵,第一种方法就是可以直接把这段代码复制一遍。但是,如果我们把该代码封装成一个函数,那么我们后续想要打印这个矩阵,就只需要调用该矩阵对应的函数即可,代码如下:

void print_matrix() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("*");
        }
        printf("\n");
    }
}

定义好函数后,我们需要通过调用来执行它。函数调用通常是将函数名加上参数(如果有)写在程序中合适的位置。调用时,程序会跳转到函数定义处,执行其中的代码,然后返回到调用点继续执行。调用函数的语法如下:

函数名(实参列表);          
// 由于我们现在定义的是无参数无返回值函数,因此,函数调用时直接使用函数名 + 小括号()
函数名();

// 例如:
print_matrix();

放在文件开头,并在 main 函数中调用,代码如下:

#include <stdio.h>

void print_matrix() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("*");
        }
        printf("\n");
    }
}

int main() {
    printf("第一次调用 print_matrix 函数\n");
    print_matrix();

    printf("第二次调用 print_matrix 函数\n");
    print_matrix();
    return 0;
}

函数在调用时需要加括号,使用方法与我们之前常用的 printf() 类似。如果我们把 print_matrix 函数的定义放在 main 函数之后,例如:

#include <stdio.h>


int main() {
    printf("第一次调用 print_matrix 函数\n");
    print_matrix();

    printf("第二次调用 print_matrix 函数\n");
    print_matrix();
    return 0;
}


void print_matrix() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("*");
        }
        printf("\n");
    }
}

在 C 语言中,函数的定义和调用顺序有一定的要求:如果函数的定义在调用之后,必须先声明函数,否则编译器会对该函数进行隐式声明。隐式函数声明的主要优点是可以在不引入额外的声明语句的情况下使用函数,但是也会带来很多问题,而且从现代 C 语言标准规范(C99、C11)是不允许不声明直接用的。

因此,如果我们的函数的定义如果写在了调用之后,一定要在文件顶部对函数进行声明,声明的语法如下:

返回值类型 函数名(参数列表);

与函数的定义格式相比,只是把函数体部分省略掉,代码如下:

#include <stdio.h>

void print_matrix();

int main() {
    printf("第一次调用 print_matrix 函数\n");
    print_matrix();

    printf("第二次调用 print_matrix 函数\n");
    print_matrix();
    return 0;
}


void print_matrix() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("*");
        }
        printf("\n");
    }
}

带有参数的函数

在上节课我们已经知道了如何去定义并调用一个最基本的函数,接下来我们就进一步地加入参数的概念:

// 函数的声明
void 函数名 (参数类型1 参数名1, 参数类型2 参数名2, ···)


int main(){
    // 调用函数
    函数名(参数名1, 参数名2, ···);
    
    return 0;
}

// 函数的定义
void 函数名 (参数类型1 参数名1, 参数类型2 参数名2, ···) {
    函数体;
}

我们为什么要用到参数呢?以上一节打印 3x3 矩阵为例,现在我们对 print_matrix() 函数进行升级,要求如下:

提供一个正整数 n,打印一个 nxn 的星号矩阵。

如果不使用函数,该程序代码如下:

#include <stdio.h>


int main() {
    int n = 5;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            printf("*");
        }
        printf("\n");
    }

    return 0;
}

但是,如果我们要将以上代码封装到函数中,其中的变量 n 是不确定的,并且需要从外部传递到函数内部,这时候,我们就可以把变量 n 设置成为该函数的参数。但是,在这里只需要定义,不需要赋值,直到调用的时候把需要传递的数值填写在函数名后面的小括号() 中,代码如下:

#include <stdio.h>


void print_matrix(int n);

int main() {
    // 在调用时传递数据
    print_matrix(4);

    // 调用时也可以传递变量
    int num = 5;
    print_matrix(num);

    // 注意形参和实参的区别
    int n = 3;
    print_matrix(n);

    return 0;
}


void print_matrix(int n) {
    // 打印 nxn 的星号矩阵
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            printf("*");
        }
        printf("\n");
    }
}

这时候,我们的函数就与刚才的无返回值无参数函数产生了一定的差异,现在,我们的函数需要传入参数,有了参数之后,我们的函数就不能再称之为无返回值无参数的函数,而是无返回值函数。

在上段代码中,我们可以看到一个问题,在定义函数时,我们的参数取名为 n,并且,在调用函数时,我们又创建了一个 int 类型的变量 n,并且把 n 作为 print_matrix() 的参数传递了进来,那么这两个 n 有什么区别呢?

这时候,我们就需要了解以下 形参实参 的区别。

  1. 实际参数(实参):是在调用有参函数时,函数名后面括号中的参数,是我们真实传给函数的参数,实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

  2. 形式参数(形参) 是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元)。形参在函数调用完成之后就自动销毁了,因此形参只在函数中有效,在函数外部使用函数的形参是不可行的。

下图中标注了哪些是形参,哪些是实参:

形参与实参
形参与实参

我们再来进行一个参数函数的练习:

编写一个函数,接受 3 个整数并比较大小,找出最大值。

代码如下:

#include <stdio.h>

void print_max(int a, int b, int c);

int main() {
    // 参数为常量
    print_max(1, 2, 3);

    // 参数为变量
    int x = 3, y = 6, z = 9;
    print_max(x, y, z);

    // 参数为表达式
    print_max(x*4, y*3, z + 3);
    return 0;
}


void print_max(int a, int b, int c) {
    // 假设 a 为最大值
    int max = a;
    // 如果 a < b,则将最大值设置为 b
    if (a < b) {
        max = b;
        // 再比较 b 与 c 的大小
        if (b < c) {
            max = c;
        }
    } else {
        // 若 a > b,则只需要判断 a 与 c 的大小
        if (a < c) {
            max = c;
        }
    }

    printf("最大值为:%d\n", max);
}

通过以上代码,我们也可以验证实参可以是常量、变量及表达式。

最后我们再来一个参数的练习题:

编写一个函数,接收一个正整数 n,计算其阶乘并输出结果(如 5! = 120)。

代码如下:

#include <stdio.h>

void factorial(int n);

int main() {
    factorial(4);
    return 0;
}


void factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }

    printf("%d! = %d\n", n, result);
}

带返回值的函数

现在我们来学习函数一般形式的最后一个版块 - 返回值。我们可以参考一下初高中数学中的函数:

f(x)=2x+1f(x)=x2+1 f(x) = 2x + 1 \\ f(x) = {x}^{2} + 1

在数学中,函数是输入(自变量)和输出(因变量)之间的关系:

输入 -> 处理过程(公式) -> 输出

C语言函数同样具备三个核心要素,输入 就相当于是 参数处理过程(公式) 相当于 函数体,输出就是 返回值

带有返回值的函数结构,即函数的的一般格式如下:

返回值类型 函数名(形参列表){
    // 函数体;
    return 返回值; // 如果返回值类型不是 void,则需要返回一个值
}

让我们分解一下这个结构:

  • 返回值类型:指定函数返回的数据类型,例如 int(整数)、float(浮点数)、void(无返回值)等;
  • return 语句:用于返回结果并结束函数执行,如果返回值类型是 void,可以省略。

无论有无返回值,函数均可被调用,其调用过程也是一致的,并且函数结束后也都能返回到主函数的调用处继续运行主函数的后续程序。但是像我们之前学习的无返回值形式的函数,他们的结果只能在控制台输出打印,并不能在程序其他的地方被使用,像我们上节课的练习 计算阶乘 ,最终结果仅仅是输出在控制台中显示,并没有办法在函数外使用。

因此,如果我们想要把函数 计算阶乘 的结果用在函数之外,就一定需要 返回值。在带有参数的函数上,最后使用 return 结束函数并把函数内容运行后得到的数据交给调用处,同时因为有了 返回值,所以返回值类型部分就不能再使用 void ,需要使用 与返回值相同的数据类型。此时有了返回值的加入,函数的一般类型就算是正式拼接完成了。比如,我们可以把计算阶乘的函数修改为:

#include <stdio.h>


int factorial(int n);

int main() {
    // 直接打印函数的返回值
    printf("5! = %d\n", factorial(5));

    // 变量接收函数的返回值
    int result = factorial(3);
    printf("3! = %d\n", result);
    return 0;
}


int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }

    return result;
}

同样的功能,还是计算两个数的和,但是我们不再直接使用 printf 函数直接在控制台上打印输出结果,而是将计算的结果返回值,以便调用处调用,由于返回的值两数的和是整数类型所以返回值类型使用基本整型 int 。既然求和函数的返回值可以被调用,那么在主函数中调用求和函数时,就需要使用变量去接收返回值以便后续使用或者在主函数中直接使用 printf 函数打印结果。

到此为止,C 语言函数的定义和调用就都学习完毕了,最后我们来看 3 道综合练习题:

练习 1:计算矩形面积:

/**
 * 计算矩形面积
 * @param length 矩形长度 (float类型)
 * @param width 矩形宽度 (float类型)
 * @return 矩形面积 (length * width)
 */
float rectangle_area(float length, float width);

// 测试用例:
// rectangle_area(5.0, 3.0) 应返回 15.0
// rectangle_area(2.5, 4.0) 应返回 10.0

代码如下:

#include <stdio.h>

float rectangle_area(float length, float width){
    return length * width;
}

int main() {
    printf("5 * 3 = %.1f\n", rectangle_area(5, 3));
    printf("2.5 * 4 = %.1f\n", rectangle_area(2.5, 4));
    return 0;
}

练习 2:编写函数,返回整型数组的平均值(浮点数类型),要求如下:

/**
 * 计算数组的平均值
 * 
 * @param arr[]   整型数组
 * @param length  数组长度
 * @return 对应数组的平均值
 */
float average(int arr[], int length);

// 测试用例:
// {1, 2, 3, 4} 的返回值为:2.5
// {99, 88, 73} 的返回值为:86.7

代码如下:

#include <stdio.h>


// 计算数组平均值
// 定义函数 `average`,接收一个整型数组 `arr` 和数组长度 `length`
// 返回值类型为 `float`,表示计算结果为浮点数
float average(int arr[], int length) {

    // 定义整型变量 `sum` 并初始化为0,用于存储数组元素的总和
    int sum = 0;

    // 使用 `for` 循环遍历数组,将所有元素累加到 `sum` 中
    for (int i = 0; i < length; i++) {
        // 每次循环将数组元素 `arr[i]` 的值加到 `sum` 上
        sum += arr[i];
    }

    // 将总和 `sum` 强制转换为浮点数(float),再除以数组长度 `length`
    // 这样可以确保结果为浮点数,而不是整数除法(如 10 / 4 = 2.5 而非 2)
    return (float)sum / length;
}


int main() {
    // 定义并初始化一个整型数组 `arr`,包含元素 {2, 4, 6, 8}
    int arr[] = {2, 4, 6, 8};
    int length = sizeof(arr) / sizeof(arr[0]);

    // 调用 `average` 函数计算数组的平均值,结果存储在浮点型变量 `a` 中
    float a = average(arr, length);

    // 使用 `printf` 函数输出结果,格式化字符串为:"该数组的平均值为:%.1f"
    // `%.1f` 表示保留一位小数输出浮点数
    printf("该数组的平均值为:%.1f", a);

    return 0;
}

变量的作用域

在我们之前的代码中,我们可以看到这样的问题:

在定义函数的时候,把形式参数的名字设置为 n。
在 main 函数中又创建了相同数据类型,且同名的变量 n。
运行程序没有报错

尤其是在定义和使用函数时,我们经常会遇到作用域的问题。在程序中定义的变量,只有在一定的代码范围内才是有效的,这个范围就称为变量的 作用域

根据作用域的不同,我们可以把变量分为 局部变量全局变量

局部变量

变量的作用域是程序中的局部范围,而非整个程序,例如,凡是在函数内部定义的变量,包括在函数体中定义的变量和在函数头中定义的形参,都是 局部变量

下面是局部变量的示例 1:

#include <stdio.h>


void func() {
    num = 2 * num;
    printf("func 函数中的 num 的值为:%d\n", num);
}

int main() {
    // num 是定义在 main 函数内部的局部变量
    int num;
    num = 10;
    func();
    printf("main 函数中的 num 的值为:%d\n", num);
    return 0;
}

运行上面这段代码之后,编译器会报错,func 函数中的 num 变量没有声明。这是因为在主函数中定义的变量 num 为局部变量,所以,num 变量只能在函数中引用,不能在其他函数中引用。也就是说,局部变量只能在定义他的函数中使用。

示例 2:

#include <stdio.h>


void func_2() {
    int a = 20;
    printf("func_2 函数中 a 的值:%d\n", a);
}

int main() {
    int a = 10;
    printf("main 函数中 a 的值:%d\n", a);
    func_2();
    printf("main 函数中 a 的值:%d\n", a);
    return 0;
}

运行结果为:

main 函数中 a 的值:10
func_2 函数中 a 的值:20
main 函数中 a 的值:10

由此可以发现,在同一个程序的不同函数中,可以定义同名的局部变量。编译器会将其视为相互独立、互不影响的变量。

全局变量

变量的作用域是整个程序文件,在 C 语言程序中,凡是在函数外部定义的变量,都是 全局变量

全局变量的作用域是从其定义点开始到本程序文件的末尾。

下面是全局变量的示例:

#include <stdio.h>


// 全局变量
int num;

void func() {
    num = 2 * num;
    printf("func 函数中的 num 的值为:%d\n", num);
}

int main() {
    num = 10;
    printf("main 函数中的 num 的值为:%d\n", num);
    func();
    printf("main 函数中的 num 的值为:%d\n", num);
    return 0;
}

运行结果如下:

main 函数中的 num 的值为:10
func 函数中的 num 的值为:20
main 函数中的 num 的值为:20

以下是执行流程:

程序启动
↓
全局变量 num = 0(默认初始化)
↓
进入 main()
↓
num = 10 → [内存值变为10]
↓
打印:在 main 函数中 num 的值为:10
↓
调用 func()
↓
num = 20 → [内存值变为20]
↓
打印:在 func 函数中 num 的值为:20
↓
返回 main()
↓
打印:在 main 函数中 num 的值为:20
↓
程序结束

这个结果清晰地展示了全局变量的核心特性:跨函数共享和持久性存储。任何函数对全局变量的修改都会影响整个程序。

C 语言常见库函数

前面我们已经学会了如何自己定义函数,C 语言标准库中其实也有很多自带的函数。为了方便管理,C 语言会把不同功能的函数放在不同的文件中,我们如果想要使用某个函数,就需要导入对应的头文件。例如,我们引入 stdio.h 文件后,就可以使用 printf() 函数。

库名头文件说明
stdio#include <stdio.h>标准输入输出库,包含 printf、scanf 等函数
stdlib#include <stdlib.h>标准库函数,包含内存分配、程序控制等函数
string#include <string.h>字符串操作函数,如 strlen、strcpy 等
math#include <math.h>数学函数库,如 sin、cos、sqrt 等
time#include <time.h>时间和日期函数,如 time、strftime 等

这些函数是完全不需要去记的,如果你打算使用某个函数,但是不知道该如何使用,可以在这个网站中查询 https://cppreference.com/open in new window

点击 header,进入头文件列表页面
点击 header,进入头文件列表页面

最简单的方式其实还是百度或者问 DeepSeek。

比如,我们可以引入 math.h 文件,使用 sqrt() 函数。直接问 DeepSeek C 语言中 sqrt() 如何使用?。DeepSeek 就会给出详细的使用案例以及注意事项:

DeepSeek 辅助学习
DeepSeek 辅助学习

代码如下:

#include <stdio.h>
#include <math.h>  // 必须包含这个头文件

int main() {
    double num = 25.0;
    double result = sqrt(num);  // 计算平方根
    
    printf("%.2f 的平方根是 %.2f\n", num, result);
    return 0;
}

如果你不清楚标准库中是否有某个功能的函数,我们也可以这么问 DeepSeek:C 语言中有没有生成随机数的函数,DeepSeek 就会把与生成随机数相关的函数列举出来:

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>  // 用于获取时间种子

int main() {
    // 设置随机种子(只需调用一次)
    srand(time(0));

    // 生成 0~RAND_MAX 的随机整数
    int r1 = rand();
    printf("随机数: %d\n", r1);

    // 生成 [0, 99] 范围内的随机整数
    int r2 = rand() % 100;
    printf("随机数: %d\n", r2);

    // 生成 [1, 100] 范围内的随机整数
    int r3 = rand() % 100 + 1;
    printf("随机数: %d\n", r3);

    return 0;
}

最后,我们再来学一个标准库中常用的函数 scanf()

scanf() 是 C 语言中的一个输入函数。与 printf() 函数一样,都被声明在头文件 stdio.h 里。 scanf() 是格式输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中。语法如下:

int scanf(格式控制,地址列表);
  • 格式控制:格式化字符串,指定输入的格式,并按照格式说明符解析输入对应位置的信息并存储于可变参数列表中对应的指针所指位置。
  • 地址列表:需要读入变量的地址或者字符串的首地址,而不是读入变量本身。用 & 符号表示取变量的地址,不用关心变量的地址具体是多少,只要在变量的标识符前加 &,就表示取变量的地址。

注意

编写程序时,在 scanf() 函数参数的地址列表处一定要使用变量的地址,而不是使用变量的标识符,否则编译器会提示出现错误。

示例代码如下:

#include <stdio.h>
int main()
{
	int score;
	printf("请输入成绩:");
	scanf("%d", &score);
	// 输出到屏幕上
	printf("成绩是:%d", score);
	
	return 0;
}

有时候程序不单会让用户输入一个数据,还可能会让用户输入多个数据,且类型不同,代码如下:

#include <stdio.h>


int main()
{
    char a = 0;
    int b = 0;
    float c = 0;
    
    printf("请按顺序输入字符、整数、浮点数:");
    scanf("%c %d %f", &a, &b, &c);
    printf("a = %c\n", a);
    printf("b = %d\n", b);
    printf("c = %.2f\n", c);
    
    return 0;
}

scanf() 处理占位符时,会自动过滤空白字符,包括空格、制表符、换行符等。就比如:用户输入的时候,按下回车键将数据分为几行,也是可以解读的。

上次编辑于:
贡献者: 罗大富BigRich