随笔——预处理详解

目录

  • 前言
  • 预定义符号
  • #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.25fint 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;elsen = 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*xint main()
{int a = 5;printf("a^2 = %d\n",SQUARE(a));return 0;
}

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

#include<stdio.h>#define SQUARE(x) x*xint 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.14fint 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 1024void get_value1(int* p)
{*p = MAX;
}
// 在这个函数中使用了 MAX 宏//确定之后不会再使用 MAX 宏了
#undef MAXint main()
{int a = 0;int b = 0;get_value1(&a);// MAX 这名不错,重新定义 
#define MAX 2048b = 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(条件编译的末尾)
#endifreturn 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(条件编译的末尾)
#endifreturn 0;
}

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

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

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

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

  1. 判断是否被定义
int main()
{//如果是定义了就编译,有两种写法//写法1
#if defined(M)printf("%d\n", 1);
#endif//写法2
#ifdef Mprintf("%d\n", 2);
#endif//如果是定义了就不编译,也有两种方法//写法1  !取反
#if !defined(M)printf("%d\n", 4);
#endif//写法2
#ifndef Mprintf("%d\n", 8);
#endifreturn 0;
}
  1. 嵌套指令(就是像if else那样嵌套)
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_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 onceint add(int x, int y);

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

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

其他预处理指令

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【ARM Cache 及 MMU 系列文章 6 -- Cache 寄存器 CTR_EL0 | CLIDR | CCSIDR | CSSELR 使用详解 1】

请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】 及【嵌入式开发学习必备专栏】 文章目录 Cache 常用寄存器Cache CSSELR 寄存器Cache CSSELR 使用场景Cache CSSELR 操作示例 Cache CLIDR 寄存器LoUU 介绍LoUU 使用 LoUIS 介绍CLIDR 使用 Cache CCSIDR 寄存器Cache CTR_EL0 C…

中科数安 |-公司办公透明加密系统,数据防泄漏软件

#数据防泄漏软件# 中科数安是一家专注于提供企业级数据防泄漏解决方案的公司&#xff0c;其办公透明加密系统是专为保护企业内部核心数据资料设计的。 PC地址&#xff1a;——www.weaem.com 该系统通过以下主要功能模块实现高效的安全防护&#xff1a; 文档透明加密&#xff1…

滴滴出行 大数据研发实习生【继任】

大数据研发实习生JD 职位描述 1、负责滴滴核心业务的数据建设&#xff0c;设计并打造适应滴滴一站式出行平台业务特点的数仓体系。 2、负责抽象核心业务流程&#xff0c;沉淀业务通用分析框架&#xff0c;开发数仓中间层和数据应用产品。 3、负责不断完善数据治理体系&#xff…

【数据结构】栈的应用

目录 0 引言 1 栈在括号匹配中的应用 2 栈在表达式求值中的应用 2.1 算数表达式 2.2 中缀表达式转后缀表达式 2.3 后缀表达式求值 3 栈在递归中的应用 3.1 栈在函数调用中的作用 3.2 栈在函数调用中的工作原理 4 总结 0 引言 栈&#xff08;Stack&#xff09;是一…

WPF视频学习-基础知识篇

1.简介WPF&#xff1a; C# 一套关于windows界面应用开发框架 2.WPF和winform的差别 &#xff0c;(WPF比较新) 创建新项目使用模板&#xff1a; WPF使用.xaml后缀&#xff0c;双击可查看操作界面和设置代码&#xff0c;其文件展开之后中有MainWindow.xaml.cs为程序交互逻辑。…

linux笔记8--安装软件

文章目录 1. PMS和软件安装的介绍2. 安装、更新、卸载安装更新ubuntu20.04更新镜像源&#xff1a; 卸载 3. 其他发行版4. 安装第三方软件5. 推荐 1. PMS和软件安装的介绍 PMS(package management system的简称)&#xff1a;包管理系统 作用&#xff1a;方便用户进行软件安装(也…

nginx mirror流量镜像详细介绍以及实战示例

nginx mirror流量镜像详细介绍以及实战示例 1.nginx mirror作用2.nginx安装3.修改配置3.1.nginx.conf3.2.conf.d目录下添加default.conf配置文件3.3.nginx配置注意事项3.3.nginx重启 4.测试 1.nginx mirror作用 为了便于排查问题&#xff0c;可能希望线上的请求能够同步到测试…

PyCharm QThread 设置断点不起作用

背景&#xff1a; 端午节回来上班第一天&#xff0c;不想干活&#xff0c;领导又再后面看着&#xff0c;突然想起一个有意思的问题&#xff0c;为啥我的程序在子进程QThread的子类里打的断点不好用呢&#xff1f;那就解决一下这个问题吧。 原因&#xff1a; 如果您的解释器上…

开发框架表单设计器办公效率高吗?

对于很多职场人来说&#xff0c;拥有一款可以提质、增效、降本的办公利器是大有裨益的。随着科技的进步和发展&#xff0c;低代码技术平台凭借可视化界面、易操作、好维护、高效率等多个优势特点&#xff0c;成为大众喜爱的办公利器。开发框架表单设计器是减少信息孤岛&#xf…

macbook本地部署 pyhive环境连接 hive用例

前言 公司的测试和生产环境中尚未提供基于Hive的客户端。若希望尝试操作Hive表&#xff0c;目前一个可行的方案是使用Python语言&#xff0c;通过借助pyhive库&#xff0c;您可以对Hive表进行各种操作。以下是一些示例记录供您参考。 一、pyhive是什么&#xff1f; PyHive是一…

从零到一建设数据中台(番外篇)- 数据中台UI欣赏

番外篇 - 数据中台 UI 欣赏 话不多说&#xff0c;直接上图。 数据目录的重要性&#xff1a; 数据目录是一种关键的信息管理工具&#xff0c;它为组织提供了一个全面的、集中化的数据资产视图。 它不仅记录了数据的存储位置&#xff0c;还详细描述了数据的结构、内容、来源、使…

细说ARM MCU的串口接收数据的实现过程

目录 一、硬件及工程 1、硬件 2、软件目的 3、创建.ioc工程 二、 代码修改 1、串口初始化函数MX_USART2_UART_Init() &#xff08;1&#xff09;MX_USART2_UART_Init()串口参数初始化函数 &#xff08;2&#xff09;HAL_UART_MspInit()串口功能模块初始化函数 2、串口…

批量申请SSL证书如何做到既方便成本又最低

假如您手头拥有1千个域名&#xff0c;并且打算为每一个域名搭建网站&#xff0c;那么在当前的网络环境下&#xff0c;您必须确保这些网站通过https的方式提供服务。这意味着&#xff0c;您将为每一个域名申请SSL证书&#xff0c;以确保网站数据传输的安全性和可信度。那么&…

面试-NLP八股文

机器学习 交叉熵损失&#xff1a; L − ( y l o g ( y ^ ) ( 1 − y ) l o g ( 1 − ( y ^ ) ) L-(ylog(\hat{y}) (1-y)log(1-(\hat{y})) L−(ylog(y^​)(1−y)log(1−(y^​))均方误差&#xff1a; L 1 n ∑ i 1 n ( y i − y ^ i ) 2 L \frac{1}{n}\sum\limits_{i1}^{n}…

【ai】openai-quickstart 配置pycharm工程

之前都是本地执行脚本【AI】指定python3.10安装Jupyter Lab环境为:C:\Users\zhangbin\AppData\Local\Programs\Python\Python310 参考之前创建的python工程 使用的是局部的私有的虚拟环境 pycharm给出的解释器 直接使用现有的,不new了 可以选择3.10 :可以选虚拟的:

Rust-02-变量与可变性

在Rust中&#xff0c;变量和可变性是两个重要的概念。 变量&#xff1a;变量是用于存储数据的标识符。在Rust中&#xff0c;变量需要声明其类型&#xff0c;例如&#xff1a; let x: i32 5; // 声明一个名为x的变量&#xff0c;类型为i32&#xff08;整数&#xff09;&#…

SpringCloud 前端-网关-微服务-微服务间实现信息共享传递

目录 1 网关获取用户校验信息并保存至请求头&#xff08;前端-网关&#xff09; 2 微服务获取网关中的用户校验信息&#xff08;网关-微服务&#xff09; 2.1 一般的做法是在公共的module中添加&#xff0c;此处示例为common 公共配置module中添加 2.2 定义拦截器 2.3 定义…

简单通用的系统安装、备份、还原方法,支持 ARM 系统【Ventory+FirePE+DiskGenius】

文章目录 0. 简介1. 制作 Ventory 启动盘1.1. 下载 Ventory1.2. 制作 Ventory 启动盘 2. 添加 FirePE 等系统镜像到启动盘2.1. 下载 FirePE2.2. 导出 .iso 系统镜像文件2.3. .iso 系统镜像文件添加至启动盘 3. 启动 FirePE 等系统镜像3.1. 在 bios 中选择启动盘启动3.2. 启动系…

# RocketMQ 实战:模拟电商网站场景综合案例(八)

RocketMQ 实战&#xff1a;模拟电商网站场景综合案例&#xff08;八&#xff09; 一、RocketMQ 实战&#xff1a;模拟电商网站场景综合案例–下单异常问题演示 1.png 1、如果订单在扣减库存、扣减优惠券、扣减余额后&#xff0c;在 未 确认订单 前&#xff0c;出现了异常&am…

vue+element el-select动态加减框数量及验证下拉框选项动态置灰(选中行的下拉框换个值后,原值没办法监控这个问题也解决了)

1效果: 2部分主要(HTML): 1:这个位置主要就是看看方法什么的吧,还有大概的结构 2:change"sort_Change(item,tablelists.orderbyList)这两个参数(都有大用): (1)item:代表每次你操作的这个数据 (2)tablelists.orderbyList:代表你这一共有几行数据(上边这个例子就会得到一个…