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;
}
- 可以看到,在预编译阶段,就已经将条件编译指令处理完了,预编译结束后,就只剩下,条件编译中为真的那段代码。如上面的代码。所以我们可以总结出下面几条规则:
- 预编译器根据条件编译指令有选择的删除代码
- 预编译器不知道代码分支的存在
- if … else… 在程序的运行期进行判断
- 条件编译指令在预编译期就进行了判断,并且将条件为真的代码保留,为假的代码删除。
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;
}
- 如果使用下面的方法编译:
- gcc 22-2.c -o 22-2.out
运行结果为:
This is second printf…
- 如果使用下面的方法编译:
- 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;
}
从上面的预编译文件可以看出:
- 11行和23行中,该程序包含了两次 global.h 这个头文件
- 15行和27行中,该程序定义了两次global 这个变量,导致了变量的重复定义
- 所以代码编译出错。将源代码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 总结
- 条件编译可以使得我们按照不同的条件编译不同的代码片段,因而可以产生不同的目标代码
- 通过编译器命令行,可以定义预处理器使用的宏
- 条件编译,可以避免重复包含同一个头文件多次
- 条件编译可以定义软件产品的发布版和调试版