物联网工程师技术之C语言预处理

举报
tea_year 发表于 2024/01/22 16:56:44 2024/01/22
【摘要】 本章重点• 无参数的宏定义• 带参数的宏定义• 头文件• 条件编译指令在前面的学习中,经常遇到用#define定义符号变量的情况,其实,#define是一种预处理指令。预处理指令在C语言中占有重要的地位,它是程序从源代码到可执行文件的编译流程中的第一步,在这一步中,编译器会根据预处理指令进行宏定义替换,包含头文件,进行条件编译等操作,。本章将针对预处理的相关知识进行详细地讲解12.1 回顾编...

本章重点

无参数宏定义

带参数宏定义

头文件

条件编译指令

在前面的学习中,经常遇到用#define定义符号变量的情况,其实,#define是一种预处理指令。预处理指令在C语言中占有重要的地位,它程序从源代码可执行文件的编译流程中的第一在这一步中,编译器会根据预处理指令进行宏定义替换,包含头文件进行条件编译等操作本章将针对预处理的相关知识进行详细地讲解


12.1 回顾编译流程

在计算机中,编译系统负责将C语言的源代码转换成计算机的可执行文件,其执行过程分为以下四个步骤,具体如下:

预处理:处理源代码中的宏定义、头文件引入和条件编译等操作。对于宏定义,编译器在源文件中直接将出现宏定义的部分进行替换;对于头文件引入,编译器会找到相应的头文件,将头文件的内容插入到源文件中;对于条件编译,编译器会根据条件编译的内容选择编译整个工程中的部分内容。

编译:在这个阶段,预处理之后的C语言源文件被翻译为汇编语言。

汇编:编译之后得到的汇编语言在这个阶段被翻译为机器指令。汇编之后得到的文件被称为二进制文件,它的内容是只有CPU可以看懂的机器语言。

链接:在这个阶段原来项目中不同的源文件经过汇编得到的二进制文件被整合在一起,并生成一个可执行文件。可执行文件可以被载入到内存中,并被CPU执行。    

12.2 宏定义

宏定义是最常用的预处理功能之一,对于预处理器而言,它在看到宏定义之后,会将随后在源代码中根据宏定义进行简单的替换操作。本节将针对宏定义的相关知识进行详细地讲解。

12.2.1 #define#undef

宏定义指令以#define开头,后面跟随宏体,它的语法如下

    #define 宏名 宏体

为了和其他变量以及关键字进行区分,宏定义中的宏名一般用全大写英文字母以及下划线来完成。下面是一个例子:

    #define PI 3.14

在这个宏定义中定义了一个标识符PI它所代表的值是3.14随后的源代码中凡是出现了PI的地方都会被替换3.14接下来,通过一个案例来验证,如例程12-1所示。

例程 121 #define指令

1 #include <stdio.h>

1 #define PI 3.14

2 int main()

3 {

4     printf("%f\n", PI);

5     return 0;

6 }

程序运行结果如12-1所示

121 #define指令

在上面的例程中,程序首先定义了一个PI值为3.14。在main函数内部使用printf打印PI的值。在预处理过程中,6里的PI会被23.14替换,最后main函数中输出一个浮点数3.14

除了#define之外相应地还有#undef指令#undef指令用于取消宏定义#define定义了一个宏之后,如果预处理器在接下来源代码中看到了#undef指令,那么从#undef之后这个都不存在,如例程12-2所示。

例程 122 #undef使用

1 #include <stdio.h>

7 #define PI 3.14

8 int main()

9 {

10     printf("%f\n", PI);

11 #undef PI

12     printf("%f\n", PI);

13     return 0;

14 }

在上面这个例子中,程序首先定义了PI,并且在6行使用printf函数输出PI的值。随后7行中利用#undef指令取消PI这个宏,7开始PI这个宏定义就不存在8行中程序依然试图使用宏定义PI并输出它的值,结果只能报错。Visual Studio上述程序会显示“未声明的标识符”的错误,如图12-2所示

122 #undef带来的错误


12.2.2     简单宏定义

这里的简单宏定义指的就是上一节那样仅仅完成简单替换工作的宏。为了加深对宏定义的认识,本节中将用几个例子来展示宏定义灵活用法。

除了像上一节那样进行简单的数值替换,宏定义还可以用来进行表达式替换。下面一个利用宏定义替换表达式的例子:

例程 123 宏定义替换表达式

1 #include <stdio.h>

15 #define ONE_PLUS_ONE 1 + 1

16 int main()

17 {

18     printf("1 + 1 = %d\n", ONE_PLUS_ONE);

19     return 0;

20 }

在上面的程序中,首先定义了一个ONE_PLUS_ONE,用来计算1+1的值随后main函数的内部调用printf1+1值输出。注意在6行中printf的参数是ONE_PLUS_ONE经过预处理之后会被替换为1 + 1也就是和下面的语句等价:

    printf("1 + 1 = %d\n", 1 + 1);

程序输出结果如下图所示:

123 宏定义替换表达式

宏定义还可以用来定义字符串。下面的例子以Hello world为例展示了宏定义的这一用法:

例程 124 宏定义替换字符串

1 #include <stdio.h>

21 #define HELLO_WORLD "hello world!\n"

22 int main()

23 {

24     printf(HELLO_WORLD);

25     return 0;

26 }

这个程序宏定义HELLO_WORLD定义了一个带回车的字符穿hello world,并在main函数中利用printf打印这个字符串和之前的例子类似预处理过程中,预处理器看到HELLO_WORLD之后会将它自动替换为相应字符串

    printf("hello world!\n");

程序的输出结果如下

124 宏定义替换字符串

如果宏定义的内容过长,还可以使用\宏定义的内容定义到下一行

例程 125 宏定义字符串

1 #include <stdio.h>

2 #define HELLO_WORLD        "HHHHHHHHHHHHHHello \

3 world!\n"

4 int main()

5 {

6     printf("%s\n", HELLO_WORLD);

7     return 0;

8 }

上面的程序运行结果如下:

125 宏定义字符串

宏定义还可以嵌套使用在一个宏定义中使用的宏定义下面是一个宏定义嵌套的例子。在这里例子中首先定义了一个PI用来表示圆周率,之后定义了一个COMP_CIR用来计算圆周长:

例程 126 嵌套宏定义

1 #include <stdio.h>

27 #define PI 3.14

28 #define COMP_CIR 2 * PI *

29 int main()

30 {

31     double r = 1.0;

32     printf("2 * pi * r = %f\n", COMP_CIR r);

33     return 0;

34 }

2行中程序利用宏定义定义了圆周率PI,在第3定义了宏COMP_CIR2 * PI *此处的宏PI在预处理时会被上面的宏定义#define PI 3.14替换。在main函数中首先定义了一个double类型的圆半径它的大小是1.0随后利用printf去输出利用宏定义计算得到的圆周长COMP_CIR r。在预处理过程中,COMP_CIR会被一层层展开为:

    printf("2 * pi * r = %f\n", 2 * 3.14 * r);

程序运行结果如下:

126 嵌套宏定义

12.2.3     带参数的宏定义

除了无参数的宏定义之外有的时候在程序中更希望能够使用带参数的宏定义这样成替换过程的时候会有更多的灵活性。下面以刚刚计算圆周长的无参数宏定义为例,将它修改为带参数的宏定义:

例程 127 带参数宏定义

1 #include <stdio.h>

35 #define PI 3.14

36 #define COMP_CIR(x) 2 * PI * x

37 int main()

38 {

39     double r = 1.0;

40     printf("2 * pi * r = %f\n", COMP_CIR(r));

41     return 0;

42 }

上述程序中3定义了一个带参数的宏定义:

    #define COMP_CIR(x) 2 * PI * x

在这里x宏定义中的参数对于带参数的宏定义,在预处理过程中首先会将参数替换进宏定义中,再用替换参数的宏定义在源代码中做替换具体地说程序8使用到了COMP_CIR(r),那么首先在第3的宏定义中,参数xr宏定义COMP_CIR(r)的值2 * PI * r

    printf("2 * pi * r = %f\n", 2 * PI * r);

这里嵌套了宏定义PI它也会被替换为3.14,最终第8行在经过预处理之后变为

    printf("2 * pi * r = %f\n", 2 * 3.14 * r);

程序最终的运行结果如下图所示:

127 带参数宏定义

由于宏定义仅仅完成文本替换的工作而不会检查运算的优先级,因此对于带参数的宏定义一般建议宏体中用括号将参数起来,即采用如下形式:

    #define COMP_CIR(x) 2 * PI * (x)

这样可以保证运算顺序不出错。下面是一个利用宏定义实现乘法例子

例程 128 带参数宏定义

1 #include <stdio.h>

43 #define MUL(x, y) (x) * (y)

44

45 int main()

46 {

47     printf("%d\n", MUL(3 + 5, 4 + 2));

48     return 0;

49 }

程序中定义了一个带两个参数xy的宏定义MUL用来实现两个数的乘法main函数中MUL的两个参数分别是3+54+2,按照目前定义,在第6printfMUL将被替换为:

    printf("%d\n", (3 + 5) * 4 + 2));

输出结果期待的48

128 带参数宏定义

然而,如果将宏定义改成:

    #define MUL(x, y) x * y

程序的输出结果就变成了25

129 带参数宏定义

这是因为宏定义只进行文本替换,在printfMUL被替换为

    printf("%d\n", 3 + 5 * 4 + 2);

出于同样的原因,在带参宏的最外面也建议用括号起来否则也会由于运算符优先级的问题导致出现不希望的结果下面是一个例子:

例程 129 带参数宏定义

1 #include <stdio.h>

2 #define ADD(x, y) ((x) + (y))

3 int main()

4 {

5     printf("3 * (4 + 4) / 6 = %d\n", 3 * ADD(4, 4) / 6);

6     return 0;

7 }

上面的程序定义了一个加法宏ADD用来将xy相加,运行上述程序将会得到:

1210 带参数宏定义

然而,如果将宏定义改为:

    #define ADD(x, y) (x) + (y)

带入到printf中将会得到:

    printf("3 * (4 + 4) / 6 = %d\n", 3 * (4) + (4) / 6);

最终程序的运行结果会变成:

1211 带参数宏定义

上面例程揭示了在带参数宏定义中使用括号的必要性。一般在定义带参数宏定义的时候,宏体中参数和最外面都要加括号以避免期望的结果发生。

看上去使用带参数宏定义稍有不慎就会出错,那么为什么还要使用它呢?确实,为了实现同样的功能,完全可以写一个函数来替代带参数的宏定义,但是请记住宏定义的处理是在预处理的时候进行的,这就意味着在程序运行时这些宏定义已经被替换为具体的程序语句了,从而减少了函数调用开销。例如同样是求数的绝对值

例程 1210 求绝对值

1 #include <stdio.h>

50 #define ABS(x) ((x) >= 0 ? (x) : -(x))

51 double compAbs(double x)

52 {

53     return x >= 0 ? x : -x;

54 }

55 int main()

56 {

57     double x = 4.5;

58     printf("abs(4.5) = %f %f\n", ABS(x), compAbs(x));

59     return 0;

60 }

上述程序的运行结果如下:

1212 绝对值

对于宏定义ABS,它在预处理的时候就被替换了,程序直接执行的是相应的三目运算符对于函数abs来说,使用的时候需要首先将实参x拷贝给abs形参x然后执行三目运算符,然后将得到的结果返回。相比之下宏定义的开销要小一些。如果想要频繁调用某一函数,而函数的实现又足够简单,程序对性能要求又非常高,那么使用宏定义不失为一种好的选择。

当然,宏定义和函数调用相比是非常不健壮的。即使上面加了层层括号的ABS也可能返回错误的结果:

例程 1211 错误的绝对值

1 #include <stdio.h>

2 #define ABS(x) ((x) >= 0 ? (x) : -(x))

3 double compAbs(double x)

4 {

5     return x >= 0 ? x : -x;

6 }

7 int main()

8 {

9     double x = 12, y = 12;

10     printf("abs(++x) = %f\n", ABS(++x));

11     printf("abs(++y) = %f\n", compAbs(++y));

12     return 0;

13 }

程序的运行结果如下所示:

1213 错误使绝对值


可以看到两次输出的结果不一致了。显然函数abs输出的结果13正确的,为什么ABS的宏就不对呢?还是整个宏展开:

    printf("abs(++x) = %f\n", ((++x) >= 0 ? (++x) : -(++x)));

现在想象一下如果x=12首先在下面的逻辑判断中:

    (++x) >= 0

首先x值被自增13接下来这个逻辑判断值显然为真因此紧接着去执行:

    (++x)

这个时候问题出现了:x在这里被再次自增了一次,因此返回打印的结果变成了再次自增后的14程序的结果偏离预期了。因此,在使用宏定义时务必要小心谨慎。


12.3 #include指令

#include指令用来引入头文件。在预处理过程中出现#include引入头文件的地方,头文件内容会被直接插入到源文件中。

本节首先介绍头文件的定义和使用方法,随后介绍如何利用#include指令引入头文件。

12.3.1 头文件

头文件就是C程序中.h结尾文件。原则上头文件包含内容没有限制,但是由于在预处理中头文件的处理方式是被直接替换所有包含了这个头文件源文件,一般在头文件里会定义函数原型以及整个项目中有可能被多个源文件使用的宏定义,结构体等。

头文件的一种典型用法是用来编写需要被其他源文件广泛使用的函数。下面是一个例子在这个例子中,程序中有一个头文件foo.hfoo.h定义了一个结构体类型Foo以及一个函数原型bar

    struct Foo

    {

        int i;

    };

    void bar(struct Foo f);

相应程序有一个源文件foo.c,在foo.c中有bar函数的实现此处bar函数一个空函数:

    #include "foo.h"

    void bar(struct Foo f)

    {

    }

由于在foo.c文件中使用到了struct Foo的定义,因此要包含foo.h的头文件,否则无法得知struct Foo的类型。

最后,在main函数中调用bar函数:

例程 1212 main.c

1 #include "foo.h"

61 int main()

62 {

63     struct Foo f = {1};

64     bar(f);

65     return 0;

66 }

main函数实现,首先定义了一个Foo类型变量f并将它成员初始化1随后调用bar函数。由于bar函数的原型定义在foo.h,因此main.c需要引入bar函数原型所在头文件foo.h,否则在main.c是找不到bar的定义的。编译上述三个文件之后,程序可以正确执行:

1214 头文件的使用

    上述例程揭示了头文件的一般使用方法:将函数原型,结构体的定义等等放在头文件XXX.h中,将对应的函数实现放在另外一个源文件中。对于其他的源文件,如果需要引用XXX.h中声明的函数,只要利用include将该头文件引入即可。下面是一个更加有意义的例子:

例程 1213 月份程序

1 #include <stdio.h>

67 int inputMonth()

68 {

69     int month;        //    输入一个月份

70     scanf("%d", &month);

71     if (month < 1)

72         month = 1;

73     if (month > 12)

74         month = 12;

75     return month;

76 }

77 int dayInMonth(int month)

78 {

79     int day;

80     switch (month)

81     {

82         case 1:

83         case 3:

84         case 5:

85         case 7:

86         case 8:

87         case 10:

88         case 12:

89             day = 31;

90             break;

91         case 4:

92         case 6:

93         case 9:

94         case 11:

95             day = 30;

96             break;

97         default:    //    二月

98             day = 28;

99             break;

100     }

101     return day;

102 }

103 void outputDay(int day)

104 {

105     if (day < 28)

106         day = 28;

107     if (day > 31)

108         day = 31;

109     printf("%d\n", day);    

110 }

111 int main()

112 {

113     int month = inputMonth();

114     int day = dayInMonth(month);

115     outputDay(day);

116     return 0;

117 }

除了main函数,上面的程序还定义了三个新的函数:

inputMonth:负责接收用户输入月份,如果用户的输入月份超出了112范围,函数还会将月份强制转换到这个范围内;

dayInMonth一个转换函数,负责给出给定月份中给的天数;

outputDay:输出特定的天数。如果天数超出了2831范围,函数还会将天数强制转换这个范围之内。

运行上述程序,假设用户输入了3

1215 月份程序

看上去非常完美,程序运行十分正常。现在假设在程序中想要加入一个新的函数用来计算一年中一共有多少天

    int dayInYear()

    {

        int day = 0;

        int month;

        for (month = 1; month <= 12; month++)

        {

            day += dayInMonth(month);

        }

        return day;

    }

函数dayInYear看上去也没有什么问题,它利用一个for循环遍历112份,然后每个月调用dayInMonth函数计算出每个月的天数,最后将它们一起累加起来求出正确的结果365。现在如果想要在main函数中调用dayInYear

    int main()

    {

        int month = inputMonth();

        int day = dayInMonth(month);

        outputDay(day);

        day = dayInYear();

        printf("%d\n", day);

        return 0;

    }

在这里我们就需要将dayInYear定义加入到main.c中。如果程序还想进一步加入新的功能,可以预见程序的main.c只会越来越长对于程序的编写和维护都是非常不利的。

幸运的是,利用头文件可以将程序从main.c解放出来。对于上面的所有函数,可以定义一个头文件date.h

    int inputMonth();

    int dayInMonth(int month);

    void outputDay(int day);

    int dayInYear();

现在main.c中无需包含所有的函数实现,只要引用头文件date.h就可以了:

例程 1214 月份程序

1 #include <stdio.h>

118 #include "date.h"

119 int main()

120 {

121     int month = inputMonth();

122     int day = dayInMonth(month);

123     outputDay(day);

124     day = dayInYear();

125     printf("%d\n", day);

126     return 0;

127 }

相应地,函数的实现放在另外一个源文件date.c由于date.c函数实现中包含了printfscanf,因此在date.c也要引用stdio.h

    #include <stdio.h>

    int inputMonth()

    {

        int month;        //    输入一个月份

        scanf("%d", &month);

        if (month < 1)

            month = 1;

        if (month > 12)

            month = 12;

        return month;

    }

    int dayInMonth(int month)

    {

        int day;

        switch (month)

        {

            case 1:

            case 3:

            case 5:

            case 7:

            case 8:

            case 10:

            case 12:

                day = 31;

                break;

            case 4:

            case 6:

            case 9:

            case 11:

                day = 30;

                break;

            default:    //    二月

                day = 28;

                break;

        }

        return day;

    }

    void outputDay(int day)

    {

        if (day < 28)

            day = 28;

        if (day > 31)

            day = 31;

        printf("%d\n", day);    

    }

    int dayInYear()

    {

        int day = 0;

        int month;

        for (month = 1; month <= 12; month++)

        {

            day += dayInMonth(month);

        }

        return day;

    }

上述三个文件放在同一工程下,运行程序,得到:

1216 月份程序

这个例子更加具体地展示了头文件的用法:在头文件中声明函数之后,一个文件中实现声明的函数,在想要使用这些函数的源文件main.c)里只要引用头文件即可,不需要将函数的实现全部拷贝到源文件中。

12.3.2 引入头文件

利用#include指令引入头文件一般有两种方法它们的语法格式分别如下:

方法:引用编译器自带的头文件,比如用于标准输入输出的stdio.h用于数学计算的math.h等等头文件的引用都属于这一类:

    #include <文件名>

它的格式#include加上英文尖括号括起的文件名。此前的例程中已经多次出现了这种例子:

    #include <stdio.h>

    #include <stdlib.h>

方法二:引用当前项目中的头文件格式为:

    #include "文件名"

它的格式为#include加上英文双引号括起的头文件名。和上一引用方式区别是利用双引号引用文件名,此时编译器会首先在当前项目的目录下搜索是否有匹配的头文件。如果没有搜索到,编译器回到头文件的目录下寻找。

合理使用两种引入头文件的方式可以提高编译的效率。比如对于编译器提供的头文件stdlib.h尽管两种引用方式都是合法的,但是第一种方式明显比第二种方式效率要高。

下面的例程展示上述两种引入头文件的方式例程包括两部分:一个程序员自己编写的头文件foo.h当中定义了一个宏NUM一个程序员自己的源文件main.c在源文件中引用头文件foo.h使用宏NUM,并引用头文件stdio.h结果输出头文件和源文件定义在同一个文件夹下头文件的定义如下:

    //    foo.h

    #define NUM    15

源文件main.c定义如下:

例程 1215 头文件的两种引用方式

1 //    main.c

2 #include <stdio.h>

128 #include "foo.h"

129 int main()

130 {

131     int num = NUM;

132     printf("num = %d\n", num);

133     return 0;

134 }

程序运行的结果是:

1217 引入头文件

需要指出的是,头文件也可以引入头文件,但是如果可能的话应该尽量避免出现这样的情况。直接重复引用头文件有可能会导致诸如类型重定义等错误。如果实在无法避免,应该在引入头文件的同时尽量考虑使用本章讲到的条件编译指令。


12.4 条件编译指令

条件编译指令用来告诉编译系统不同的条件下,需要编译不同位置的源代码。正确合理地使用条件编译指令可以给予程序很大的灵活性

12.4.1 #if/#else/#endif

#if指令#else指令和#endif指令三者经常结合一起使用。它们的使用方法if else语句类似:

#if 条件

    源代码1

#else

    源代码2

#endif

编译器只会编译源代码1和源代码2中的一段。当条件为真时,编译器会编译源代码1否则编译源代码2一个经典的使用#if/#else/#endif场景是一个程序需要支持不同平台时,根据#if当中的条件可以选择编译不同的代码,从而实现对不同平台的支持下面是一个例子:

例程 1216 #if指令

1 #include <stdio.h>

2 #define WIN32    0

3 #define x64        1

135 #define SYSTEM WIN32

136 int main()

137 {

138 #if SYSTEM == win32

139     printf("win32\n");

140 #else

141     printf("x64\n");

142 #endif

143     return 0;

144 }    

例子中,程序里首先用宏定义SYSTEM定义了操作系统的位数是32位。在main函数利用一个条件编译指令判断SYSTEM是否32位,如果是的话,输出win32,否则认为是64系统,输出x64。程序的输出结果如下:

1218 #if指令

实际的项目中当然不会输出printf那么简单。由于不同平台可能需要不同的代码来处理诸如数据类型不一致等情况,#if/#else/#endif的这种框架可以用来实现在源代码中支持不同的平台,以确保程序可以兼容不同运行环境。

12.4.2 #elif

#elif作用和else if语句类似。我们可以把上面的例程进行扩展让程序支持更多的平台:

例程 1217 #elif指令

1 #include <stdio.h>

145 #define windows    0

146 #define linux    1

147 #define mac        2

148 #define SYSTEM mac

149 int main()

150 {

151 #if SYSTEM == windows

152     printf("win\n");

153 #elif SYSTEM == linux

154     printf("linux\n");

155 #elif SYSTEM == mac

156     printf("mac os\n");

157 #endif

158     return 0;

159 }    

上述例程中,首先定义了SYSTEM随后在#if条件编译指令的部分进行扩展:如果宏SYSTEM的值windows那么输出win,否则如果SYSTEM的值是linux,输出linux,最后如果SYSTEM值是mac则输出mac os。通过不同#elif下编写代码可以让这个程序实现对不同操作系统平台的支持

上述程序的输出结果如下

1219 #elif指令

和程序语言中的if else结构一样,条件编译指令中的#elif可以有多个,而且最后也可以没有#else,就像上面的例子中那样。

12.4.3 #ifdef

条件编译指令#ifdef用来确定某一个是否已经被定义了,它需要和#endif一起使用。如果这个宏已经被定义了,就编译#ifdef#endif中的内容,否则就跳过。

#if/#else/#endif不同的是,#if/#else/#endif用来从多段源码中选择一段编译#ifdef可以用来控制单独的一段源码是否需要编译,它的功能一个单独的#if/#endif类似

#ifdef一个应用是用来控制是否输出调试信息下面是一个例子:

例程 1218 #ifdef指令

1 #include <stdio.h>

160 #define DEBUG

161 int main()

162 {

163     int i = 0;

164 #ifdef DEBUG

165     printf("i = %d\n", i);

166 #endif

167     int j = 3;

168 #ifdef DEBUG

169     printf("j = %d\n", j);

170 #endif

171     int sum = i + j;

172 #ifdef DEBUG

173     printf("i + j = %d\n", sum);

174 #endif

175     return 0;

176 }

上面的例程中,首先定义了DEBUG,用来控制是否需要输出调试信息main函数主体部分非常简单:定义了整型变量ij定义了整型变量sum用来计算ij每一次定义之后都有一printf语句用来输出变量值。由于DEBUG已经被定义,因此所有的printf都会被编译,程序的运行结果如下:

1220 #ifdef指令

假设现在程序已经调试完成了,在发布的时候不希望有这些冗余的调试输出信息这个时候只要将DEBUG宏定义取消,所有#ifdef包含printf信息都不经过编译

    //#define DEBUG

程序输出结果如下

1221 #ifdef指令

12.4.4 #ifndef

#ifdef相反#ifndef用来确定某一个宏是否没有被定义,它也需要和#endif一起使用它的用法和#ifdef相反:如果这个宏还没有被定义,那么就编译#ifndef#endif中间的内容,否则就跳过。

#ifndef经常和#define一起使用,它们用来解决头文件中的内容被重复定义的问题。在一个源文件中如果相同的头文件被引用了两次就很有可能出现类型重定义下面是一个具体的例子:

在这个例子中三个头文件foo.hbar1.hbar2.h有三个源文件main.cbar1.cbar2.c首先是foo文件的内容:

    struct Foo

    {

        int i;

    };

foo.h文件中定义了一个结构体Foo它包含一个整型变量i

接下来是bar1.h内容:

    #include "foo.h"

    void bar1(struct Foo f);

bar1定义了一个函数原型bar1,它参数一个结构体Foo类型的变量。在bar1.h引用了头文件foo.hbar1实现在源文件bar1.c

    void bar1()

    {

    }

为了简便,这里bar1是一个空函数。

类似地,bar2.h也定义了函数原型bar2它的参数也是一个Foo类型的结构体变量

    #include "foo.h"

    void bar2(struct Foo f);

bar2函数的实现在源文件bar2.c中。为了简便,bar2也是一个空函数

    void bar2()

    {

    }

最后,在main函数中定义一个Foo类型变量f,并调用bar1bar2两个函数

例程 1219 main.c

1 #include "foo.h"

177 #include "bar1.h"

178 #include "bar2.h"

179 int main()

180 {

181     struct Foo f = { 1 };

182     bar1(f);

183     bar2(f);

184     return 0;

185 }

如果直接编译上述程序发现编译无法通过Visual Studio会提示struct类型重定义的错误

1222 类型重定义

这是因为main.c源文件中,结构体Foo定义被多次引入了。具体地说,1行的#include指令Foo定义引入了一次,后两行引入bar1.hbar2.h虽然没有定义Foo但是两个头文件都分别引用foo.h,因此最终在main.c当中我们将会看到的是:三次Foo结构体定义,函数bar1的声明,函数bar2声明

    struct Foo

    {

        int i;

    };

    struct Foo

    {

        int i;

    };

    void bar1(struct Foo f);

    struct Foo

    {

        int i;

    };

    void bar2(struct Foo f);

    int main()

    {

        struct Foo f = { 1 };

        bar1(f);

        bar2(f);

        return 0;

    }

这样的main.c显然是不能编译通过的。问题就在于虽然Foo只在foo.h定义了一次,但是它在main函数中由于头文件之间的嵌套引用导致foo.h最终多次引用从而导致在main.c多次出现Foo重复定义。

利用#ifndef#define组合可以解决这个问题。现在foo.h如下的修改:

    #ifndef _FOO_H_

    #define _FOO_H_

    struct Foo

    {

        int i;

    };

    #endif

修改后foo.h当中包含了#ifndef条件编译指令。注意在#ifndef的编译指令内部包括一条#define指令,当这一段代码初次编译时,_FOO_H_尚未被定义,符合#ifndef的条件,因此结构体Foo的定义可以被编译foo.h内容再次被编译时,由于在初次编译已经定义了_FOO_H_,因此#ifndef的条件不符合内容被跳过。这样就保证了main.c即使多次引用了foo.hFoo结构体的定义也仅仅被编译一次。利用#ifndef指令过预处理后main.c文件相当于下面这个文件

例程 1227 main.c

1 #ifndef _FOO_H_

186 #define _FOO_H_

187 struct Foo

188 {

189     int i;

190 };

191 #endif

192 #ifndef _FOO_H_

193 #define _FOO_H_

194 struct Foo

195 {

196     int i;

197 };

198 #endif

199 void bar1();

200 #ifndef _FOO_H_

201 #define _FOO_H_

202 struct Foo

203 {

204     int i;

205 };

206 #endif

207 void bar2();

208 int main()

209 {

210     struct Foo f = { 1 };

211     bar1(f);

212     bar2(f);

213     return 0;

214 }

尽管Foo的定义还是出现了三次,由于#ifndef保护,Foo只会被编译一次程序这一次能够正确通过编译并执行执行结果如下:

1223 #ifndef指令

当然如果可能的话,还是应该尽量文件中嵌套引用别的自定义头文件。不过确实在有些时候头文件的嵌套引用实在难以避免了,这时候#ifndef不失为一种有效的解决方法

12.5 本章小结

本章首先回顾了编译系统的工作流程,随后介绍了三种预处理指令:宏定义#include指令,以及条件编译指令。预处理完成之后,编译系统会生成不包含任何预处理指令的源文件并交给编译器,进入编译系统的其他步骤


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。