随笔——预处理详解

前言

之前我们在《随笔——编译与链接》中对预处理作了大致的说明,但仅仅大致地了解预处理还不够,所以有了本文。

预定义符号

C语言本身就具有⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。

__FILE__ //进行编译的源文件名
__LINE__ //__LINE__ 所在的行号
__DATE__ //文件被预处理的日期
__TIME__ //文件被预处理的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

光说你可能不知道我在讲什么,那就用代码实际试一试:

#include<stdio.h>

int main()
{
    printf("%s\n",__FILE__);
    printf("%d\n",__LINE__);
    printf("%s\n",__DATE__);
    printf("%s\n",__TIME__);
    printf("%d\n",__STDC__);
    return 0;
}

还是和以前一样,先用cd指令切换到.c文件所在文件夹
在这里插入图片描述

对.c文件进行编译,生成可执行程序
在这里插入图片描述

执行当前文件夹下的程序
在这里插入图片描述
执行结果
在这里插入图片描述
那有没有不完全遵循标准C的编译器呢?当然有,比如VS,或许VS有自己的想法:
在这里插入图片描述
在这里插入图片描述
另外
在这里插入图片描述
我们也可以在VScode上生成.i文件看一下
在这里插入图片描述


使用clear清除一下控制台
在这里插入图片描述
注释掉(我的块注释快捷键是shirt+Alt+A,不知道你们是不是)(可以在【文件】-【首选项】-【键盘快捷方式】中搜索查看)
在这里插入图片描述

#define

在之前的文章(《随笔——自定义类型:结构体》《随笔——自定义类型:联合和枚举》)中,我们曾稍微点了一下#define,现在我们将系统学习它。

#define定义常量

#define如何定义常量呢?
格式如下:

#define name stuff

在预处理阶段,所有与name相同的标识符都会被替换成stuff,在所有替换完成后,#define name stuff会被清除(注意:这个stuff是name+空格后的同行所有内容)

#include<stdio.h>

#define MAX 1000
#define STR "hello word"
#define F 1.25f

int main()
{
    int n = MAX;
    char * str = STR;
    float f = F;
    printf("%d\n",n);
    printf("%s\n",str);
    printf("%.2f\n",f);
    return 0;    
}

重新生成.i文件看一看
在这里插入图片描述
在这里插入图片描述


中间指令敲错了,把源文件删了,所以重写了。


编译并执行

在这里插入图片描述
还有一些例子

#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__ )

可能你不清楚这个for(;;)是个什么东西,其实它是省略循环变量初始化,判断,调整,循环内容的for循环,由于没有循环变量判断,意味着判断条件恒为真,会永远循环下去,就像它的名字do_forever。
此时

do_forever;
//相当于
while(1);

问:使用#define定义常量时要不要加分号
这要看你实际上是怎么用的,我在前面曾经说过,stuff是name+空格后的同行所有内容;这意味着,如果加上分号的话,文本替换的时候也会把分号一起替换过去,比如下面的代码

#include<stdio.h>

#define MAX 1000;

int main()
{
    int n = MAX;
    printf("%d\n",n);
    return 0;    
}

你别说,确实可以跑起来:
在这里插入图片描述
但这样是有危险的,我们看.i文件:
在这里插入图片描述
其中的int n = 1000;;实际被当成两条语句,一条是表达式语句(int n = 1000;)另一条是空语句(;)

下面我们写个跑不动的

#include<stdio.h>

#define MAX 1000;

int main()
{
    int n = 0;
   if(1)
   n = MAX;
   else
    n = 0;
    return 0;    
}

配对的if else语句间只能有一条语句,这里没打大括号,这两条语句无法形成一条复合语句,于是if else无法配对,就跑不了了;所以最好不要加分号。

当然也不能一刀切,最后还是要看你到底怎么用。如果你有能力把“俗手”打成“妙手”(2022全国新高考Ⅰ卷作文),那你就用呗。

#define定义宏

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

下面是宏的声明方式:

#define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。

宏的特点是灵活,灵活既是宏最大的优点,也是其最大的缺点。在后续的阅读中,你将会体验到这一点。

如果你是第一次看到宏这个概念,你可能会觉得它很抽象,但别担心,再往下看看你就懂了。

下面我们通过宏求一个数的平方:

#include<stdio.h>

#define SQUARE(x) x*x

int main()
{
    int a = 5;
    printf("a^2 = %d\n",SQUARE(a));
    return 0;
}

在这里插入图片描述
看一下.i文件
在这里插入图片描述
上面的宏其实是有问题的,我们现在再把代码稍微改一下:

#include<stdio.h>

#define SQUARE(x) x*x

int main()
{
    int a = 5;
    printf("(a+1)^2 = %d\n",SQUARE(a+1));
    return 0;
}

看看结果:
在这里插入图片描述
很明显,出问题了,结果应该是36,怎么是11呢?

看看.i文件就知道了
在这里插入图片描述看到了吗?宏是直接替换的,运算顺序出错了:

怎么解决呢?有两种解决方案:

第一种,暂时性地解决:
在这里插入图片描述
第二种:一劳永逸地解决:
在这里插入图片描述
你看,结果对了。
在这里插入图片描述
看看.i文件
在这里插入图片描述

你以为只加一层括号就万事大吉了?不不不,再换一个宏看看:

#include<stdio.h>

#define SQUARE(x) (x)*(x)
#define DOUBLE(x) (x)+(x)

int main()
{
    int a = 5;
    printf("(a+1)^2 = %d\n",SQUARE(a+1));
    printf("10*(a+a) = %d\n",10*DOUBLE(a));
    return 0;
}

在这里插入图片描述
我觉得即使不看.i文件你也知道原因了:
在这里插入图片描述

所以还要加上一层括号

#include<stdio.h>

#define SQUARE(x) ((x)*(x))
#define DOUBLE(x) ((x)+(x))

int main()
{
    int a = 5;
    printf("(a+1)^2 = %d\n",SQUARE(a+1));
    printf("10*(a+a) = %d\n",10*DOUBLE(a));
    return 0;
}

在这里插入图片描述
总结:所以在使用宏定义求表达式的值时,一定要多加括号,从而避免在使用过程中参数中的操作符或邻近操作符之间的运算顺序出错。

不过,什么事都不是绝对的,在某些特殊情况下,加括号反而会出问题

比如若要实现宏offsetof:该宏用于计算结构体中某变量相对于首地址的偏移

#include<stdio.h>

 //什么意思呢?就是把0强制转换成结构体类型指针,相当于地址0是这个结构体的起始地址
 //注意,m不要带括号,本身->的结合律就是从左往右,一带括号就会访问出错
#define My_offsetof(s,m) ((size_t)(&(((s*)0)->m)))

typedef struct test1
{
	char c1;
	int i;
	char c2;
}test1;

int main()
{
	printf("%zd\n", My_offsetof(test1, i));
	return 0;
}

带有副作用的宏参数

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

比如:

x+1;//不带副作用
x++;//带有副作用

虽然最后得到的值都是x+1,但是,第二行中x的值也发生了变化

现在我们写个宏,用来找出两个数中的较小数

#include<stdio.h>

#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{
    int a = 3;
    int b = 5;
    int m = MAX(a++,b++);
    printf("m==%d\n",m);
    printf("a==%d\n",a);
    printf("b==%d\n",b);
    return 0;
}

在这个代码中,我本来只想让ab自加一次,可最后结果是什么呢?

在被替换后,代码就变成了

int m = ((a++)>(b++)?(a++):(b++));

首先是(a++)>(b++),后置加加,先使用,后加加;所以是比较3和5,3>5吗?不大于,执行分号后面的;使用过后ab自加一,此时a变成了4,b变成了6,最后是(b++),先使用,于是m接收到了6,然后b再自加一,变成了7;所以,最后a等于4,b等于7,m等于6。

看看结果:
在这里插入图片描述
再看看.i文件吧:
在这里插入图片描述

你说这要是ab都自加二倒也能接受,但ab中一个是自加一,一个是自加二,而且到底是谁自加一谁自加二还不确定,要看a和b的具体值。

宏替换的规则

在程序中使用#define定义常量和宏,替换时需要涉及几个步骤。

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

简单来说,就是一股脑全替换

比如对于这个代码来说

#include<stdio.h>

#define C 7
#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{
    int a = 3;
    int b = 5;
    int m = MAX(C,MAX(a,b));
    printf("m==%d\n",m);
    return 0;
}

其.i文件是这样的
在这里插入图片描述

注意:

  1. 由 #define 定义的常量和宏在定义时可以嵌套其他已经定义的常量和宏(当然这些常量和宏必须在其之前定义),而在使用时,除自身外也可以嵌套其他定义的常量和宏,或者参数不同的同种宏。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

比如

定义时的嵌套:

  • 在定义一个宏时,可以使用其他已经定义的宏。需要注意的是,嵌套使用的宏必须在当前宏之前定义。

例如:

   #define PI 3.14
   #define CIRCUMFERENCE(r) (2 * PI * (r))

在定义 CIRCUMFERENCE 宏时,使用了已经定义的 PI 常量。

使用时的嵌套:

  • 在使用一个宏时,可以嵌套使用其他已经定义的宏,或者参数不同的同种宏。

例如:

   #define SQUARE(x) ((x) * (x))
   #define DOUBLE(x) ((x) + (x))
   
   int area = SQUARE(5);
   int double_area = DOUBLE(SQUARE(5));

在使用 DOUBLE 宏时,嵌套使用了 SQUARE 宏

定义时的自身嵌套:

  • 一个宏不能直接或间接地嵌套自身,否则会导致无限递归替换,预处理器会报错。

例如:

   #define RECURSIVE_MACRO(x) RECURSIVE_MACRO(x)

使用时的自身嵌套:

  • 好吧,这个根本打不出来,可以无视了。

字符串常量的内容并不被搜索:

例如:

#include<stdio.h>

#define PI 3.14f

int main()
{
    printf("PI是%f\n",PI);
    return 0;
}

你看字符串里的就不能替换
在这里插入图片描述

宏和函数的对比

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

比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整形、长整型、浮点型等等。宏的参数是类型无关的

《函数栈帧的创建与销毁》中,我们曾提到使用函数时需要经历三个阶段:新函数栈帧的准备和分配、函数的实际执行,以及函数栈帧的释放和参数传递。对于较简单的小型计算,这些阶段的时间占比分别可能是22%、53%和25%,使用宏定义可以节省47%的时间。但对于复杂计算,时间占比分别可能是1%、98%和1%,此时使用宏定义只能节省2%的时间。此外,宏的使用本身存在一定风险,因此在这种情况下应优先选择函数。

如果用函数实现比较两个数大小的功能,那么这两个数的类型必须是固定的。
比如:

int max(int x, int y)
{
    int ret = 0;
    ret = x >= y ? x : y;
    return ret;
}

如果这两个数是浮点数,那么这个函数将无法使用。
宏就不用担心类型,从这个角度来说,这是一种优点。


和函数相比宏的劣势:

  1. 每次使用宏的时候,⼀份宏定义的代码将被插入到程序中。除非宏比较短,否则将会大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏没有类型检查,所以不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程序出现问题。

一,使用函数只需调用目标函数即可,而宏会逐条展开,增加预处理器的负担。二,宏在预处理阶段已被全部展开,这意味着调试时的代码与我们肉眼所见的代码不一致。三,以比较两个数大小为例,如果输入的是一个字符和一个整型,函数会直接报错,而宏则会盲目地将这两个数带入,可能引发意想不到的错误。四,此外,宏的使用需要多加括号,这一点我们之前已经提到过。


宏也有自己的独门绝技,由于没有类型检查,宏的参数甚至可以是类型,这是函数无论如何也做不到的。
比如:

#include<stdio.h>
#include<stdlib.h>

#define Malloc(n, type) (type*)malloc(n*sizeof(type))

int main()
{
	//以往,我们开辟一个int[5]的数组是这样写的
	int* p1 = (int*)malloc(5 * sizeof(int));
	//有了宏之后,可以这样写:
	int* p2 = Malloc(5, int);

	//略

	return 0;
}

宏和函数的⼀个对比

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅增长函数代码只出现在一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回时的额外的开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在定义时多打括号函数参数只在传参的时候求值一次,它的结果值传递给函数,表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对类型的操作是合法的,它就可以使用任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们的执行内容是相同的
调试宏是不方便调试的函数可以逐语句调试
递归宏是不能递归的函数可以递归

在C++中,关键字inline用于创建内联函数,内联函数既具有函数特点,也具有宏特点

#和##

#运算符

在宏定义中,# 运算符用于将宏参数转换为字符串。这称为字符串化。当在宏定义中使用 # 运算符时,宏参数会被转换为一个字符串常量。

#include<stdio.h>

int main()
{
    //比如,现在有三个参数,我想把它们分别打印出来
    int a = 4;
    int b = 2;
    float c = 1.25f;

    printf("The value of a is %d\n",a);
    printf("The value of b is %d\n",b);
    printf("The value of c is %.2f\n",c);
    return 0;
}

在这里插入图片描述
我们发现这三个printf的内容很相近呀,能不能把它们合并呢?

当然可以,但在修改之前,我们要先知道一个事实:

在C语言中,字符串常量可以自动连接。这意味着当两个或多个字符串常量放在一起时,编译器会将它们连接成一个单一的字符串。这种现象称为字符串连接。

在这里插入图片描述
那#运算符又是怎么回事呢?

#include<stdio.h>

#define Printf(x) printf(#x"\n")

int main()
{
	Printf(a);
	Printf(b);
	Printf(c);
	return 0;
}
//会打印出来什么呢?

在这里插入图片描述
看看.i文件
在这里插入图片描述


回到刚开始的例子,我们就可以这样写:

#include<stdio.h>

#define PRINTF(v, format) printf("The value of " #v " is " format  "." "\n", v )

int main()
{
	int a = 3;
	int b = 4;
	float c = 1.25f;
	PRINTF(a, "%d");
	PRINTF(b, "%d");
	PRINTF(c, "%.2f");
	return 0;
}

在这里插入图片描述

##运算符

##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称为记号粘合。这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。

这里我们想想,写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。(什么?为什么不直接用宏定义?这里只是一个示例,你就把它当成内部运算很复杂的那种)

int max_int(int x, int y)
{
	return x >= y ? x : y;
}

float max_float(float x, float y)
{
	return x >= y ? x : y;
}

我们发现,这两个函数其实很类似呀,能不能把它们合并成一个通用模版呢?
当然可以,此时##就派上用场了:

#include<stdio.h>

#define GENERALMAX(type) \
		type max_##type(type x, type y)\
		{   \
			return x >= y ? x : y;\
		}

GENERALMAX(int);
GENERALMAX(float);


int main()
{
	printf("%d\n", max_int(3, 5));
	printf("%.2f\n", max_float(3.12f, 6.25f));
	return 0;
}

在这里插入图片描述
看看.i文件
在这里插入图片描述
在预处理器看来,max_##type中有两个字符,一个是max_另一个是type,type就是宏参数呀,于是就被替换了,替换之后,预处理器看到这两个字符中间还有一个##运算符,于是就把这两个字符合并成一个字符了。

如果用的是max_type,预处理器会说,这个字符和宏参数不一样,所以我不替换;
如果用的是max_ type,预处理器会替换,但替换之后不把这两个字符合起来,函数名只能是一个字符呀,所以函数会定义失败。

当然,这还是不方便调试的,应该先把这个通用模版多试几次,尽可能优化完善,确定没问题再写成宏。

命名约定

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

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

当然,这只是一个建议,这个习惯的根本目的是用来区分的,只要形成自己的一套区分习惯就行了
比如,我的做法是:
对于没有函数参与的宏,其名全大写;
对于有函数参与的宏,在函数名的基础上进行修改,作为宏名;
如果没有把原函数的用法固定化,那只首字母大写,比如前面的Malloc,和原函数用法差别不大,那只首字母大写
如果把原函数的用法固定化,那就全大写,比如前面的PRINTF,它的字符输出已经几乎固定了,那全大写

#undef

这条指令用于移除⼀个宏定义。本来预处理器是确定再也找不到一个对应的匹配标识符,再把那行#define删了,现在,你用这个指令,就相当于把那行#define提前删了。这适用于什么场景呢?比如你已经创建好一个宏了,并且也用完了,你确定之后不会再用了,然后你想设计一个新的宏,结果起名困难症犯了,你觉得之前那个宏名字挺不错,反正之后也用不上了,那就先用#undef消除这个宏定义,然后,再对其重新定义。(这里我用的宏是泛称,既包括常量,也包括宏定义)

#include<stdio.h>

#define MAX 1024

void get_value1(int* p)
{
	*p = MAX;
}
// 在这个函数中使用了 MAX 宏

//确定之后不会再使用 MAX 宏了
#undef MAX

int main()
{
	int a = 0;
	int b = 0;
	get_value1(&a);

	// MAX 这名不错,重新定义 
#define MAX 2048
	b = MAX;

	printf("%d\n", a);
	printf("%d\n", b);

	return 0;
}


注意:使用#undef取消宏定义时,不需要宏参数
比如,有宏定义:

#define CASE(number,statement) case number: statemment; break

消除只需要写

#undef CASE

即可


命令行定义

许多C的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性就派上用场了。(假定某
个程序中声明了⼀个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外⼀个机器内存大些,我们需要⼀个数组能够大些。)

#include<stdio.h>

int main()
{
    int arr[sz];
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        arr[i] = i + 1;
    }
    for(i = 0; i < sz; i++)
    {
        printf("%d ",arr[i]);
    }
    return 0;
}

你看这个sz既不是变量。也不是常量,完全没有定义,怎么让它跑起来呢?
此时就可以用-D指令,-D指令用于在编译时定义预处理器宏。它等效于在源代码中使用 #define 指令。通过使用 -D 选项,可以在命令行中定义宏,而不需要在源代码中进行修改。
比如对这个代码使用指令:

gcc main.c -D sz=10 -o main

看,跑起来了:
在这里插入图片描述
再换一个参数
在这里插入图片描述

条件编译

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

比如:程序出bug了,你绞尽脑汁写了一个调试代码,用来检测哪里出了问题,现在你解决bug了,这个调试代码毕竟是辛辛苦苦写出来的,不想删,或者以后可能还会用到,那就可以对其选择性编译

#include<stdio.h>

//定义了__DEBUG__,虽然没有定义内容,但也是定义了
#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i + 1;
	}
	
	//if(如果)def(被定义)  为真,就编译代码;为假,就不编译
#ifdef __DEBUG__

	//调试代码,调试是否赋值成功
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}

	//end(条件编译的末尾)
#endif

	return 0;
}

看看结果:
在这里插入图片描述
那我要是取消定义呢?

#include<stdio.h>

 //定义了__DEBUG__,虽然没有定义内容,但也是定义了
//#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i + 1;
	}

	//if(如果)def(被定义)  为真,就编译代码;为假,就不编译
#ifdef __DEBUG__

	//调试代码,调试是否赋值成功
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}

	//end(条件编译的末尾)
#endif

	return 0;
}

在这里插入图片描述
常见的条件编译指令
1.
#if 常量表达式
//…
#endif
//常量表达式由预处理器求值。

下面就不逐一执行了,你们看明暗对比
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2. 多个分⽀的条件编译

#if 常量表达式
//...
#elif 常量表达式
//...
#elif
//...
#endif

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 判断是否被定义
int main()
{
	//如果是定义了就编译,有两种写法

	//写法1
#if defined(M)
	printf("%d\n", 1);
#endif

	//写法2
#ifdef M
	printf("%d\n", 2);
#endif


	//如果是定义了就不编译,也有两种方法

	//写法1  !取反
#if !defined(M)
	printf("%d\n", 4);
#endif

	//写法2
#ifndef M
	printf("%d\n", 8);
#endif
	return 0;
}
  1. 嵌套指令(就是像if else那样嵌套)
#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

你随便找个标准库头文件看看,为了在不同的平台上都能跑得动,里面一堆#define 条件编译

头文件的包含

包含方式

 //对于标准头文件来说,使用<>来包含
#include<stdio.h>

//对于本地头文件来说,使用""来包含
#include"add.h"

//<>和""的查找顺序是有差别的
//""先查找项目文件夹(源程序所在目录),找不到再去标准头文件的位置查找,如果还是查找不到,就报错
//<>直接从标准头文件的位置开始查找,如果查不到,直接报错

//这意味着对于标准头文件使用""也是可行的,但很明显,这样做会降低查找效率,也不容易区分是库⽂件还是本地⽂件了

int main()
{
	int a = 3;
	int b = 4;
	printf("%d\n", add(a, b));
	return 0;
}

//对于VS2022来说,标准头文件主要存储在两个位置,具体位置因安装位置的不同而不同
//这两个位置分别用于储存
//一些与系统相关的头文件,如stdio.h
//一些与C语法关系更大的头文件,如limits.h
//可以使用Everything(https://www.voidtools.com/zh-cn/)对上述两个举例头文件进行搜索

在这里插入图片描述

//add.c
int add(int x, int y)
{
	return x + y;
}
//add.h
int add(int x, int y);

嵌套包含

之后我们开发一些更为复杂的项目时,可能会遇到头文件嵌套或者说重复包含的情况。
比如说现在有四份原码,分别命名为ground.c ,middle_1.c,middle_2.c,top.c,
middle_1.c和middle_2.c都会调用ground.c ,top.c则会调用middle_1.c和middle_2.c
这样到最后,头文件展开的时候ground.h就会重复出现,如果ground.h很长,编译器
的负担就会大大增加。
在这里插入图片描述
再换个例子,比如我们这样写:

//main.c
#include<stdio.h>

#include"add.h"
#include"add.h"
#include"add.h"
#include"add.h"
#include"add.h"

int main()
{
    int a = 3;
    int b = 4;
    printf("%d\n", add(a, b));
    return 0;
}
//add.c
int add(int x, int y)
{
	return x + y;
}
//add.h
int add(int x, int y);

预处理之后会怎么样呢?
在这里插入图片描述
我们看到已经出现了重复包含

这时候前面学的条件编译指令就派上用场了

我们把add.h稍微改一下,其它代码不变

#ifndef __ADD_H__

#define __ADD_H__

int add(int x, int y);

#endif

在预处理一下:
在这里插入图片描述
只有一份了。一旦add.h出现一次,就会定义__ADD_H__,下一次再遇到add.h,预处理器看到开头是
#ifndef __ADD_H__,然后__ADD_H__又是被定义过的,于是就会跳过这个代码段。

如果你嫌末尾还要写#endif比较麻烦,就可以用#pragma once一行指令解决。

#pragma once

int add(int x, int y);

效果都是一样的
在这里插入图片描述

这样就可以避免头文件的重复引入。

其他预处理指令

#error
#pragma
#line
...
不做介绍,自己去了解。(其实是我还没查)
#pragma pack()在《随笔——自定义类型:结构体》已经介绍过了
具体参考《C语言深度解剖》

《随笔——自定义类型:结构体》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值