0.前言
您好,这里是limou3434的一篇博客,感兴趣您可以看看我的其他博文系列。本次我主要给您带来了C语言有关预处理的知识。
1.宏的深度理解与使用
1.1.数值宏常量
#define PI 3.1415926
注意define和#之间是可以留有空格的
1.2.字符宏常量
#include <stdio.h>
//注意下面这个字符串要加上反斜杠得转移,整体也要加上双引号,也可以带上续行符
#define PATH "C:\\users\\\
limou_file"
int main()
{
printf("%s", PATH);//成功打印
}
1.3.使用宏充当注释
//gcc会好演示一些
#include <stdio.h>
//下面这个语句是没有办法充当注释使用的,因为“先去注释仔宏替换”
#define ANNOTATION //这里得注释被去掉了
int main()
{
ANNOTATION printf("abcd\n");//那么这里就是没有内容(空)的宏,所以前面就用空来替代了
return 0;
}
包含但不仅限于这四个(为什么不直接转为二进制呢?为了站在巨人的肩膀上,因为直接转化成二进制的成本太大,先转化为已有的汇编代码,再由汇编代码转为二进制,则转化成本更小)
1.预处理:头文件展开、去注释、宏替换、条件编译(先“去注释”、“再宏替换”)
2.编译:C语言翻译为汇编语言
3.汇编:将汇编语言转化为可重定向目标文件(可被链接)
4.链接:自身程序+库文件进行关联(静态链接、动态链接),形成可执行程序
1.4.宏定义表达式
- 宏定义表达式在使用的时候尽量加上括号就行,但是还是有些东西需要我们去注意
#include <stdio.h>
#define INT_VAL(a, b) \
a = 0;\
b = 0; //尽管可以这么写,但是由于分号的存在容易出错
int main()
{
int x = 10;
int y = 20;
INT_VAL(x, y);
printf("%d %d", x, y);
if(1)
INIT_VAL(x, y);
else
printf("nihao\n");
return 0;
}
//那么如果在宏里面加上花括号呢?也同样不行,如果这么做,有的人写的if else语句比较规范,喜欢加上花括号。这个时候照样出错(花括号后面有分号了,这是if else语句中不被允许的写法),那么有没有什么方法可以在宏里面写入大量的代码块呢?
- 大块代码的宏编写可以使用do while循环,这种结构也叫“do-while-zero结构”,这是一种大量使用的编码技巧
#define DEF do{/*某些代码*/}while(0)
int main()
{
//带花括号
if(1)
{
DEF;
}
else
{
;
}
//不带花括号
if(1)
DEF;
else
;
}
//这样会先执行循环的判定条件,就完美解决了“带花括号”或“不带花括号”时,带上分号出错的两种情况
- 宏调用时,宏和“()”之间可以使用空格
1.5.取消宏定义#undef(或叫“限定宏的有效范围”)
- 在源文件的任何地方宏都可以被定义,宏的作用域从定义开始往后都是有效的。(在宏定义的后方所有代码文本只要有宏的存在都可以被替换,但是写在前面的代码文本哪怕有也不会被替换)
void function(void)
{
printf("%d", NUM);
}
int main()
{
function();
#define NUM 100
printf("%d", NUM);
return 0;
}
- #undef是为了辅助宏的使用范围,不过在使用的时候也有一些需要注意的地方(其实只需要一条一条宏语句看下去,逐一执行宏替换就行,这样理解宏就不会出问题)
注意一
int main()
{
#define X 3
#define Y X*2 //注意这里的X不会先被前一句语句替换
#undef X
#define X 2
int z = Y;
printf("%d", z);
return 0;
}
//结果为4,对于“int z = Y”这条语句来说,可见的只有“#define Y X*2”和“#define X 2”两条语句
注意二
#define M 10
int main()
{
printf("%d\n", M);//这里在函数调用之前就被替换了
}
int main()
{
#undef M
show();//可以正常打印10
}
- 宏尽量不在代码块中使用#define和#undef,尽管这是合法的,但是会让人误解这个宏有局部的作用域
- 尽量使用不同的函数而不使用宏定义表达式,出现问题比较难以调试
2.条件编译的基本使用与理解
条件编译做的是代码裁剪的工作,例如:著名的Linux的内核在功能上也是使用条件编译来进行功能裁剪,来满足不同平台的软件
2.1.条件编译有多种写法
2.1.1.写法一(判断宏是否被定义,在源代码出场率较低)
#ifdef 宏标识符//一般很少写多分支
//code1
#elif 宏标识符
//code2
#else
//code3
#endif
2.1.2.写法二(判断宏是否没被定义,在源代码出场率较低)
#ifndef 宏标识符//一般很少写多分支
//code1
#elif 宏标识符
//code2
#else
//code2
#endif
2.1.3.写法三(判断宏是真还是假,在源代码出场率还可以)
#if 常量表达式(如果宏没被定义,默认为假。如果是空宏,则会报错)
//code1
#elif 常量表达式
//code2
#else
//code3
#endif
2.1.4.写法四(使用#if实现#ifdef和#ifndef)
从以下的代码可以看到#if可以实现的功能很多,完全可以替代很多的条件编译指令
//1.检测宏是否被定义#ifdef
#if define(宏名)
//某些code
#else
//某些code
#endif
//2.测宏是否没被定义#ifndef
#if !define(宏名)
//某些code
#else
//某些code
#endif
//3.检测两个以上的宏是否都被定义
#if (define(宏1) && define(宏2))//最外层的括号最好加上,更加严谨
//code1
#else
//code2
#endif
//4.检测两个以上的宏中的其中一个是否没被定义
#if !(define(宏1) && define(宏2))//最外层的括号最好加上,更加严谨
//code1
#else
//code2
#endif
//5.检测两个以上的宏的其中一个是否被定义
#if (define(宏1) || define(宏2))//最外层的括号最好加上,更加严谨
//code1
#else
//code2
#endif
2.1.5.写法五(条件编译的嵌套)
//条件编译是允许多层嵌套的
#if define(宏名)
#if define(宏名)
//code1
#endif
#else
//code2
#endif
2.2.“宏是/否被定义”和“宏为真/假”
- 这两种是不一样的:宏定义不管真假,宏真假必定是有被定义的。
- 不过注意#define 某标识符这种写法也算是定义了宏,即使后面没有任何值,这种的宏可以叫作“空宏”
2.3.条件编译的意义
快速实现版本维护、方便代码在不同平台移植
2.4.在命令行中定义宏(但是这样的应用场景不多)
- 比如linux下的命令
gcc .c文件 -D 宏名=宏值
- 在VS2022中,可以到“选中项目->属性->配置属性->C/C+±>预处理器(相当于Linux下的命令)->预处理定义->将里面追加“;宏名=宏值”。这样不在源文件中定义宏也可以正常使用宏。(请注意,在C语言中C、CPP等名称会有些敏感,可能会出现一些不可预知的错误,请尽量不出现这样的宏名)
2.5.文件包含的本质
2.5.1.避免重复包含头文件的方法
为了避免头文件被重复包含,有两种方式:一是使用“#pragma once”,而是使用“条件编译”
#ifndef _TEST_H_ //根据自己的头文件名字命名
#define _TEST_H_ //根据自己的头文件名字命名
//然后放入一些头文件的内容
#endif
2.5.2.头文件展开的本质
那么什么是头文件展开呢?可以简单理解为将头文件内容“拷贝”到目标源文件,但是这种“拷贝”是经过一定处理的
3.“#”和“##”符号
首先需要做一些铺垫,在C语言里多个字符串会自动连接,即:“abcd"和"efgh"这两串字符串如果相邻,就会连接在一起成为"abcdefgh”,C语言将两者视为一串字符串
3.1.“#”符号
本质是将对应的字面值转化为字符串(这一替换过程在Linux下gcc的预处理中会更加清晰)
int main()
{
#define STR(X) #X
printf("PI:"STR(3.1415926)"\n");//这里被替换程“#3.1415926”,然后C语言将这样带有#的内容视为字符串
return 0;
}//直接打印“PI:3.1415926”
而这一特性还可以利用起来:写成转化字面值为字符串的宏定义,而非直接写算法做处理
#define TOSTRING(S) #S
int main()
{
char str[64] = TOSTRING(1000000);//但是注意,这个括号有什么就转化什么,放入变量的话只会打印变量的名字(实际上这个时候变量也还没开辟空降)
printf("%s", str);
return 0;
}
3.2.“##”符号
将宏参组合形成一个全新的符号(不是字符串,要区分开来)
#define NUMBER(n) number##n
int main()
{
int NUMBER(1) = 100;
int NUMBER(2) = 1000;
int NUMBER(3) = 10000;
printf("%d\n", NUMBER(1));
printf("%d\n", NUMBER(2));
printf("%d\n", NUMBER(3));
return 0;
}
4.常见的预处理符号
除了#define、#include、#ifdef等,还有一些比较常用的,但是有的部分我只是列出没做解释,您可以试着查询一下。
4.1.#pragma
用于给编译器传递指令或控制编译器的行为
4.1.1.#pragma message():编译时的消息提醒
和#error最大的区别是:代码会通过,只是做一个提醒
4.1.2.#pragma once:防止头文件被包含
4.1.3.#pragma code_seg
4.1.4.#pragma hdrstop
4.1.5.#pragma warning
比如:#pragma warning(disable:4996)
4.1.6.#pragma comment
4.1.7.#pragma pack()
4.2.#error
用于输出错误信息并停止编译,其最核心的作用就是可以自定义编译错误
4.3.#line
用于定制代码行号和文件名称
预定义符号“FILE”在预处理期间做处理,打印文件名
预定义符号“LINE”在预处理期间做处理,打印当前行号
4.4.#warning
用于输出警告信息
4.5.#include_next
用于引用下一个同名的头文件,主要是用于避免头文件重复包含的问题