• 【C进阶】——预处理详解


    前言

    预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

    这篇文章,我们就一起来学习一下C语言的预处理。

    1. 预定义符号

    首先给大家介绍一下预定义符号。

    __FILE__      //进行编译的源文件
    __LINE__     //文件当前的行号
    __DATE__    //文件被编译的日期
    __TIME__    //文件被编译的时间
    __STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这些预定义符号都是C语言内置的,我们可以直接使用。

    举个例子:

    #include 
    int main()
    {
    	printf("%s\n", __FILE__);
    	printf("%d\n", __LINE__);
    	printf("%s\n", __DATE__);
    	printf("%s\n", __TIME__);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    2. 预处理指令——#define

    #define到底是什么东西呢?

    #define是 C语言 和 C++ 中的一个预处理指令,其中的“#”表示这是一条预处理命令·。凡是以“#”开头的均为预处理命令,“define”为宏定义命令,“标识符”为所定义的宏名。#define的部分运行效果类似于word中的ctrl+F替换,与常量const相比有着无法替代的优势。

    那它有哪些作用呢?

    2.1 #define 定义标识符

    语法:
    #define name stuff

    比如我们之前经常这样写:#define MAX 100

    MAX的值就是100。
    通过上一篇文章的学习,我们知道#define定义的符号是在预处理阶段就完成替换的。

    当然其它类型的数据也🆗的,不止可以是整型:

    #define MAX 1000
    #define reg register          //为 register这个关键字,创建一个简短的名字
    #define do_forever for(;;)     //用更形象的符号来替换一种实现
    #define CASE break;case        //在写case语句的时候自动把 break写上。
    // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
    #define DEBUG_PRINT printf("file:%s\tline:%d\t \
                              date:%s\ttime:%s\n" ,\
                              __FILE__,__LINE__ ,       \
                              __DATE__,__TIME__ )  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以是关键字,甚至是代码一段代码也可以。

    那现在有一个问题:
    在define定义标识符的时候,要不要在最后加上分号 ; ?

    比如:

    #define MAX 1000;
    
    • 1

    建议不要加上 ; ,这样容易导致问题。
    像这种场景:

    #define MAX 100;
    int main()
    {
    	int max = 0;
    	if (3>5)
    		max = MAX;
    	else
    		max = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这段代码看上去好像没什么问题。
    但是:
    在这里插入图片描述

    它就是一段有问题的代码。

    为什么呢?

    就因为#define MAX 100;我们加了分号。
    替换之后的代码就变成了这样:
    在这里插入图片描述
    相当于max = 100;又跟了一条空语句,但是因为if后面没跟大括号,只能匹配一条语句,这样就出现语法错误了。

    除此之外,#define还有什么作用呢?

    2.2 #define 定义宏

    #define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

    宏的申明方式

    #define name( parament-list ) stuff
    其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
    在这里插入图片描述
    注意:
    参数列表的左括号必须与name紧邻。
    如果两者之间有任何空格存在,参数列表就会被解释为stuff的一部分。

    举个例子:

    现在又一个变量a和b,要求出它们的最大值。
    当然有很多方式,我们可以直接比较,或者写一个函数。
    那可不可以定义一个宏来完成这个功能呢?
    当然!
    我们可以把要比较的a和b作为参数传给宏,那我们写的宏也要能够来接收这两个数。
    那就可以这样写:

    #define MAX(x,y) x>y?x:y
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int max = MAX(a, b);
    	printf("%d\n", max);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    能完成任务吗?
    在这里插入图片描述
    大家会不会感觉好像跟函数有点像啊?

    其实宏跟函数还是不同的,#define定义的宏本质上还是替换。
    我们可以利用Linux环境给大家看一下预处理之后宏替换完的代码:
    在这里插入图片描述
    和函数是不同的,后面我们会详细讲解宏和函数的对比。

    2.3 使用宏的注意事项

    我们再来看这样一个宏:

    #define SQUARE( x ) x * x
    
    • 1

    大家应该很容易看出这个宏的功能,就是求一个数的平方嘛。

    这个宏接收一个参数 x 。
    如果在上述声明之后,你把

    SQUARE( 5 );
    
    • 1

    置于程序中,预处理器就会用下面这个表达式替换上面的表达式:

    5 * 5
    
    • 1

    我们来试一下:

    #define SQUARE( x ) x * x
    int main()
    {
    	int ret = SQUARE(5);
    	printf("%d\n", ret);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行程序:
    在这里插入图片描述
    但是,这个宏写的好不好呢?有没有什么问题呢?

    其实这个宏是存在一点问题的。

    观察下面的代码段:

    #define SQUARE( x ) x * x
    int main()
    {
    	int a = 5;
    	printf("%d\n", SQUARE(a+1));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    大家思考一下结果是啥?

    a = 5,a+1就是6,那6的平方就是36嘛。
    乍一看,你可能觉得这段代码将打印36这个值。

    可事实是这样吗?
    在这里插入图片描述
    结果是11,为什么?

    替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
    printf ("%d\n",a + 1 * a + 1 );
    那就是5+1*5+1,可不就是11嘛。

    这样就比较清晰了,由替换产生的表达式由于操作符优先级的问题并没有按照预想的次序进行求值。

    如何解决,那也很简单:

    在宏定义上加上两个括号,这个问题便轻松的解决了:
    #define SQUARE(x) (x) * (x)
    这样预处理之后就产生了预期的效果
    printf ("%d\n",(a + 1) * (a + 1) );
    在这里插入图片描述

    接着我们再来看一个宏定义:

    #define DOUBLE(x) (x) + (x)
    
    • 1

    它来计算一个数的二倍,这次我们加上了括号:

    #define DOUBLE( x ) (x) + (x)
    int main()
    {
    	printf("%d\n", DOUBLE(5));
    	printf("%d\n", DOUBLE(5 + 1));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行一下:
    在这里插入图片描述
    定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

    int a = 5;
    printf("%d\n" ,10 * DOUBLE(a));
    
    • 1
    • 2

    这将打印什么值呢?

    5的二倍是10,10*10=100。

    看上去,好像打印100,但事实上是这样嘛?
    在这里插入图片描述
    为啥是55了?

    我们发现替换之后:
    printf ("%d\n",10 * (5) + (5));
    乘法运算先于宏定义的加法,所以出现了55的结果 。

    这又这么解决:

    在宏定义表达式的两边再加上一对括号就可以了。

    #define DOUBLE( x ) ((x) + (x))
    
    • 1

    所以我们得出结论

    用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

    2.4 #define 替换规则

    在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

    1. 在调用宏时,首先对其参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

    什么意思呢?举个栗子:

    #define M 10
    #define SUM(x,y) x+y
    int main()
    {
    	int ret = SUM(5, M);
    	printf("%d\n", ret);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这段代码中,调用宏SUM时,首先对其参数进行检查,发现它的第二个参数M是由#define定义的符号,就会首先对M进行替换:
    int ret = SUM (5, 10);
    然后再进行宏的替换:
    int ret = 5+10;

    1. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
    2. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

    注意:

    1. 宏参数和#define 的定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
    就像上面我们定义的宏SUM中的第二个参数M就是其它#define定义的符号。

    2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
    举个栗子:
    在这里插入图片描述
    在这里插入图片描述

    3. #和##

    3.1 #的作用

    我们先来看这样一个代码:

    int main()
    {
    	printf("hello world\n");
    	printf("hello ""world\n");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果会是什么呢?

    首先第一个肯定是"hello world",那第二个呢,会不会跟第一个输出有不同的地方呢?
    在这里插入图片描述
    并没有不同。
    我们发现字符串是有自动连接的特点的。

    那现在有这样一个场景:

    int main()
    {
    	int a = 8;
    	printf("the value of a is %d\n", a);
    
    	float b = 3.5;
    	printf("the value of b is %f\n", b);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行它:
    在这里插入图片描述
    打印出来这样两句话。

    那如果我们不想每次都调用printf,想写一个宏来实现这个功能,应该这么搞呢?

    那我们肯定要把a,b的不同的值作为参数传给宏,那%d,%f是不是也得传过去啊,要不然不知道一什么形式打印啊。
    打印a就传%d,打印b就传%f。

    那定义得宏就要有两个参数:

    #define PRINT(val,format)
    
    • 1

    那宏得内容怎么写:

    #define PRINT(val,format) printf("the value of val is"format"\n",val)
    
    • 1

    这样好像不行,这样的话of后面得val就固定死了,打印得时候不会变成对应的a,b。

    那这样:

    printf("the value of" val "is"format"\n",val)
    
    • 1

    这样也不行,“the value of”,"is"和format都是字符串,能拼起来,但是val 自己一个数字,没法和它们组成字符串啊。

    那这时候,就要用到#了,那它的作用是什么呢?

    使用 # ,可以把一个宏参数变成对应的字符串

    那就可以这样写了:

    printf("the value of " #val " is "format"\n",val)
    
    • 1

    val 前面加上一个#,它就会把参数val转化成字符串。

    我们验证一下:
    在这里插入图片描述
    这样就可以了。

    代码中的 #val 会被预处理器处理为:“val”

    而我们用函数是没法实现这样的功能的。

    3.2 ## 的作用

    #的作用我们知道了,那##的作用又是啥呢?

    ##可以把位于它两边的符号合成一个符号。
    它允许宏定义从分离的文本片段创建标识符。

    什么意思呢?举个例子大家就明白了:

    #define CAT(a,b) a##b
    int main()
    {
    	int helloworld = 10;
    	printf("%d", CAT(hello, world));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    打印helloworld 这个变量的时候,我们没有直接给变量名,而给的是CAT(hello, world),我们知道宏CAT(hello, world)替换之后就是hello##world
    ##就将hello,world合成符号helloworld

    我们看看能不能成功打印:
    在这里插入图片描述
    但是要注意:

    这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

    4. 带副作用的宏参数

    什么叫带有副作用呢?
    比如:

    int main()
    {
    	int a = 2;
    	int b = a + 1;//不带副作用
    	int c = ++a;//带有副作用
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    int b = a + 1;,b的值变成了2,但是a还是1,这就没有副作用;
    但是,int c = ++a;b的值是变成了2,但同时a的值也自增1,这就是带有副作用的。

    而对于宏来说:

    当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。
    副作用就是表达式求值的时候出现的永久性效果。

    举个例子:

    #define MAX(a, b) ( (a) > (b) ? (a) : (b) )
    int main()
    {
    	int x = 3; 
    	int y = 4; 
    	int max = MAX(++x, ++y);
    	printf("max=%d x=%d y=%d\n", max, x, y);//输出的结果是什么?
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    结果会是什么呢?

    首先这个宏的功能我们很容易看出来就是求a, b中的最大值。
    int max = MAX(++x, ++y);
    ++x的值是4,++y的值是5,5和4的最大值是5。
    但结果会是5嘛?
    在这里插入图片描述

    并不是我们所想的。

    这就是宏参数的副作用导致的。

    这据代码替换之后是这样的:

    int max = ((++x) > (++y) ? (++x) : (++y));
    
    • 1

    ++x的值是4,++y的值是5,4大于5不成立,所以问号❓后面的++x不执行,x还是4,++y执行,所以y又自增了一次,变成6。

    这就导致了最终结果与我们预想不同。
    所以:

    尽量不要使用带有副作用的宏参数,以避免产生一些不可预料的结果。

    5. 宏和函数对比

    通过前面的学习,不知道大家有没有一种感觉,就是宏和函数好像有一点相似:

    好像都是我们给它传一些参数,然后它给我们返回一个结果。
    但是,事实上它们是两个不同的东西。

    接下来,我们就对宏和函数进行一个对比:

    1. 宏通常被应用于执行简单的运算。

    比如:求一下两个数的最大值

    当然,要解决这个问题,我们定义一个宏或者写一个函数都是可以的。

    用函数:

    int Max(int x, int y)
    {
    	return (x > y ? x : y);
    }
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int max = Max(a, b);
    	printf("%d\n", max);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    用宏:

    #define MAX(x,y) ((x)>(y)?(x):(y))
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int max = MAX(a, b);
    	printf("%d\n", max);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当然也是可以的:
    在这里插入图片描述

    5.1 宏的优点

    那现在大家思考一下,就对于实现这个小问题来说,函数和宏用哪一个更好?

    使用宏更好,优先选择宏的方法。

    那为什么呢?
    原因有二:
    对于这样一个非常简单的运算来说:

    1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以对于简单的运算来说,宏比函数在程序的规模和速度方面更胜一筹。

    2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。
    反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
    宏是类型无关的。

    5.2 宏的缺点

    当然和函数相比宏也有劣势的地方:

    1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

    我们知道宏是进行替换的,那我们使用一次宏,就会进行一次替换,如何用的次数比较多,而且宏定义的代码也比较长,可能就会导致程序的长度大幅度增长。
    而对于函数来说,我们定义好的函数在程序中只留存一份,我们每次使用,只需要调用一下就行了。

    1. 宏是没法调试的。
    2. 宏由于类型无关,也就不够严谨。

    这是它的一个优点,但同时也是缺点。

    4.宏可能会带来运算符优先级的问题,导致程容易出现错。

    这个其实我们在讲宏的时候就演示过了,在使用宏的时候,有些地方如果我们不加括号,可能就会导致由替换产生的表达式因为操作符优先级的问题而并没有按照我们预想的次序进行求值。

    5.3 宏能做一些函数做不到的事情

    宏有时候可以做函数做不到的事情。

    比如:宏的参数可以是一种数据类型,但是函数不行。

    举个栗子:

    之前的文章里我们已经学过了动态内存开辟。
    如果我们想开辟40个字节的空间放整型数据,可以这样搞:

    int main()
    {
    	int* p = (int*)malloc(sizeof(int) * 10);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而且我们可以实现这样一个宏:

    #define MALLOC(num,type) (type*)malloc(num*sizeof(type))
    
    int main()
    {
    	int* p = MALLOC(10, int);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样是不是更加方便,我们直接把元素个数和类型传给宏,它就帮我们开辟好空间了。

    但是:

    这件事宏可以完成,函数就不行,因为函数不能传一个数据类型作为参数吧。

    5.4 总结

    在这里插入图片描述
    总的来说:

    宏和函数各有优劣,没有决对的谁好谁坏,我们在不同的情况下选择适合的就行了。

    6. 命名约定

    一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。
    那我们平时的一个习惯是:

    把宏名全部大写
    函数名不要全部大写

    当然,也是有例外的,我们其实之前就遇到过:

    之前文章里我们学过的用来求偏移量的offsetof ,它的命名虽然是全小写的,但是它并不是库函数,而是一个宏。
    在这里插入图片描述

    7. #undef

    #undef是什么东西呢?

    我们已经知道#define是用来定义标识符和宏了,那#undef呢?
    这条指令用于移除一个宏定义。
    #undef NAME
    如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除

    举个例子:
    在这里插入图片描述

    移除前我们可以正常使用,#undef移除后我们就不能再使用这个符号了。

    8. 命令行定义

    什么是命令行定义呢?

    许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

    举个例子(命令行定义在vs上不好演示,,这里还是在Linux环境下给大家演示):

    我们写这样一段代码:
    在这里插入图片描述
    我们创建了一个数组,大小是SZ,但是我们并没有定义SZ这个变量。
    那这样可以运行嘛?
    在这里插入图片描述
    肯定是不行的,这里报错说SZ没有定义。

    那有没有什么方法可以解决呢?

    当然,这时就可以使用命令行定义:
    在这里插入图片描述
    大家看,我们在编译时,通过一个命令给SZ指定一个大小,然后运行,就通过了,并且成功打印出了数组元素。

    那命令行定义有什么用处呢?

    当我们根据同一个源文件需要编译出一个程序的不同版本时,我们就可以通过命令行定义来实现。
    假定某个程序中声明了一个某个长度的数组,如果一个机器内存有限,我们需要一个很小的数组,但是另外一个机器内存比较大,我们需要这个数组能够大一些。
    那这时我们就可以通过命令行定义在每次编译时指定数组大小为我们需要的长度,以此来满足我们的需求。

    9. 条件编译

    在编译一个程序的时候我们选择将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

    那什么时候会用到条件编译呢?
    比如说:

    调试性的代码,删除可惜,保留呢又有点碍事,所以我们可以选择性的编译。

    举个例子:

    #include 
    #define __DEBUG__
    int main()
    {
    	int i = 0;
    	int arr[10] = { 0 };
    	for (i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    #ifdef __DEBUG__
    		printf("%d\n", arr[i]);//为了观察数组元素是否赋值成功。 
    #endif //__DEBUG__
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这段代码中printf("%d\n", arr[i]);是我们为了观察数组元素是否赋值成功而增添的语句,那对于这句代码我们就可以使用条件编译,需要观察的时候就让它进行编译,不需要的时候就可以不让他编译。

    那怎么实现呢?我们看到里面有这样一句代码:

    #ifdef __DEBUG__
    		printf("%d\n", arr[i]);
    #endif 
    
    • 1
    • 2
    • 3

    这就是一个条件编译语句,#ifdef __DEBUG__的作用就是如果__DEBUG__这个符号定义了,就会编译它后面控制的语句,如果没符号没定义,就不会编译。

    我们来验证一下:
    在这里插入图片描述

    如果我们现在把#define __DEBUG__注释掉:
    在这里插入图片描述

    接下来我们就来学习一下常见的条件编译指令:

    9.1 单分支条件编译

    #if 常量表达式
    	//...
    #endif
    //常量表达式由预处理器求值。
    
    • 1
    • 2
    • 3
    • 4

    如果常量表达式为真,后面被控制的语句就会参与编译。
    注意条件编译能控制的语句到#endif之前,它们之间可以有很多条语句。

    举个例子:

    int main()
    {
    #if 1
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1为真,所以会参加编译:

    在这里插入图片描述

    这样呢:

    int main()
    {
    #if 1>8
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1>8结果为假,那就不会编译
    在这里插入图片描述

    9.2 多个分支的条件编译

    #if 常量表达式
     //...
    #elif 常量表达式
     //...
    #else
     //...
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其实这个和我们之前学的if else很像的,我相信不需要给大家解释很多,区别就是这个是用来控制语句是否编译的
    需要注意的就是别忘了最后要加上#endif

    9.3 判断是否已定义

    #if defined(symbol)
    #ifdef symbol	前两个是等价的,作用一样
    
    #if !defined(symbol)
    #ifndef symbol	后两个也是等价的
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其实我们一开始给大家举的那个例子就是#ifdef symbol 嘛,它和#if defined(symbol)其实是一样的作用,什么作用呢?

    如果后面的symbol符号是已定义的,那么它们后面跟的语句就会参与编译,反之则不会。

    举个例子:

    #define A 1
    #define B 2
    int main()
    {
    #ifdef A
    	printf("haha\n");
    #endif
    
    #if defined(B)
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    符号AB都是已经被定义过的,所以两个printf语句都会参与编译,最终运行代码可以打印。

    如果我们注释掉或移除定义,当然两个printf语句就不会参与编译了。

    #define A 1
    #define B 2
    int main()
    {
    #undef A
    #undef B	//移除定义
    
    #ifdef A
    	printf("haha\n");
    #endif
    
    #if defined(B)
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    #if !defined(symbol), #ifndef symbol 作用也是等价的,它们又是什么作用呢?

    其实从字面意思就能看出来,它们的作用是如果symbol符号没定义,后面跟的语句才会编译:

    int main()
    {
    #ifndef A
    	printf("haha\n");
    #endif
    
    #if !defined(B)
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这次AB都没有定义:
    在这里插入图片描述

    但是后面的语句参与编译了。
    当然如果定义了,它们就不会参与编译了。

    当然记得它们后面也都要加上#endif

    9.4 嵌套指令

    当然条件编译也支持像ifelse语句那样进行嵌套:

    #if defined(OS_UNIX)
    #ifdef OPTION1
    	unix_version_option1();
    #endif
    #ifdef OPTION2
    	unix_version_option2();
    #endif
    #elif defined(OS_MSDOS)
    #ifdef OPTION2
    	msdos_version_option2();
    #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    条件编译指令的嵌套和ifelse分支的控制也是基本一样的,就不再给大家一一举例了。

    10. 文件包含

    我们已经知道, #include 指令可以使被包它含的那个文件被编译。
    就像它实际出现于 #include 指令所在的地方一样。

    其实在上一篇文章里我们就一起验证过,当我们的程序包含了一个头文件,比如#include ,那么在预处理之后头文件stdio.h中的内容就真的会被替换到代码中。

    这种替换的方式很简单:

    预处理器先删除这条指令,并用包含文件的内容替换。
    这样一个文件被包含10次,那就实际被编译10次。

    10.1 头文件被包含的方式

    10.1.1 本地文件包含
    #include "filename"
    
    • 1

    本地的头文件包含,我们应该使用双引号“ ”

    对于双引号“ ”包含的头文件,查找策略:

    先在源文件所在目录下查找,如果未找到该头文件,编译器就像查找库函数头文件一样在标准位置(标准库里)查该找头文件。
    如果还找不到就提示编译错误。

    在这里插入图片描述

    10.1.2 库文件包含
    #include 
    
    • 1

    对于C标准库里的头文件,我们使用尖括号<>来包含。

    对于尖括号<>包含的头文件,查找策略:

    查找尖括号<>包含的头文件直接去标准路径下去查找,如果找不到就提示编译错误。

    因此:

    对于本地的头文件,我们不能使用尖括号<>包含,只能用双引号“ ”包含
    在这里插入图片描述

    这样是不是可以说,对于库文件也可以使用 “ ” 的形式包含

    答案是肯定的,可以。
    但是这样做查找的效率就低了,当然这样也不容易区分是库文件还是本地文件了

    10.2 解决头文件被重复包含的问题

    有时候,在不经意间我们可能会对一个头文件进行多次包含,而我们自己可能并没有发觉。

    比如这样的场景:
    在这里插入图片描述

    comm.h和comm.c是公共模块。
    test1.h和test1.c使用了公共模块。
    test2.h和test2.c使用了公共模块。
    test.h和test.c使用了test1模块和test2模块。
    这样最终程序中就会出现两份comm.h的内容。
    这样就造成了头文件的重复包含。

    而我们知道:

    一个文件被包含一次,就会被编译一次;包含10次,就编译10次,所以一个头文件如果被重复包含,就会导致编译时间增加,或者其它的一些错误

    那如何解决这个问题呢?
    两个方法!

    10.2.1 条件编译

    每个头文件中加上:

    #ifndef __TEST_H__ 这个符号是我们自己命名的
    #define __TEST_H__
    	//头文件的内容
    #endif
    
    • 1
    • 2
    • 3
    • 4

    什么意思呢?解释一下:

    加上这样一个条件编译之后,我们知道#ifndef __TEST_H__的作用是如果这个符号没定义,后面的代码才会参与编译。
    所以,第一次包含这个头文件的时候,符号还没定义,后面的代码(#endif之前的)会参加编译,而下一句代码#define __TEST_H__就会定义这个符号。
    这样如果我们以后再次包含了这个头文件,此时这个符号已经定义了,那么头文件的内容就不会在参与编译了。

    这样就可以避免一个头文件被重复包含。

    不过,这是一种比较古老的方法,现在,我们可以用一种更简便的方法。

    10.2.2 #pragma once

    这个方法是:

    在头文件中加上这句代码:#pragma once,就可以避免头文件被重复包含。

    其实现在vs上,我们新创建一个头文件,它里面自动就会加上这句代码:
    在这里插入图片描述
    在这里插入图片描述
    所以,以后我们自己定义的头文件,最好都加上#pragma once,就可以很好的避免头文件被重复包含。

    好的,那这篇文章的内容就到这里,希望能帮助到大家,如果有些的不好的地方,也欢迎各位大佬指正,我们一起进步!!!
    在这里插入图片描述

  • 相关阅读:
    mmpose系列 (一):hrnet 基于mmpose 训练body+foot 23点关键点
    工具篇 | 05 | IntelliJ IDEA
    二叉树中查找后继节点问题
    跬智信息(Kyligence)成为信创工委会技术活动单位
    UE4 C++:UPROPERTY宏、属性说明符、元数据说明符
    基于监督学习的多模态MRI脑肿瘤分割,使用来自超体素的纹理特征(Matlab代码实现)
    leetcode day07 最后一个单词的长度
    Docker 部署Harbor 443端口冲突
    C#界面实时显示当前时间 定时器
    【LeetCode:2586. 统计范围内的元音字符串数 | 模拟】
  • 原文地址:https://blog.csdn.net/m0_70980326/article/details/127479073