函数

1. 函数概述

1.1 函数的分类

C语言是一种通用的高级编程语言,拥有丰富的函数库和强大的系统编程能力。在C语言中,函数是模块化和可重用的代码段,用于实现特定的功能函数将一系列的语句组织在一起,可以接受输入参数并返回值

函数的定义包括函数名参数列表返回类型函数体:

  • 函数名是唯一的标识符,用于调用函数。
  • 参数列表指定了函数接受的参数,可以是任意数量和类型的参数。
  • 返回类型指定了函数返回值的类型,可以是基本数据类型、指针类型或结构体类型。
  • 函数体是实现具体功能的语句块。

C语言中的函数可以分为以下几类:

  1. 标准库函数(Standard Library Functions):C语言提供了很多常用的函数,如输入输出函数(如scanfprintf)、字符串处理函数(如strcpystrlen)、数学库函数(如sqrtsin)等。这些函数被包含在标准库中,可以直接调用。

  2. 用户自定义函数(User-defined Functions):在程序中,我们可以自定义函数,通过函数名调用并执行特定的功能。用户自定义函数可以接受参数并返回值,可以将程序分解成多个函数,提高代码的模块化和可读性。

  3. 递归函数(Recursive Functions):递归函数是一种特殊的函数,它在函数体内部调用自身。通过递归调用,函数可以解决涉及重复计算的问题,实现更简洁和优雅的算法。但需要注意递归函数应当包含条件语句以避免无限递归。

  4. 内联函数(Inline Functions):内联函数是一种将函数的代码嵌入到调用点的编译器特性。内联函数的主要目的是减少函数调用的开销,提高程序的执行速度。通过使用关键字inline来声明内联函数。

  5. 回调函数(Callback Functions):回调函数是一种通过函数指针作为参数传递的函数。它可以在程序运行时动态地指定回调函数,以实现特定的行为。常见的例子是在图形界面编程中,通过回调函数响应用户的操作。

无论是标准库函数、用户自定义函数还是其他类型的函数,都可以根据需要进行组合和调用,实现复杂的程序逻辑和功能。

1.2 函数的作用

函数在编程中有多种作用和用途,以下是函数的一些常见作用:

  1. 代码模块化:函数可以将一段代码逻辑封装成一个独立的模块,使代码结构更清晰和可维护。通过将程序分解成多个函数,可以使代码更易于理解、修改和调试。
  2. 代码重用:函数的设计就是为了提高代码的重用性。通过将通用的功能封装成函数,可以在程序中多次调用该函数来实现相同或类似的功能,避免重复编写重复的代码。
  3. 提高可读性:函数使代码的逻辑更加清晰和易于理解。给函数起一个好的函数名,并使用适当的注释,可以使代码的意图更加明确,提高代码的可读性。
  4. 提高代码的维护性:使用函数可以将代码结构化,并使其更易于理解和修改。当需要修改某个功能时,只需修改对应的函数,而不需要修改整个程序。这种模块化的设计使得代码更易于维护和改进。

总的来说,函数是编程中的基本构建块,它们提供了一种有效的方式来组织、重用和管理代码。通过合理地设计和使用函数,可以提高代码的可读性、可维护性、可测试性和复用性,进而改善开发效率和代码质量。

关于函数的使用,下面通过两个例子来进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 示例1
#include <stdio.h>
// 求两数的最大值
int max(int a, int b)
{
if (a > b)
{
return a;
}
else
{
return b;
}
}

int main()
{
// 操作1 ……
int a1 = 10, b1 = 20, c1 = 0;
c1 = max(a1, b1);

// 操作2 ……
int a2 = 11, b2 = 21, c2 = 0;
c2 = max(a2, b2);
return 0;
}
  • 第 4 行的max是一个自定义函数,有两个int型参数ab,返回值为int类型
  • main函数中可以多次调用自定义函数max,并且可以动态给它指定不同的实参,从而得到不同的最大值,非常灵活

通过例子对函数有了大致的了解之后,接下来我们来详细讲解如何去定义和声明一个函数。

2. 函数的定义和声明

2.1 函数定义

在编程中,函数定义是指定义一个函数的结构、功能和操作的代码块。函数定义包含函数的名称、参数列表、返回类型、函数体等组成部分。

以下是一个函数定义的一般格式:

1
2
3
4
5
6
返回类型 函数名(参数列表) 
{
// 函数体
// 执行的操作和逻辑
// 可能包括返回语句
}

具体来说,各个部分的含义如下:

  • 返回类型:函数执行完成后返回的数据类型。

    • 可以是基本数据类型,如整数、浮点数、字符等
    • 可以是指针、结构体、自定义类型等。
    • 如果函数没有返回值,可以使用 void 关键字表示。
    1
    2
    3
    4
    5
    6
    // 函数返回值为整形 int
    int max(int a, int b){}
    // 函数返回值为布尔类型 bool
    bool isRunning(int c){}
    // 没有返回值
    void show(){}
  • 函数名:函数的名称(最好见名知义),用于调用和引用函数。

    • 合法的函数名由字母、数字和下划线组成
    • 一般约定以字母开头
    1
    2
    3
    4
    5
    6
    // 有意义的函数名
    int max(int a, int b){} // 求最大值
    int sum(int a, int b, int c){} // 求和
    // 没有意义的函数名
    void zaaaaaaaa(){}
    void xyc123(char c){}
  • 参数(形参)列表:参数列表中的参数可以有N(N>=0)个

    • 定义函数的时候指定的参数叫做形参,形参不占用存储空间
    • 调用函数的时候指定的参数叫做实参,实参是占用存储空间的
    • 参数可以有多个,用逗号分隔,每个参数包括参数类型和参数名
    • 如果函数没有参数,参数列表可以为空,或者指定为 void
    • C 语言中,形参不允许指定默认值(赋值)
    1
    2
    3
    4
    5
    6
    7
    8
    // 没有参数
    void show(){}
    void print(void){}
    // 有参数
    int max(int a, int b){} // 2个参数, a, b 都是形参
    int sum(int a, int b, int c){} // 3个参数, a, b, c 都是形参
    // 给形参初始化 -- error
    int show(int a = 9){} // error, 语法错误
  • 函数体:函数的具体实现,包括一系列语句和操作。

    • 函数体内可以有变量声明、条件语句、循环语句、函数调用等
    • 如果函数有返回值,需要再函数体最后添加return 语句
      • return语句是函数体内部执行的最后一个语句
      • return语句一般出现在函数体末尾,但是也可能出现在函数体中间位置,表示函数从该位置退出了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 函数有返回值,需要返回一个整形
    int max(int a, int b)
    {
    if(a > b)
    {
    return a; // 返回一个变量, 变量是整形
    }
    else
    {
    return b+10; // 返回一个表达式, 表达式结果是整形
    }
    }

    double sum(int a, int b)
    {
    // 返回值是一个表达式, 值是整形
    // 会先将表达式结果隐式转换为 double 类型, 然后再返回
    return a+b;
    }

    // 函数没有返回值
    void show()
    {
    printf("hello, world\n");
    }

    void print()
    {
    printf("hello, world\n");
    return; // 相当于没有返回任何数值, 可以省略不写
    }

2.2 函数声明

所谓函数声明,就是在函数尚在未定义的情况下,事先将函数的名称、参数列表和返回类型通知编译器,这样可以保证编译能正常进行,作为用户也可以通过声明去理解和使用这个函数。函数声明通常放置在头文件中,可以在多个源文件中共享。

函数声明的一般格式如下:

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

关于函数声明我们应该掌握如下几点:

  1. 函数声明可以写到头文件也可以写到源文件
  2. 一个函数只能被定义一次,但可以声明多次
  3. 如果函数定义的位置在主调函数(调用它的函数)之后,则必须在调用此函数之前对被调用的函数作声明

以下是一个函数声明的示例:

1
int add(int a, int b);

在上述示例中,函数声明了一个名为 add 的函数,它接受两个整数类型的参数 ab,返回类型为 int。函数声明只说明了函数的名称、参数列表和返回类型,而没有提供函数的具体实现。可以在函数调用的地方使用这个声明,而编译器会在链接时找到该函数的定义。

  1. 函数定义在主调函数之前 - 不需要声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
    int add(int a, int b)
    {
    return a + b;
    }

    int main()
    {
    int result = add(8, 9);
    printf("result = %d\n", result);
    return 0;
    }

    在上面的例子中,主调函数对应的就是main函数,在main函数之前add函数已经被定义出来了,所以此时不需要进行函数声明。

  2. 函数定义在主调函数之后 – 需要先声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>
    int add(int a, int b);
    int main()
    {
    int result = add(8, 9);
    printf("result = %d\n", result);
    return 0;
    }

    int add(int a, int b)
    {
    return a + b;
    }

    main函数是程序的入口函数,在这个函数中调用了add函数,此时编译器是懵逼的,全然不知道有这么一个函数的存在,所以在编译的时候我们就能看到相关的警告信息:

    1
    warning C4013: “add”未定义;假设外部返回 int

    解决方法也很简单,只需要在主调函数(main函数)之前添加被调用函数的函数声明即可,此时编译器就知道add函数是何方神圣(叫什么名字),有什么神通了(参数有几个分别是什么类型,返回值是什么类型)。

最后,给大家总结一下函数定义和声明的区别:

  • 定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。

  • 声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)告知给编译器,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。

3. 函数的调用

定义函数后,必须要该函数进行调用,这样它的函数体才能够被执行。自定义函数和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main() 函数里调用别的函数,一个 C 程序里有且只有一个main()函数

3.1 函数的执行流程

下面通过一个简单的例子给大家讲一下C语言中程序的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}

int main()
{
int result = add(8, 9);
printf("result = %d\n", result);
return 0;
}
  1. 找到程序中的入口函数main()从第一行开始执行
  2. 调用了自动以函数add(int a, int b)
    • 编译器会在main()函数的上面寻找这个叫做add的自定义函数
    • 如果找到了,检查函数的参数类型是否匹配,没参数则忽略
  3. 执行add()函数,此时main()函数里面的执行会被add()这一行(第9行)代码阻塞,直到add()函数执行完毕,main()中的代码才会继续向下(第10行)执行。
  4. main()函数执行到return 0, 程序退出,执行完毕。

3.2 函数的形参和实参

在函数调用中,有两个相关概念:形式参数(形参)实际参数(实参):

  1. 形式参数(形参):在函数定义中声明的参数

    • 形参用于描述函数接受的参数类型和顺序。

    • 形参在函数定义中充当占位符,表示函数在执行时将接受的值。

    • 形参不占用内存空间

    • 形参通常在函数定义的括号内声明,并且可以有多个,用逗号分隔。

    • 形参只在函数内部可见,其作用域局限在函数内部。

    下面是一个简单的函数定义,其中 ab 就是形式参数:

    1
    2
    3
    4
    5
    int add(int a, char b) 
    {
    int sum = a + b; // b 会进行隐式类型转换 char->int
    return sum;
    }

    在上述示例中,add 函数定义了两个整型形式参数 ab

  2. 实际参数(实参):在函数调用时传递给函数的具体值或表达式。

    • 实参作为函数调用的一部分,提供了函数在执行时所需的数据。

    • 实参可以是常量、变量、表达式或函数返回值等。

    • 实参传递给函数,按照函数定义中形式参数的顺序进行匹配。

    下面是一个函数调用的示例,其中 整数2 和 字符a 就是实际参数:

    1
    int result = add(2, 'a');

    在上述示例中,函数 add 被调用时,实际参数 23 传递给了函数的形式参数 ab

在进行函数调用的时候,形参和实参有如下区别:

  • 实参变量对形参变量的数据传递是“值传递”,即单向传递,只能由实参传给形参,反之则不被允许。
  • 调用函数时、调用时、调用时,编译器临时给形参分配存储单元。调用结束后,形参单元被释放。
  • 实参单元与形参单元是不同的单元,因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
void modify(int a, int b)
{
a += 100;
b += 200;
printf("形参 a = %d, b = %d\n", a, b);
}

int main()
{
int num1 = 9, num2 = 6;
modify(num1, num2);
printf("实参 num1 = %d, num2 = %d\n", num1, num2);
return 0;
}

以上代码输出的结果如下:

1
2
形参 a = 109, b = 206
实参 num1 = 9, num2 = 6
  1. 在自定义函数modify内部操作了两个整形变量,就是这个函数的两个形参ab,这两个形参只能在改函数体内部使用。
  2. 在主函数main中对自定义函数modify进行了调用,并且给函数指定了两个实参num1num2,也就意味着这两个实参的值被赋值给了函数的两个形参,相当于做了这样的操作:
    • a = num1;
    • b = num2;
  3. 实参num1num2只是将值传递给了modify函数的形参ab
    • 形参ab值的改变不会对实参num1num2有任何影响
    • 实参num1num2访问范围是主函数main的函数体
    • 实参num1num2的生命周期随着主函数main的函数体结束而结束

3.3 函数的返回值

函数返回值是指在函数执行完毕后,通过 return 语句将结果返回给函数调用的地方。返回值允许函数将计算结果、状态信息或其他需要传递的数据返回给调用者。

函数的返回值在函数定义时通过返回类型声明,在函数调用时使用接收变量来接收返回值。以下是一般的函数返回值的语法格式:

1
2
3
4
5
6
返回类型 函数名(参数列表) 
{
// 函数体
// 执行的操作和逻辑
return 表达式; // 返回值
}

具体来说,函数返回值的相关要素如下:

  • 返回类型:函数执行完成后返回的数据类型。

    • 可以是基本数据类型,如整数、浮点数、字符等
    • 可以是指针、结构体、自定义类型等。
    • 如果函数没有返回值,可以使用 void 关键字表示。
  • return 语句:用于返回函数执行的结果。

    • 它可以后接一个表达式,表示返回的值。
    • 如果函数的返回类型是 void,则不需要 return 语句。

以下是一个函数返回值的示例:

1
2
3
4
5
int add(int a, int b) 
{
int sum = a + b;
return sum;
}

在上述示例中,函数 add 接受两个整数类型的参数 ab,在函数体中计算它们的和,并通过 return 语句返回结果。

在函数调用时,可以使用变量来接收函数的返回值:

1
int result = add(2, 3);

在上述示例中,函数 add 将返回结果 5,并将其赋值给变量 result如果函数调用者对该函数的返回值不感兴趣,可以不去接收这个返回值,无视即可。

函数的返回值可以用于进行进一步的计算、输出、赋值或其他操作。通过返回值,函数可以将结果传递给调用者,使得函数的执行结果能够被利用和处理。

4. 内联函数

在C语言中,内联函数(Inline Function)是一种用于优化代码执行效率的机制。内联函数在编译时将函数的代码直接插入到调用它的地方,而不是通过函数调用的方式执行,从而减少了函数调用的开销,提高了代码的执行速度

C语言的内联函数使用 inline 关键字来声明。将函数声明为内联函数只是给编译器一个建议,告诉它将函数的代码插入到调用的地方。编译器可以选择忽略内联函数的建议,继续将函数编译为常规函数。

以下是内联函数的一般格式:

1
2
3
4
5
6
inline 返回类型 函数名(参数列表) 
{
// 函数体
// 执行的操作和逻辑
return 表达式; // 可选的返回语句
}

具体来说,内联函数的使用规则和特点包括:

  1. 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
  2. 内联函数的定义通常放在头文件中,以便在多个源文件中共享。
  3. 内联函数的函数体通常较简单,避免包含复杂的控制流程、循环或递归调用和大量的代码。
  4. 内联是以代码膨胀为代价,从而省去了函数调用的开销,从而提高了函数的执行效率(空间换时间)

下面是一个简单的内联函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
inline int multiply(int a, int b)
{
return a * b;
}

int main()
{
int result = multiply(10, 20);
printf("result = %d\n", result);
return 0;
}

在上述示例中,multiply 函数被声明为内联函数,它接受两个整数类型的参数 ab,并返回它们的乘积。在使用这个内联函数时,编译器将尝试在调用该函数的地方插入相应的代码,而不会执行函数调用操作。

5. 递归函数

在C语言中,递归函数是一种函数调用自身的技术递归函数可以用于解决需要重复执行相同操作的问题,将问题不断分解为更小的子问题,并通过函数调用自身来处理这些子问题,直到达到最简单的情况。

递归函数的基本原理是把大问题分解成一个或多个相同的小问题,然后通过调用自身来解决这些小问题。递归函数包括两部分:基本情况和递归调用:

  • 基本情况指的是递归函数终止的条件,当函数遇到基本情况时,不再调用自身,直接返回结果。

  • 递归调用指的是在函数内部调用自身来解决规模较小的子问题。通过递归调用,问题的规模不断缩小,直到达到基本情况,然后逐层回溯,获得最终的结果。

需要注意的是,递归函数必须具备终止条件,否则会陷入无限递归的循环中,导致程序异常

5.1 阶乘

阶乘是一个正整数与比它小的所有正整数的乘积。一般来说,我们用符号 "!" 表示阶乘

例如,5 的阶乘表示为 5!,计算方法为:
5! = 5 × 4 × 3 × 2 × 1 = 120

阶乘的计算可以通过循环或递归来实现。下面分别给出阶乘的循环和递归算法示例。

1. 循环计算阶乘:

1
2
3
4
5
6
7
8
9
unsigned int factorial(unsigned int n) 
{
unsigned int result = 1;
for (unsigned int i = 1; i <= n; ++i)
{
result *= i;
}
return result;
}

2. 递归计算阶乘:

1
2
3
4
5
6
7
8
9
10
11
unsigned int factorial(unsigned int n) 
{
if (n == 0 || n == 1)
{
return 1;
}
else
{
return n * factorial(n - 1);
}
}
  • 0和1的阶乘为1,当参数为 0 或者 1时,就是递归结束的条件

  • 如果参数为n(n>1),需要用当前的值n乘以n-1的阶乘,也就是n * factorial(n - 1)

通过上图可知递归分为两个阶段

  • 执行函数体内不满足递归结束条件的部分代码,继续递归(压栈)
  • 递归结束条件满足之后,执行剩余代码,并返回数据,依次类推(出栈)
  • 最后被调用的函数第一个返回数据(也就意味着这个函数第一个执行完函数体内的所有代码),第一个被调用的函数最后一个返回数据(此时得到的就是最终的值)

5.2 斐波那契数列

斐波那契数列(Fibonacci Sequence)是一个数列,其中每个数字都是前两个数字之和。数列的前两个数字通常定义为0和1,然后通过递推关系计算后续的数字。斐波那契数列的定义如下:

F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n ≥ 2)

根据这个定义,斐波那契数列的前几个数字为:0, 1, 1, 2, 3, 5, 8, 13, 21, …

以下是两种常见的计算斐波那契数列的方法:迭代和递归。

1. 迭代方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int fibonacci(int n) 
{
if (n == 0)
{
return 0;
}

int prev = 0;
int curr = 1;
for (int i = 2; i <= n; i++)
{
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}

2. 递归方法:

1
2
3
4
5
6
7
8
int fibonacci(int n) 
{
if (n <= 1)
{
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}

在上述示例中,递归函数 fibonacci 接受一个整数参数 n,用于计算斐波那契数列的第 n 项。

  • n 小于等于 1 时,达到基本情况,直接返回 n
  • n 大于 1 时,通过递归调用 fibo_recursive(n-1)fibo_recursive(n-2) 来计算第 n 项的值。

6. 多文件编程

6.1 代码模块化

当需求比较复杂的时候或者做一个比较大的项目的时候,不可能将所有的源码都写到一个文件中,此时就需要进行模块化处理,思路如下:

  1. 将需求拆分成若干个小模块,每个模块对应一个源文件
  2. 给每个源文件提供一个头文件,通过这种方式实现函数的复用
    • 头文件进行函数声明
    • 源文件进行函数定义

假设我现在要做一个计算器,根据需求拆分成了加、减、乘、除四个模块,那么此时对应的源文件就是五个:

  • 处理加法的源文件:add.c
  • 处理减法的源文件:subtract.c
  • 处理乘法的源文件:multiply.c
  • 处理除法的源文件:divide.c
  • 主函数main(入口函数)对应的源文件:test.c

add.h

1
2
3
4
5
6
// add.h
#ifndef ADD_H_
// 函数声明
int add(int a, int b);
#define ADD_H_
#endif

add.c

1
2
3
4
5
6
#include "add.h"
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}

subtract.h

1
2
3
4
5
6
// subtract.h
#ifndef SUBTRACT_H_
// 函数声明
int sub(int a, int b);
#define SUBTRACT_H_
#endif

subtract.c

1
2
3
4
5
6
#include "subtract.h"
#include <stdio.h>
int sub(int a, int b)
{
return a - b;
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "substract.h"
#include "add.h"
#include <stdio.h>

int main()
{
int number = 9;
int number1 = 3;
int res1 = add(number, number1);
int res2 = sub(number, number1);
printf("res1 = %d, res2 = %d\n", res1, res2);
return 0;
}

关于乘法和除法的例子在这里就不写了,如法炮制就可以,完全是一样的。对于上面的代码有一下细节需要说明一下:

  • 一个头文件可能被多个源文件包含,只要包含了这个头文件,在源文件中就可以使用头文件中声明的函数了
  • 一个头文件中也可以包含其它头文件
  • 在包含多个头文件的时候,对顺序没有要求
  • 要避免自定义头文件被重复包含

在C语言中,一个源文件对应一个头文件并不是必须的,有时候多个源文件可以对应同一个头文件,也就是说N个源文件中定义的函数的声明都被放到了同一个头文件中。

calc.h

1
2
3
4
5
6
7
// add.h
#ifndef ADD_H_
// 函数声明
int add(int a, int b);
int sub(int a, int b);
#define ADD_H_
#endif

add.c

1
2
3
4
5
6
#include "add.h"
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}

subtract.c

1
2
3
4
5
6
#include "subtract.h"
#include <stdio.h>
int sub(int a, int b)
{
return a - b;
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
#include "calc.h"
#include <stdio.h>

int main()
{
int number = 9;
int number1 = 3;
int res1 = add(number, number1);
int res2 = sub(number, number1);
printf("res1 = %d, res2 = %d\n", res1, res2);
return 0;
}

6.2 避免头文件重复包含

在包含头文件的时候可能由于失误将一个头文件包含了两次,或者是因为头文件的嵌套包含导致某个头文件被包含了多次(>1)

1
2
3
4
5
6
7
8
9
10
11
// a.h
#include "hello.h"
#include <stdio.h>
#include "hello.h" // 头文件被重复包含

// b.h
#include "a.h"

// c.h
#include "a.h"
#include "b.h" // 头文件展开之后发现 a.h 在 c.h 中包含了两次

如果出现了以上情况,程序在编译的时候就会报错。为了避免同一个文件被include多次,C/C++中有两种方式:

  • 使用文件保护宏

    文件保护宏(header guard macro)是一种传统的预处理指令,用于防止头文件重复包含。文件保护宏可以确保在编译时只包含一次特定的头文件,从而避免由于多次包含导致的重复定义错误。

    文件保护宏需要在头文件的开头和结尾进行定义和结束,如下所示:

    1
    2
    3
    4
    5
    6
    #ifndef HEADER_FILE_NAME_H
    #define HEADER_FILE_NAME_H

    // 头文件的内容

    #endif // HEADER_FILE_NAME_H
    • HEADER_FILE_NAME_H 是一个唯一的标识符,用于确保宏定义的唯一性,因此每个头文件都应该使用不同的宏名称。
    • 可以根据需要自定义宏的名称,常见的做法是以头文件名称的大写形式作为宏的名称
    • 在编译过程中,首次包含头文件时,ifndef 条件为真,进入 #define 块,并定义了该宏。随后的重复包含,由于宏已经有定义,条件为假,不再执行 #define 块内的代码。
  • 使用 #pragma once

    #pragma once 是一种预处理指令,用于确保头文件只被编译一次。它是一种用于防止头文件重复包含的非标准的预处理指令,被大多数主流编译器所支持。

    使用 #pragma once 可以替代传统的头文件保护宏,如 #ifndef#define#endif。通过使用 #pragma once,可以更简洁地确保头文件只被编译一次,而无需手动编写宏定义。

    示例使用 #pragma once 的头文件:

    1
    2
    3
    #pragma once

    // 头文件内容

    需要注意的是,虽然 #pragma once 在大多数情况下提供了简洁的头文件包含机制,而且几乎所有主流的编译器都支持它,但它不是C或C++的标准预处理指令。因此,如果您希望代码更具可移植性,可以继续使用传统的头文件保护宏。