C预处理详解2
四、#undef
这条指令用于移除一个宏定义
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
例如:如果定义一个宏MAX为100,则打印这个宏的结果就是100
#define MAX 100
int main()
{printf("%d\n",MAX);return 0;
}
而如果再用 #undef 指令对这个宏名进行重新定义,那么它之前的就会被移除,那么这时候运行时就会报错,因为此时的宏MAX未定义
五、命令行定义
许多C的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了⼀个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外⼀个机器内存大些,我们需要⼀个数组能够大些。)
例如:(使用VS code gcc)
我们写一个打印数字的代码,创建一个变长数组,此时我们可以在命令行输入命令:
gcc test.c -D sz= 10 -o test.exe
我们可以自行规定数组的长度,此时所设置的长度是10,那么运行起来(在命令行输入 .\test.exe)时就会打印 0~9 十个数字
而我们也可以规定其他的长度,例如100等
我们也可以打开 test.i 文件查看(以sz=100为例),发现它确实变成了我们想要的长度:
在命令行输入 gcc test.c -D sz=100 -E -o test.i
六、条件编译
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很方便的,因为我们有条件编译指令。
比如如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
例如以下的代码,我们写一个宏PRINT,使用 #ifdef 和 #endif 这一对条件编译指令进行选择性编译,如果运行时发现这个宏存在(被定义为0或非0的数字,甚至是不定义都可以),就会打印 hehe,
而如果发现不存在这个宏,则 #ifdef 这条指令下的语句就不会进行打印,颜色也会变灰
我们也可以通过 gcc 观察:
常见的条件编译指令
1.
#if 常量表达式
//……
#endif
例如:#if 的条件可以是一个常量、常量表达式,如果为0,条件为假,就不执行
常量表达式由预处理器求值:
例如:
创建一个宏M,宏的值可以为常量或常量表达式,如果为0,则 #if 的条件为假,不执行
(注意与 #ifdef 和 #endif 进行区分,这一对指令是只要宏存在,无论它的条件是什么都可以)
2.多个分支的条件编译
#if 常量表达式
//……
#elif 常量表达式
//……
#else
//……
#endif
例如:
宏M的值为多少就执行哪一条指令
3.判断是否被定义
#if defined(symbol) #if !defined(symbol)
//…… //……
#endif #endif
或者这样写: 或者这样写:
#ifdef symbol #ifndef symbol
//…… //……
#endif #endif
#if !defined(X) ≡ #ifndef X(检查是否未定义)【如果宏X已定义,则执行#if 和 #endif 之间的代码;如果未定义,则不执行内部代码】
#if defined(X) ≡ #ifdef X(检查是否已定义)
例如:
4.嵌套指令
#if defined(S1)
#ifdef OPTION1
//……
#endif
#ifdef OPTION2
//……
#endif
#elif defined(S2)
#ifdef OPTION2
//……
#endif
#endif
例如:
如果宏S1存在,那就执行此条指令下的代码,然后再判断P1,P2哪一个宏存在;而如果是宏S2存在,就执行 #elif defined 这条指令下的代码
七、头文件的包含
7.1 头文件被包含的方式
7.1.1 本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。 如果找不到就提示编译错误。
例如:创建一个 test.h 头文件
源文件所在目录下查找:
在标准位置查找头文件:
如果在当前目录未找到test.h这个头文件,就去标准位置查找
VS2022环境的标准头文件路径:
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt
7.1.2 库文件包含
#include <filenname.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使⽤ " "的形式包含?
答案是可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
7.2 嵌套文件包含
我们已经知道, #include 指令可以使另外⼀个文件被编译。就像它实际出现于 #include指令的地方⼀样。 这种替换的⽅式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。 ⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
例如:
test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{return 0;
}
test.h
void test();
struct Stu
{int id;char name[20];
};
如果直接这样写,test.c 文件中将 test.h 包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。如果test.h 文件⽐较大,这样预处理后代码量会剧增。如果⼯程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头文件被重复引入的问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif
或者:
#pragma once
就可以避免头文件的重复引入。
而在 VS2022 中,头文件在开头会默认有:
八、其他预处理指令
8.1 #error
语法:
#error "错误消息"
作用:强制中断编译过程,并输出自定义错误消息。
典型用途:
检查不满足的编译条件(如依赖的宏未定义)
防止使用不兼容的编译器或环境
提示开发者修复配置问题
例如:
int main()
{
#if !defined(__cplusplus)
#error "本代码必须使用C++编译器!"
#endifreturn 0;
}//如果使用C编译器(而非C++),编译将中断并显示错误消息。
8.2 #pragma
语法:
#pragma 指令内容
作用:向编译器传递特定指令
典型用途:
控制内存对齐(#pragma pack)
禁用编译器警告(#pragma warning(disable: xxx))
确保头文件只包含一次(#pragma once)
其他编译器优化或特殊功能
8.3 #line
语法:
#line 行号 "文件名"
作用:修改编译器报告的行号和文件名