解析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. 使用的几点注意事项与说明

  1. ifdef 和 ifndef 后面必须跟上宏的名称,表示该宏有没有被定义,不可跟表达式
  2. 必须以 #endif 进行结尾(初学者易忘)。
  3. 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. 几点注意与说明

  1. #if 更强调表达式的真假,而 #ifdef 只关注宏是否被定义。
  2. #if 进行判断时,若使用宏,空宏会报错,宏未定义会认为条件为假
  3. #if defined(宏名) / #if !defined(宏名),可以模拟 #ifdef / #ifndef。
  4. #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.为何要有条件编译?

条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里“实际情况”,取决于运行平台,代码本身的
业务逻辑等。
可以认为有两个好处:

  1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小。
  2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现。

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中条件编译,头文件包含知识,以及 #/## 的运用的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ngfor 中向元素添加 id 以及在 ngfor 中条件创建元素

打印与日志文件中条件匹配的数据

C#-#define条件编译

C#-C#-#define条件编译

在哈希中条件包含键值对[关闭]

c/c++源文件为何要包含自己的头文件?(编译器检查定义和声明的一致性)