1 基本概念

  • 条件编译的行为类似于C语言中的if … else…
  • 条件编译是预编译指示指令,用于控制是否编译某段代码

比如下图的代码:

  • 上面的 #if、#else、#endif 就是条件编译的构成当然,还有其他的样式,后面会通过例子详细说明。

1.1 代码分析

如下代码:

  • 22-1.c
#include <stdio.h>

#define C 1
int main(){
    
    const char* s;
    
    #if(C == 1)
        s = "This is first printf...\n";
    #else 
        s = "This is second printf...\n";
    #endif
    
    printf("%s", s);
    return 0;
}
  • 上述代码编译运行结果为:

This is first printf…

上面的代码,看起来很简单,实际上,因为条件编译虽然像if…else 但是它并不像if…else那样在程序运行的时候进行判断,而是在预编译器编译的时候就决定了。什么意思呢?可以使用预编译指令对上述代码进行预编译(当然,为了简洁,先将1行和第14行需要使用标准库文件的代码注释掉)

  • gcc -E 22-1.c -o 22-1.i
  • 生成的预编译文件22-1.i 内容如下:
# 1 "22-1-lyy.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "22-1-lyy.c"



int main(){

    const char* s;


        s = "This is first printf...\n";





    return 0;
}
  • 可以看到,在预编译阶段,就已经将条件编译指令处理完了,预编译结束后,就只剩下,条件编译中为真的那段代码。如上面的代码。所以我们可以总结出下面几条规则:
  1. 预编译器根据条件编译指令有选择的删除代码
  2. 预编译器不知道代码分支的存在
  3. if … else… 在程序的运行期进行判断
  4. 条件编译指令在预编译期就进行了判断,并且将条件为真的代码保留,为假的代码删除。

1.2 通过命令行定义宏

  • 补充一点,我们还可以通过命令行定义宏,而无需再代码中定义,如下图所示:

如下代码:

  • 22-2.c
#include <stdio.h>

int main()
{
    const char* s;

    #ifdef C
        s = "This is first printf...\n";
    #else
        s = "This is second printf...\n";
    #endif

    printf("%s", s);
    
    return 0;
}
  1. 如果使用下面的方法编译:
  • gcc 22-2.c -o 22-2.out

运行结果为:

This is second printf…

  1. 如果使用下面的方法编译:
  • gcc -DC 22-2.c -o 22-2.out 或者 gcc -DC=1 22-2.c -o 22-2.out

运行结果为:

This is first printf…

我想上面的示例,已经足以让我们学会使用命令行定义宏了

2 #include 的本质

这个我们已经熟悉的不能再熟悉的东西了,包含头文件,有什么玄机?

  • #include 的本质是将已经存在的文件嵌入到当前文件中
  • #include 的间接包含,同样会产生嵌入文件的操作,这样会导致一个文件包含两个相同的头文件,这会产生错误。

比如下图的形式,编译会出错:

以下面三个代码为例,来说明:

  • global.h代码
// global.h
int global = 10;
  • test.h
// test.h
#include "global.h"
const char* NAME = "test.h";
char* hello_world()
{    
	return "Hello world!\n";
}
  • 2-3.c
#include <stdio.h>
#include "test.h"
#include "global.h"

int main()
{
    const char* s = hello_world();
    int g = global;
    
    printf("%s\n", NAME);
    printf("%d\n", g);
    
    return 0;
}

编译运行上述三个代码;

  • gcc 22-3.c -o 22-3.out
  • ./22-3.out

上面错误的行数不一样是因为我个人的代码行数与上面的不一样,这个可以忽略

  • 从上面的错误可以看出,变量global 重复定义了。很明显,我们并没有重复定义。
  • 对上述代码进行预编译(首先将第1、10、11 行注释掉,方便文件内容查看),gcc -E 22-3.c -o 22-3.i 得出的预编译文件内容如下:
# 1 "22-3.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "22-3.c"

# 1 "test.h" 1




# 1 "global.h" 1



int global = 10;
# 6 "test.h" 2

const char* NAME = "test.h";
char* hello_world(){
   return "Hello world!\n";
   }
# 3 "22-3.c" 2
# 1 "global.h" 1



int global = 10;
# 4 "22-3.c" 2

int main()
{
    const char* s = hello_world();
    int g = global;




    return 0;
}

从上面的预编译文件可以看出:

  1. 11行和23行中,该程序包含了两次 global.h 这个头文件
  2. 15行和27行中,该程序定义了两次global 这个变量,导致了变量的重复定义
  3. 所以代码编译出错。将源代码test.h中包含的global.h文件注释掉,重新编译即可。

2.1 解决重复包含头文件的问题

在一个大型软件中,重复包含头文件是非常普遍的,如果我们一个一个的去找,那就会太浪费时间。

使用条件编译,是可以解决该问题的。比如将上面的global.h 与test.h改为如下样式:

  • global.h
// global.h
#ifndef _GLOBAL_H_
#define _GLOBAL_H_
int global = 10;

#endif
  • test.h
// test.h
#ifndef _TEST_H_
#define _TEST_H_

#include "global.h"

const char* NAME = "test.h";

char* hello_world(){ 
   return "Hello world!\n";
   }

#endif

改好上面两个文件后,重新编译运行代码,结果如下:

  • 上面改变后的global.h与test.h 只是加了个条件编译指令,如果某一头文件被包含过一次,再要包含它的时候,由于不符合条件编译的条件,就不会有后面的代码,这样就避免了一个头文件被重复包含的错误。(如果不理解,回头从头看起,多看两遍)

3 条件编译的应用

条件编译可以定义软件产品的发布版与调试版

可以在代码中使用条件编译来定义需要调试的代码,当整个软件产品都调试通过最终需要发布的时候,可以直接去掉相关的宏定义(或者直接使用命令行的时候,不定义调试需要的宏即可),就可以使得代码在编译的时候,直接删除调试部分的代码,而无需自己动手删除。

本部分就不写实验了。

4 总结

  • 条件编译可以使得我们按照不同的条件编译不同的代码片段,因而可以产生不同的目标代码
  • 通过编译器命令行,可以定义预处理器使用的宏
  • 条件编译,可以避免重复包含同一个头文件多次
  • 条件编译可以定义软件产品的发布版和调试版