解析C中条件编译,头文件包含知识,以及 #/## 的运用
Posted 追道者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解析C中条件编译,头文件包含知识,以及 #/## 的运用相关的知识,希望对你有一定的参考价值。
条件编译是C程序预处理时进行的操作,本质是进行代码的选择性裁剪工作。指令很多,我们逐一来看。笔者建议,阅读本文前,可以了解一下宏的有关知识,具体可见博文:万能的替身演员 #define
一. ifdef / ifndef
1. 基本认识
#include <stdio.h>
int main()
{
#ifdef CPP
printf("CPP\\n");
#endif
return 0;
}
顾名思义,ifdef 如果定义了宏CPP,执行 #ifdef 和 #endif 之间的语句,否则不执行。
还可以进行二分支:
#include <stdio.h>
#define CPP
int main()
{
#ifdef CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
这时候,如果你定义了CPP这个宏,打印CPP,如果没定义,执行 #else 下面的内容。
了解了 ifdef 那么 ifndef 就非常简单了,它与 ifdef 在含义上恰好相反。
#include <stdio.h>
#define DEBUG
int main()
{
#ifndef DEBUG
printf("Release\\n");
#else
printf("Debug\\n");
#endif
return 0;
}
ifdef 是如果定义,ifndef 即 if not def 如果没有定义宏 DEBUG 执行 ifndef 下面的内容,否则,执行 #else 下面的内容。
ifdef 和 ifndef 一般不使用多分支形式,因为对于一个宏,只有定义与不定义两种情况。
2. 使用的几点注意事项与说明
- ifdef 和 ifndef 后面必须跟上宏的名称,表示该宏有没有被定义,不可跟表达式。
- 必须以 #endif 进行结尾(初学者易忘)。
- ifdef 和 ifndef 仅仅关注的是宏有没有被定义,而不是宏定义的值,将一个宏定义为 0 值,甚至定义为空宏,该宏也仍然被定义。
3. 原理
ifdef 和 ifndef 具体是如何做的呢?我们以以下例子为例:
#include <stdio.h>
#define CPP
int main()
{
#ifdef CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
我们切换到Linux平台,利用gcc编译器看一下这段代码预处理后的结果,大段的头文件展开部分我们就不看了:
我们可以观察到,预处理结束后,我们的代码中只剩下了printf(“CPP\\n”)这一条有效语句。
其实,条件编译的原理与宏类似,宏的基本原理是直接文本替换,而条件编译的基本原理是直接根据条件进行文本裁剪。预处理时,编译器检测到宏 CPP 被定义,就会保留 #ifdef 下面的代码,而直接将 #else 以下的代码直接裁剪。
如果我们将 #define CPP 这一行语句注释掉:
#include <stdio.h>
//#define CPP
int main()
{
#ifdef CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
预处理的结果就变成了:
ifndef 原理相同,不赘述。
总结:条件编译是在预处理阶段,直接根据条件,进行文本裁剪工作。
二. if elif else
1. 基本用法
这组概念与 ifdef/ifndef 不同,后者只能判断宏的定义与否,而前者则可以判断表达式的真假,与C语言基本语法中的 if 表达式在一定程度上比较类似。
#include <stdio.h>
int main()
{
#if 1
printf("True\\n");
#else
printf("False");
#endif
return 0;
}
当然,它也可以使用宏进行辅助判断,只是这里和 ifdef 有一点不同的地方,如下:
#include <stdio.h>
#define CPP 0
int main()
{
#if CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
打印结果为:
注意,#if 可以被认为是判断表达式真假,CPP虽然被定义了,但它的值为0,仍被认为不满足条件。而 #ifdef 是判断宏有没有被定义,只要宏被定义了,不管它本身是何值,都满足 ifdef 的条件。
那么,读者请想一下,如果定义成空宏,结果如何?
#include <stdio.h>
#define CPP
int main()
{
#if CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
会直接报错:
提示为 #if 后面必须跟一个表达式。因为空宏可以看做文本替换时,替换成一个空格,而 #if 后面必须跟上一个表达式,所以报错。
但是,有趣的是,宏如果未定义,编译器会看成是假值,并不会报错,这点请读者特别注意:
#include <stdio.h>
int main()
{
#if CPP
printf("CPP\\n");
#else
printf("NO CPP!\\n");
#endif
return 0;
}
还有一种用法是 #if 可以模拟 ifdef / ifndef 如:
#define CPP 0
#define C 1
int main()
{
#if defined(CPP)
printf("CPP!\\n");
#elif defined(C)
printf("C!\\n");
#else
printf("NO!\\n");
#endif
return 0;
}
可以用 #if defined(宏) 来模拟 #ifdef 这时候的关注点又重新回到了宏是否被定义。如该段代码的执行结果为:
宏CPP虽然值为0,但其仍然被定义,故仍然满足条件。
多分支形式和 if 表达式类似,都是从上到下扫描,扫描到满足的条件就直接进入执行,以后的其他分支不再进入。
还可以用 #if !define(宏名) 来模拟 #ifndef 如:
#include <stdio.h>
#define DEBUG
int main()
{
#if !defined(DEBUG)
printf("Release\\n");
#else
printf("Debug\\n");
#endif
return 0;
}
#if 还支持多分支,嵌套使用,以及逻辑判断,这里就和C基础语法中的 if 表达式很类似了:
#include <stdio.h>
//#define DEBUG
#define C 0
#define CPP 1
int main()
{
#if defined(DEBUG)
printf("Debug\\n");
#if C
printf("C\\n");
#elif CPP
printf("CPP\\n");
#else
printf("NO\\n");
#endif
#else
printf("Release\\n");
#if (C && CPP) //这里不带括号也行,但是带上比较优雅~
printf("C && CPP\\n");
#endif
#endif
return 0;
}
大家可以自行去修改,测试这段代码,按照 if 表达式的逻辑理解就行。
2. 几点注意与说明
- #if 更强调表达式的真假,而 #ifdef 只关注宏是否被定义。
- #if 进行判断时,若使用宏,空宏会报错,宏未定义会认为条件为假。
- #if defined(宏名) / #if !defined(宏名),可以模拟 #ifdef / #ifndef。
- #if 可以进行多分支,嵌套,判断条件可以进行逻辑运算,这里与C基础语法的 if 表达式类似。
3.原理
与 #ifdef 一样,都是在预处理阶段进行代码裁剪,我们举个例子:
#include <stdio.h>
#define C 1
#define CPP 1
int main()
{
#if defined(DEBUG)
printf("Debug\\n");
#if C
printf("C\\n");
#elif CPP
printf("CPP\\n");
#else
printf("NO\\n");
#endif
#else
printf("Release\\n");
#if (C && CPP)
printf("C && CPP\\n");
#endif
#endif
return 0;
}
我们看预处理的结果:
没错,在预处理阶段,编译器会帮我们根据判断条件选择相应代码保留,而那些不满足条件的代码会直接被编译器裁剪。这也是 #if 和 if 表达式的最本质的区别。
既然如此,这里有个 “丧心病狂” 的代码:
#include <stdio.h>
void SayHello()
{
printf("Hello World\\n");
#ifndef ERROR
}
int main()
{
#endif
SayHello();
return 0;
}
现在编译执行这段代码并没有任何错误,因为我们没有定义宏 ERROR:
可是如果我们定义这个宏(宏不一定非要在代码中定义,gcc可以使用-D选项让编译器帮我们定义这个宏):
因为一旦定义了这个宏,源代码中 #ifdef 和 #endif 之间的代码将会被编译器直接裁掉,导致代码错误。
之所以举这个例子,还是希望大家明白,条件编译,仅仅是在预处理阶段,进行代码层面上的,简单的裁剪工作。
三. 条件编译的作用和意义
1.为何要有条件编译?
条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的
业务逻辑等。
可以认为有两个好处:
- 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小。
- 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现。
2.条件编译都在哪些地方用?
举个例子:
我们经常听说过,某某版代码是完全版/精简版,某某版代码是商用版/校园版,某某软件是基础版/扩展版等。
其实这些软件在公司内部都是项目,而项目本质是有多个源文件构成的。所以,所谓的不同版本,本质其实就是功能的有无,在技术层面上,公司为了好维护,可以维护多种版本,当然,也可以使用条件编译,你想用哪个版本,就使用哪种条件进行裁剪就行。
著名的Linux内核,功能上,其实也是使用条件编译进行功能裁剪的,来满足不同平台的软件。
四. 文件包含
1.理解 #include
我们在敲代码的时候,大多数同学第一行语句便是 #include <stdio.h> 那么今天,我们来聊一聊 #include 的具体作用。
#include <stdio.h>
int main()
{
printf("Hello World!\\n");
return 0;
}
我们以这段简单代码为例,看一下预处理展开的结果:
不知道大家有没有注意到,区区6行代码,预处理后竟然变成了 843 行之多,而源代码中的后五行可以看到,预处理阶段并没有进行处理,那么前面800多行代码是什么呢。
我们看预处理的第一行:
没错,原来 #include <stdio.h> 的内容已经没有了,我们在往下翻翻:
有一些我们或认识,或不认识的函数声明。
结论:#include <头文件名> 在预处理阶段将头文件,递归性的,选择性的,拷贝到我们的源代码中。
关键词:
- 递归性的:一个头文件里可能包含着其他头文件,展开的时候会将所有的涉及到的头文件一并展开。
- 选择性的:拷贝头文件的时候会选择性的拷贝,如原文件中的注释部分,不会拷贝到我们的源代码里。
2. #ifndef #define #endif 与 #pragma once
写过头文件的同学都知道,头文件必须要有上面的两种结构之一。
这种结构的作用是:防止因头文件被重复包含而造成编译效率降低。
一般结构是这样的,假设头文件名称为test.h:
#ifndef _TEST_H_ //一般这个宏这样命名是最标准的
#define _TEST_H_
/*
头文件内容:
标准头文件包含
函数声明
...
*/
#endif
至于为什么这样的结果可以让头文件只被包含一次,相信了解了 #include 的作用,以及条件编译的知识,大家应该清楚了。
想简单一点,可以直接在头文件第一行加上:
#pragma once
和上面的结构效果相同。
注:重复包含,会引起多次拷贝,主要会影响编译效率!同时,也可能引起一些未定义错误,但是特别少。
五. # / ##
1.单井号
单井号一般与宏配合使用,如下:
#include <stdio.h>
#define STR(x) #x
int main()
{
char* s = STR(123456);
printf("%s\\n", s);
return 0;
}
打印结果为:
它的作用,就是在宏中使用,把宏参 s 转换为字符串。
但是,我们不能这么用 # :
#include <stdio.h>
#define STR(x) #x
int main()
{
int num = 123456;
char* s = STR(num);
printf("%s\\n", s);
return 0;
}
大家猜猜结果是什么?结果是:
是不是一下子感觉神秘的 # 瞬间弱智?哈哈哈,其实,这还是因为,宏在预处理阶段就被处理,而变量是在编译阶段才进行处理,所以你给 STR() 传递的 num 它在预处理阶段可不会认为是一个变量,而会直接当成一个符号,进行转字符串操作。
结论:# 与宏配合使用,会把宏参的字面值转换为字符串。
2.双井号
在宏中使用,会连接两个符号。先看一个小例子:
#include <stdio.h>
#define ADDN(x) STUDENT##X
int main()
{
ADDN(1);
ADDN(2);
ADDN(3);
ADDN(4);
ADDN(5);
}
这段代码会报错,我们看它预处理的结果:
没错,双井号会把前后两个符号连接起来,形成一个新的符号,至于为什么会报错,因为 Student1 等等并不是C的标准符号或自义定符号,编译器找不到此符号的定义。
要正确理解符号和字符串的概念:
- 字符串:C语言中用双引号括起来的字符组合,可以存储为变量。
- 符号:C语言中标准符号或自义定符号,如 int a = 0 其中 int 就是一个符号。
##仅仅做的是将两个符号进行连接。
一个正确的小例子:
#include <stdio.h>
#define CONT(x,n) (x##e##n)
int main()
{
printf("%f\\n", CONT(1.1, 2)); //预处理时会被替换为 1.1e2 浮点数的科学计数法
system("pause");
return 0;
}
鉴于笔者水平有限,若有大佬指出错误或不足之处,笔者感激不尽。
以上是关于解析C中条件编译,头文件包含知识,以及 #/## 的运用的主要内容,如果未能解决你的问题,请参考以下文章