随笔——预处理详解

news2024/11/8 15:37:41

目录

  • 前言
  • 预定义符号
  • #define
    • #define定义常量
    • #define定义宏
  • 带有副作用的宏参数
  • 宏替换的规则
  • 宏和函数的对比
  • #和##
    • #运算符
    • ##运算符
  • 命名约定
  • #undef
  • 命令行定义
  • 条件编译
  • 头文件的包含
    • 包含方式
    • 嵌套包含
  • 其他预处理指令

前言

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

预定义符号

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;
}

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

带有副作用的宏参数

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

比如:

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;
}


命令行定义

许多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语言深度解剖》

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1813575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BitSet位图进行去重海量数据

问题 怎么在40亿个整数中找到唯一重复的数字? 1.Set的不可重复性 if(set.contains(x))System.out.println("重复的数字是"x);else{set.add(x);}但是&#xff0c;contains()方法消耗的时间&#xff0c;消耗的空间很大&#xff0c;毕竟有约40亿的数据&#xff0c;所…

上位机图像处理和嵌入式模块部署(h750 mcu和图像处理)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们说过&#xff0c;h750和之前的103、407相比较&#xff0c;本身cpu频率比较高&#xff0c;flash大小一般&#xff0c;但是ram比较大&#x…

uni-app前端,社区团购系统搭建部署

目录 前言&#xff1a; 一、社区团购模式&#xff1a; 二、社区团购系统功能&#xff1a; 三、总结&#xff1a; 前言&#xff1a; 区团购系统是一种利用互联网技术和组织力量&#xff0c;通过线上线下结合的方式&#xff0c;为社区居民提供方便快捷的商品和服务采购平台。…

前端三大件速成 05 javascript(2)字符串对象、数组对象、函数对象、BOM对象、DOM对象

文章目录 一、字符串对象1、创建字符串对象的两种方式2、字符串属性3、字符串的方法&#xff08;1&#xff09;编排方法&#xff08;2&#xff09;查询字符串索引&#xff08;3&#xff09;字符串切片&#xff08;4&#xff09;大小写转换&#xff08;5&#xff09;获取指定字符…

Neo4j 桌面版打不开踩坑贴

真的踩坑。。。没有人告诉我为啥桌面版和社区版不能一起下啊&#xff01;&#xff01; 我是先下载了社区版之后再下载的桌面版&#xff0c;结果桌面版界面一直打不开。 尝试了网上多种办法都没效果&#xff0c;好多都是说jdk不兼容导致无法打开&#xff0c;让我从JDK 17 ->…

element 表格第一列合并,第二列展开后出现错位情况

展开后发现蓝色一行挤下来&#xff0c;而且还错位了 解决思路&#xff1a;展开行&#xff0c;在dom上其实是新增了一行的高度&#xff0c;合并上新增一个高度就可以 <el-tablev-loading"tabLoading"fitref"oneRef"height"100%":span-method…

VS Code扩展开发介绍和快速示例

VS Code 介绍 VS Code&#xff08;Visual Studio Code&#xff09;是一款由微软开发的轻量级的免费开源的源代码编辑器&#xff0c;它支持多种操作系统&#xff0c;包括Windows、macOS和Linux。以下是对VS Code的详细介绍&#xff1a; 一、跨平台支持 VS Code是一个真正的跨…

Java语言+前端框架html+Thymeleaf +后端框架springboot开发的UWB智能定位系统源码 UWB三维可视化人员定位系统源码

Java语言前端框架htmlThymeleaf 后端框架springboot开发的UWB智能定位系统源码 UWB三维可视化人员定位系统源码 UWB定位系统基于B/S架构的软件和嵌入式硬件都具有很好的扩展性和兼容性&#xff0c;可以与其他系统接口&#xff08;比如&#xff1a;围界、AB门、高压电网、报警、…

HyperBDR新版本上线,自动化容灾兼容再升级!

本次HyperBDR v5.5.0版本新增完成HCS&#xff08;Huawei Cloud Stack&#xff09;8.3.x和HCSO&#xff08;Huawei Cloud Stack Online&#xff09;自动化对接&#xff0c;另外还突破性完成了Oracle云(块存储模式)的自动化对接。 HyperBDR&#xff0c;云原生业务级别容灾工具。支…

Vue3【十八】Vue3的生命周期

Vue3【十八】Vue3的生命周期 Vue3【十八】Vue3的生命周期 生命周期 vue组件实例在创建时要经历一系列的初始化步骤&#xff0c;在此过程中vue会在何时的时机&#xff0c; 调用特定的函数&#xff0c;从而让开发者有机会在特定时段运行自己的代码&#xff0c; 这些特定的函数统称…

字符串循环遍历抵消、队列的应用-649. Dota2 参议院

题目链接及描述 649. Dota2 参议院 - 力扣&#xff08;LeetCode&#xff09; 题目分析 题目描述的意思&#xff1a;对于一个字符串循环执行抵消操作&#xff0c;&#xff08;R的个数为1时可以使后续的一个D失效&#xff0c;D的个数为1时可以使后续的一个R失效&#xff09;【相…

私人云盘(自动云同步)

一、项目简介 模仿小米的云服务&#xff0c;实现一个通过TCP实现的私人云盘&#xff0c;因为能力有限&#xff0c;所以只实现自动云同步这一个功能&#xff0c;具体可以分为三个小功能&#xff0c;即保持云端和终端数据一致、实现文件的上传与下载以及手动同步 二、涉及到的知…

LayerNorm层归一化

1.背景 与 Batch normalization 不同&#xff0c;Layer normalization 是在特征维度上进行标准化的&#xff0c;而不是在数据批次维度上。像 Batch Norm 它的核心是数据批次之间的归一化【强调的是第 i 批次和第 i1 批次的区别&#xff0c;然后BN去缩小他们的的区别】&#xf…

Jacob环境探索(兼容性、管理员、DLL位置、VS环境,COM权限)

概述&#xff1a; 最近在生产开发实践出现了很多问题&#xff0c;经过了一系列排查&#xff0c;特做如下总结 探索成果&#xff1a; 1. jacob.dll的建议位置 首先jacob的官网&#xff0c;以及官方GitHub&#xff0c;你可以从这里找到DLL文件&#xff0c;以及相关资料然后DLL文…

lxml库在爬虫领域的贡献及应用

重头戏lxml库里面的xpath 一段代码给各位开开胃 这段代码首先导入了lxml库中的etree模块&#xff0c;然后定义了一个包含HTML内容的字符串html。接着&#xff0c;我们使用etree.HTML()函数解析这个HTML字符串&#xff0c;得到一个表示整个HTML文档的树形结构。最后&#xff0c;…

WindTerm使用SSH密钥连接阿里云实例,服务器设置SSH密钥登录

安装Windterm 地址https://github.com/kingToolbox/WindTerm/releases 下载完放到文件夹就可以打开 阿里云开启密钥对 打开阿里云ecs控制台 https://ecs.console.aliyun.com/keyPair/region/cn-wulanchabu 网络与安全->密钥对&#xff0c;创建密钥对&#xff0c;创建成…

STM32项目分享:智能蓝牙手环

目录 一、前言 二、项目简介 1.功能详解 2.主要器件 三、原理图设计 四、PCB硬件设计 1.PCB图 2.PCB板打样焊接图 五、程序设计 六、实验效果 七、资料内容 项目分享 一、前言 项目成品图片&#xff1a; 哔哩哔哩视频链接&#xff1a; https://www.bilibili.c…

改变Layout布局中路由渲染区域页面跳转变全屏

有一个需求需要点击侧边栏跳转页面时变全屏&#xff0c;而不是还在content中

QWidget 属性——windowTitle·windowIcon·qrc

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、windowTitle二、windowIcon三、qrc 一、windowTitle windowTitle 是一个通常用于表示窗口标题…

企业级开源项目,云缓存解决方案:CacheCloud

CacheCloud&#xff1a;简化缓存管理&#xff0c;释放数据潜力- 精选真开源&#xff0c;释放新价值。 概览 CacheCloud是由搜狐视频团队开发的一款开源的Redis缓存云平台&#xff0c;支持Redis多种架构(Standalone、Sentinel、Cluster)高效管理、有效降低大规模redis运维成本&…