• c语言入门---预处理


    一.程序的翻译环境和执行环境

    在ANSI c的任何一种实现中,存在两个不同的环境:
    第一种是翻译环境,在这个环境中源代码被转换为可执行的机械指令。
    第二种是执行环境,它用于实际执行代码。

    那么我们这篇文章主要讲的就是我们这里的第一个环境翻译环境。

    二.详解编译+链接

    1.翻译环境

    我们首先来看一个图片:
    在这里插入图片描述
    我们的翻译环境就是这么的一个图,我们写的每一个程序都会经历上面的这些步骤,那么根据我们之前学习函数时知道一件事就是我们未来写的程序当中有是有多个.c文件的,那么我们这里就将这些.c文件称为源文件,那么这些源文件都会单独的经过我们的编译器生成对应的目标文件,那么我们这里就可以通过文件里面来看看是不是确实有这么一些的功能,我们首先在编译器里面写一下这些代码:

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

    我们先不运行,我们看看我们创建的这个文件里面有什么:
    在这里插入图片描述
    我们点进这个预处理的这个文件里面:
    在这里插入图片描述
    我们发现里面有这些文件,然后我们将这个代码运行一下再来观察一下我们这里的文件会发生什么样的变化:
    在这里插入图片描述
    首先我们看到这里多了一个Debug文件,那么这个我们知道毕竟我们是在该环境下跑的程序嘛,然后我们再点击这个预处理这个文件里面看看:
    在这里插入图片描述
    那么我们这里就发现他多出来了一个Debug文件,我们点进这个文件再来看看:
    在这里插入图片描述
    我们这里就可以看到我们这里有许多的文件,那么这里面的最后一个文件末尾为obj的文件就是我们上面所说的目标文件,我们这里还可以再创建一个源文件出来,我们这里就叫这个文件为add.c:然后在这个文件里面实现我们的加法函数:
    在这里插入图片描述
    我们再将这个程序运行一下再来看我们上面的Debug文件会有什么样的变化:
    在这里插入图片描述
    那么我们这里就可以清楚的发现这个文件夹里面又多出来了一个add.obj文件,那么这里就和我们这个图说的一模一样:
    在这里插入图片描述
    我们这里的多个源文件,依次经过我们的编译器生成了目标文件,也就是这里的后缀为obj的文件,然后这里生成的多个目标文件最后会和链接库一起在链接器的作用下生成我们这里的可执行程序,那么这里的链接库的作用是什么呢?我们平时使用的一些库函数,是不是得包含一些头文件啊,但是我们使用这些库函数的话是不是得依赖一些东西啊,那么这些东西就是我们这里的链接库来提供的,看到这里想必大家能够明白我们这里翻译环境的作用,那么我们这里就来总结一下我们这里的过程:

    1. 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
    2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
    3. 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

    那么看到这里想必大家能够粗略的了解这个大致的过程,那么我们这里就就可以再来对其进行细分来详细的了解这个过程。

    2.编译本身分为的几个阶段。

    根据我们上面的理解我们发现一个可执行的程序分为两个步骤,一个是编译的过程,另外一个就是链接的过程,但是实际的过程中我们的编译又要得细分为三个步骤,第一个就是预处理阶段,第二个是编译阶段,第三个就是汇编阶段,那么这三个阶段我们就可以画成下面这个图:
    在这里插入图片描述
    我们这里就来详细的讲解一下这三个阶段分别干了什么事情具有什么样的作用,那么我们这里为了方便观察我们这里就使用的是gcc的这个编译器。

    1.预处理/预编译

    首先我们的代码是这样:
    在这里插入图片描述
    我们这里创建了一个test.c文件,然后在文件写入了如上的代码,然后我们这里要做的就是要运行这段代码,但是我们这里的运行就不是将他一下子全部运行结束,而是让他运行到我们这里的预处理阶段就停下来我们来观察一下中间的变化,那么为了达到这个功能的话我们这里在运行的时候就得输入这么一句话:gcc test.c -E这句话的作用就是运行完预处理阶段之后就停下来我们再按一下回车就可以发现我们这个屏幕上面输出了一堆东西
    在这里插入图片描述
    那么这里就肯定不是我们想要的结果,因为这样的话我们就不方便观察,如果我们这里能够将生成的结果放到一个文件里面去的话,那就能够很好的观察我们的结果,那么我们这里就得将我们上面输入的命令进行改进,改成这样gcc test.c -E -o -test.i那么这样的话我们就可以把这个输出的结果全部都放到我们这个test.i文件里面去,我们再敲一个回车就可以看到我们这里自动生成了一个test.i文件
    在这里插入图片描述
    但是我们还是发现这个文件里面的内容依然是十分的多:
    在这里插入图片描述
    但是我们这里一直往下翻就会发现我们这里的内容虽然很多但是在这个文件的最后还是放着我们之前写的代码
    在这里插入图片描述
    但是我们写的这个代码#include好像不见了,那么这时候有小伙伴们就要想了,我们这里#include是包含头文件的意思,然后我们这里在经过预处理之后我们的这个代码就不见了,那么这个能不能说明我们这里杂七杂八的内容就是我们这里的stdio.h里面的内容复制过来的呢?那么这里我们只用将stdio.h这个文件找到将里面的内容进行对比就知道了,我们这个代码也告诉了我们这个stdio.h这个文件在哪里:
    在这里插入图片描述
    那么我们这里就可以通过命令行的方式打开这个文件:
    在这里插入图片描述
    然后我们这里再点击一下回车就会出现这样的情况:
    在这里插入图片描述
    那么这个就是我们这个文件里面的内容,我们可以往下翻一下这个文件我们发现在900多行处有这么一些代码:
    在这里插入图片描述
    这3个单词就很特殊,那么我们就以这三个单词为目标看看我们上面的程序的运行结果中是不是也有这3个特殊单词所对应的大致的内容呢?我们找找就可以发现确实有:
    在这里插入图片描述
    那么我们通过这一点就可以充分的说明我们在预处理的时候会将stdio.h中包含的全部内容复制粘贴到我们的文件里面去,这也是为什么我在用别人的函数的时候得引用头文件的原因,那么我们这里预处理做的事情起始还没有结束,我们将这个代码进行修改我们在代码中增加一个注释,再用#define 第一个MAX 将他的值复制成100,然后我们再在main函数里面使用这个MAX我们来看看我们的代码就是这个样:
    在这里插入图片描述
    我们再来看看预处理之后的结果:
    在这里插入图片描述
    我们仔细的对比一下就会发现我们这里的注释没有了,我们#号定义的表示符的那段代码也不见了,并且我们下面的MAX也替换成我们之前定义的值,那么这里我们就又发现了预处理还会做的两件事:第一件:define定义的符号的替换和删除定义的符号,第二件:注释的删除。当然这里还有很多的细节我们这里就没必要了解的那么深。

    2.编译

    我们上面的操作是预处理的操作,那么我们预处理完之后就得进入我们的编译的阶段,那么我们这里就得再输入一段命令:gcc test.i -S那么这段命令的作用就是让我们的程序把编译这个阶段执行完就停下来,当然我们这里是对test.i进行的操作,这个文件是已经执行完了预处理的阶段所以他会接着执行我们的编译阶段,当然我们这里也可以对test.c进行这样的操作:
    gcc test.c -S那么这个就是对我们的源文件进行操作,他会先执行一边预处理阶段阶段再来执行编译的阶段,当然这里的效果都是一样的,所以我们这里更喜欢使用前者,我们这里输入完指令之后再敲一下我们的回车就会发现我们这里又多出来了一个test.s文件:
    在这里插入图片描述
    那么这个文件就是我们预处理之后所生成的一个文件,我们打开这个文件发现里面的内容是这样的:
    在这里插入图片描述
    我们这里就发现这里有些奇奇怪怪的代码,那么这个代码就是我们的汇编代码,所以我们这里就发现编译阶段做的一件事情的就是将我们c语言写的代码转换成汇编代码,当然这里的转换可并不是跟我们英文翻译成中文一样简单,这里的转换得分成四个步骤:

    • 语法分析
    • 词法分析
    • 符号汇总
    • 语义分析

    那么我们这里语法分析就是看你写的这个代码有没有语法上的错误,比如说分号写漏掉了啊,函数的格式用错了啊等等,这里的词法分析就是会稍微复杂一点比如说我们上面的这个代码中的int g_val = 2022;:时
    在这里插入图片描述
    他就会干这些事情,首先将这个int提出来放到一边表示这是一个关键字,看到了后面的g_val他就会把他提出来放到另一边表示这个时一个变量名,看到后面的等号就会把他提出来放到另一边表示这是一个赋值符号,再往后看到了一个2022就会把他提出来再放到另外一边表示这是一个数值,等这样的操作遍历整个代码最终会汇总成一个语法树当然这里只是一个大致的过程实际情况更加的复杂,然后就是这里的语义分析,我们这里得转换成汇编代码首先我们得看的懂我们这里的代码,得分的清这个是一个函数,这个是一个循环等等,那么我们这里的语义分析干的就是这个事,那么最后就是我们这里最重要的符号汇总,我们这里的符号汇总干的事情就是将我们这里的代码中的全局的符号(全局的变量和函数)全部进行汇总起来比如说我们这里的:g_val,main函数,Add函数。那么这就是我们这个汇编的过程要干的事情,当然看到这里想必大家看了之后还是有那么点懵,不要紧我们继续往下看,那么这一步走完之后就来到我们的下一步:汇编。

    3.汇编

    那么我们这里就可以输入这个指令让我们的代码执行汇编这个操作:gcc test.s -c这个指令这样我们就可以只进行我们的汇编操作,然后我们再按一下回车就可以看到我们这里生成了一个test.o这个文件也就是我们前面所说的目标文件
    在这里插入图片描述
    然后我们就尝试着打开这个文件我们就发现出现了这么一问题在这里插入图片描述
    我们打不开这个文件,但是打开这个文件后看到这个文件的内容我们也看不懂:
    **
    但是这里却说明了一个事情就是我们这里的汇编就是将我们的汇编代码转换称为二进制指令,并且他这里还会干一件事情就是形成符号表,那么这个形成符号表这个怎么来理解呢?我们这里就可以在编译器上写这么一个代码:
    我们这里写了一个源文件里面写了一个加法函数,在另外一个源文件里面使用了这个函数在这里插入图片描述

    在这里插入图片描述

    那么我们这里是两个源文件,那么他就会分别经过编译这个阶段形成我们的目标文件,那么也就是说他都会经过我们这里的编译阶段,而编译阶段是会进行符号汇总的所以我们这里的test.c文件就会汇总出Add和 main这两个符号,而我们这里的Add.c文件就只能汇总出Add这一个符号,然后就会生成test.s和Add.s这两个文件,然后这两个文件就又会单独经过我们的汇编分别形成test.o文件和Add.o文件,而这两个文件里面就会包含我们这里的符号表,那这个符号表是什么意思呢?就是给我们之前汇总出来的符号关联一个地址出来,比如说上面的Add.o文件里面的符号表中的Add就会关联一个0x100这个地址,所以test.o文件里面的符号表中的符号也会一一关联出来地址,比如说main就会关联一个0x200这个地址,但是我们这里也有一个Add这个符号但是这个符号在我们这个文件里面只是简单的对其进行了声明并没有对其进行具体的实现,所以我们这里关联的地址就是一个无效的地址比如说0x000,这里就是我们所谓的汇编过程。

    在这里插入图片描述

    4.链接

    这里就到了我们链接这一步,这一步的作用就是合并段表和符号表的合并以及重定位,我们首先来讲讲合并段表的意思是什么,我们这里有两个文件一个是test.o文件一个是Add.o文件,那么这两个文件在我们的linux环境地下都是有一个特定的格式的,这个格式就是elf格式,这个格式的特点就是不同的区域放着同样的类型的数据,比如说一号区域放着这个类型的数据,二号区域放着另外一个类型的数据这样依次类推等等这样依次类推,那么我们这里的test.o文件是这样的格式,那么我们的Add.o是不是也是这样的格式啊,那么他们这两个文件的同样的区域是不是就放着同样类型的数据,那么这里的合并段表的作用就是将这里两个文件的相同区域的相同类型的数据进行合并合二为一,这样的话我们的两个段表就合并成了一个段表,当然这里的合并还要包括我们这里的链接库,最终形成我们的可执行程序,那么这里有小伙伴们就要问了这里为什么要进行合并呢?因为在我们Linux环境下的可执行程序的格式也是elf形式,而且可只执行程序也是一个文件,所以我们这里就得进行合并。我们这里生成的可执行程序只有一个文件,所以按照道理来说就只有一个符号表,但是经过我们上面的操作之后有两个符号表啊!那么这里我们就还得做一件事就是将我们的符号表进行合并,那么这里的合并就是将两个合并成一个,但是这里的合并就会出现一个问题就是我们这里的合并中有两个Add,那么这里就会进行赛选删除一个,然后编译器就发现test.o里面的那个Add相关地址是一个无效地址,那么他就会将这个符号表中的Add进行删除,这样我们合并的符号表中就只剩下一个Add函数了,最终我们的合并之后的符号表就是这样的:
    在这里插入图片描述
    那么我们这个生成的最终恶的符号表的作用是什么呢?大家想想我们在写程序的时候通常都会用到一些函数,那么在编译过程使用这些函数的时候,我们的编译器就会在这个最终生成的符号表中找这个函数对应的位置,那么大家想一下如果我们一个程序当中使用的一个函数只有函数的声明没有函数的实现的话,那么他的地址对应的是不是就是一个无效的地址,那么你在使用这个函数的时候他就会对应的找到那个无效的地址里面去,但是这个无效的地址是找不到这个函数的实现的,那么这个时候就会报出链接性的错误出来,那么这个就是符号表的作用,并且大家想一下我们这个符号表的形成是在收集所有文件中的符号,那么这个操作还有一个好处就是可以方便我们函数和符号的跨文件使用啊,对吧因为就算我这个文件没有,但是通过我们的符号表我们还是能够找到函数,那么这就说明了其他文件中一定存在这个函数,那么我就至少可以跑的过去对吧,那么这里大家可以再理解理解。

    3…运行环境

    程序执行的过程:

    1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
      的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
    2. 程序的执行便开始。接着便调用main函数。
    #include 
    int main()
    {
     int i = 0;
     for(i=0; i<10; i++)
     {
     printf("%d ", i);
     }
     return 0; }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
      地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
      一直保留他们的值。
    2. 终止程序。正常终止main函数;也有可能是意外终止。

    三.预处理详解

    1.预定义符号

    我C语言给了一些预定义的符号如下所示:

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

    那么我们这里就可以依次来看看这些符号的作用,我们首先写一个这样的代码:

    #include
    int main()
    {
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		printf("i=%d\n", i);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那么我们这里确实能将这里的i的值全部都打印出来:
    在这里插入图片描述
    那么这是我要是想看看这个程序的地址在哪我们该怎么办呢?我们这里就可以用到这个预处理的指令:__FILE__,那么我们这里的代码就可以改成这样:

    #include
    int main()
    {
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		printf("%s i=%d\n",__FILE__, i);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们再来看看这个代码的运行结果为:
    请添加图片描述
    那么我们还想看看这个代码所运行的日期呢?那么我们这里就可以用这个:__LINE__代码我们就可以改成这样:

    #include
    int main()
    {
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		printf("%s %s i=%d\n", __FILE__,__DATE__, i);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们来看看这个代码的运行结果为:

    在这里插入图片描述
    那么这里我要是还想要看看这个代码运行的时间呢?我们这里就可以使用这个预定义符号:__TIME__
    那么我们的代码如下:

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

    我们来看看这个代码的运行结果为:
    在这里插入图片描述
    那么这里就是我们这个4个预定义符号的使用方法,当然我们这里还有最后一个没有讲,那么这个符号的作用就是来判断该编译器是否准寻我们的ANSI C标准的,如果遵循的话这里就会打印1,那么这里我们就可以来测试一下我们这里的vs编译器:

    #include
    int main()
    {
    	printf("%d", __STDC__);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当我们讲这个代码运行一下就会发现我们这个程序压根就运行不起来:
    在这里插入图片描述
    那么这就说明我们的vs编译器就不支持ANSI C标准,我们可以看看gcc编译器是怎么样的:
    在这里插入图片描述
    我们可以看到我们的gcc编译器就能够打印出1来,那么这就是5个预定义符的作用。

    四.#define

    1.#define定义标识符

    我们这里的#define可以定义很多的表示符,比如说我们可以定义一个标识符MAX将他的值定义为100,比如说下面的代码:

    #include
    #define max 100
    int main()
    {
    	int a = max;
    	printf("max的值为:%d", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们还可以用一些标识符来代替我们的一些关键字,比如说我们在写switch语句的时候总是得在代码的末尾添加一个break,但是很多时候我们总会忘记添加这个break,那么我们这里就可以为了简化,将二者进行合一定义成一个,这样我们就方便了许多这个出来比如下面的代码:

    #include
    #define CASE break;case
    int main()
    {
    	int a = 0;
    	scanf("%d", &a);
    	switch (a)
    	{
    	case 1:
    	CASE 2:
    	CASE 3:
    	CASE 4:
    	break;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    那么这里就有一个问题,我们这里在define定义标识符的时候,要不要在最后加上分后呢?那么这里我还是建议大家不加,因为我们写程序的时候都会情不自禁的加上一个分号,所以我们这里在定义的时候就建议不加,虽然有时候它不会报错但是在很多情况下还是会报错的。比如说下面代码:

    #include
    #define condition 1;
    int main()
    {
    	if (condition)
    	{
    		printf("haha\n");
    	}
    	else
    	{
    		printf("hehe\n");
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们可以看到这里我们加一个分号就导致程序的错误:
    请添加图片描述

    2.#define定义宏

    #define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
    宏(define macro)。下面是宏的申明方式:#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。注意:参数列表的左括号必须与name紧邻。
    如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。如:#define SQUARE( x ) x * x这个宏接收一个参数 x .如果在上述声明之后,你把SQUARE( 5 );置于程序中,预处理器就会用下面这个表达式替换上面的表达式:5 * 5,但是我们这里就会有一个问题我们要是传这么一个值上去呢?比如说下面的代码:

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

    首先按照我们正常的逻辑来看的话,我们这里是将6传给了我们这里的宏,也就是说我们上面x的值就会等于6,那么计算的结果就会等于36 ,但是我们将这段代码运行一下就可以发现我们这里计算的结果并不是36,而是11
    在这里插入图片描述
    这是为什么呢?我们说·#define的机制是将参数进行替换,替换到文本之中那么我们这里又说计算吗?好像没有吧?也就是说我们这里的宏它的原理是进行替换并不会计算,那么我们这里的将5+1作为参数来进行替换的话,我们这里的结果就是这样的:5+1*5+1,所以我们这里的代码运行的结果就是11,所以这里就是一个问题所在,那么我们这里就有一个对应的解决办法,我们可以将这里进行替换的两个参数加上一个括号变成这样:#define SQUART( x ) (x) *(x) 这样的话就可以解决我们这里的传参是个表达式的问题,我们来看看这个代码的运行的结果:

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

    在这里插入图片描述
    这样算的话我们的结果却是是36,很多小伙伴以为这样我们这里的宏就没有问题了 ,但是我们来看看下面的代码

    #include
    #define DOUBLE( x ) (x) + (x)
    int main()
    {
    	int a = 0;
    	a = 10*DOUBLE(5 + 1);
    	printf("%d", a);
    	return 0;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那么我们这里宏的意义就是先求出一个两倍的x的值,但是我们这里是将10乘以这个宏的运算再赋值给我们的a,然后再打印a的值,那么我们这里按道理来说打印的结果应该是120,但是我们实际运行的结果却是:
    在这里插入图片描述
    那么这是为什么呢?我们来看我们这个宏的作用是直接进行替换,所以我们这里实际运行的结果就是:10*(5+1)+(5+1)所以我们这里的10就会先和(5+1)进行结合运算的结果就是60,然后再加上(5+1)最终的结果就是66,外面乘法的优先级大于我们里面的加法所以这里就会导致错误,所以这就是我们的问题所在,那么我们这里解决该问题的方法就是将我们这个宏的表达式加一个整体的括号让其表示为整体,以免外来的符号进行干扰,也就是这样:#define DOUBLE( x ) ((x) + (x))所以我们在使用宏的时候就得注意的一点就是:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

    3.#define 替换规则

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

    1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
      被替换。
    2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
    3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
      述处理过程。
      注意:
    4. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
    5. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

    4.#和##

    我们首先来看一段代码:

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

    大家觉得这个能打印出来hello world这句话吗?我们这个与之前的有所不同就是我们这里的将原本的一句话改成了两句,那么这样做的话还可以正常的运行吗?答案是可以的,我们来看看运行的结果:
    在这里插入图片描述
    那么我们再来看一个代码:

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

    我们来看看这个代码,我们发现这个代码好像有那么点特殊就是我们这里好像里面的代码都很一样啊,都是打印出一各几乎相同话和一个变量的值出来,那么我们这里能不能将其变成一个函数来使得更加的节省时间呢?就想下面的代码一样:

    #include
    void print(int n)
    {
    	printf("the value of n is %d\n", n);
    }
    int main()
    {
    	int a = 10;
    	print( a);
    	int b = 20;
    	print( b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    那么我将这个打印出来之后就发现我们这个代码存在一个问题:本来应该输出a的地方它输出的是n,本来是b的地方它输出的也是n,那么这又该如何来办呢?
    在这里插入图片描述
    那么我们这里就可以使用宏加#来解决这个问题:我们可以先用一份宏来来代替我们这里的pruintf函数就想这样:#define PRINT(N) printf("the value of N is %d\n",N)因为我们这里的问题所在是字符串里面的N它不会进行替换,所以我们这里就把它拿出来
    #define PRINT(N) printf("the value of "N"is %d\n",N)这样他就会与你传过来的参数进行替换你传过来的是a我就替换成a,你传过来的是b我就替换成b,但是这就不是一个字符串了啊,那么此时我们就可以在这个N前面加上一个#,这个#的作用就是将我们这里的宏参数变成对应的字符串,那么也就是说如果你传过来的是a。那么这里的#N就会变成#a,但是这个不会替换成#10,然后这里的#a就会变成“a”所以我们这里就变成了这样#define PRINT(N) printf("the value of a is %d\n",a)这样的话我们这里就就可以打印出我们想要的值,那么我们这里就可以看看我们下面这个代码的打印结果:

    #include
    #define PRINT(N) printf("the value of "#N" is %d\n",N);
    int main()
    {
    	int a = 10;
    	PRINT(a);
    	int b = 20;
    	PRINT(b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    那么这里是我们单个#的作用,我们这里还有##,那么这个操作符的作用就是可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。比如说下面的代码:

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

    那么我们这里就可以看到我们这里的代码打印出来的结果为:
    在这里插入图片描述
    那么这里的代码是什么意思呢?我们这里将hello传给了我们的a,将world传给了我们的b,然后这里的a##b就是将这里的a和b连接起来,那么这里就变成了helloworld,而我们前面将helloworld的值赋值为了100,所以我们这里打印的结果就是100,那么这就是我们这个##的作用将两个符号合并起来。

    5.带副作用的宏参数

    其实使用宏对我们的参数还是有一定的要求的,因为我们这里的宏他的本质是替换,所以当我们给了一些带有副作用的参数的时候就会带来一系列的问题,比如说我们想要比较两个数的大小,比较完之后就将这两个数的值加一,那么我们这里用宏就可以这么写:

    #include
    #define MAX(a,b) a>b?a:b
    int main()
    {
    	int a = 3;
    	int b = 4;
    	int c =MAX(a++, b++);
    	printf("a=%d b=%d c=%d", a, b,c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但是当我们这里传过去的值是带有副作用的话,我们这里就会出现问题我们这里本应该打印的值4 5 4但是当我们运行起来之后我们就会发现结果完全不一样:
    在这里插入图片描述
    那么这就是因为我们这里的传过来的参数存在着副作用,那么这就导致我们这个代码变成了这样:
    a++>b++?a++:b++所以我们这里打印出来的结果就有所不同,那么这里大家在使用宏的时候就得注意一下尽量不要让我们宏的参数带有副作用,但是我们的函数就不会有这样的问题,函数是将值进行复制,然后用复制的值是实现函数体的内容那么即使你传过来的参数有副作用那么对我们的本体也没有啥伤害,而我们的宏是直接的替换所以这就会导致一些不必要的事情发生,那么这就是两者的不同之处。

    6.宏和函数的对比

    看到这里想必很多小伙伴脑海都有这么一个问题,我以后要实现某个功能的话,我们是使用函数来实现这个功能呢?还是用宏来实现这个功能呢?那么我们这里就各有各的道理,两者在不同的方面都有不同的优势,

    宏的优势

    首先我们来看看宏相对于函数具有什么样的优势:我们还是用上面的那个例子:求两个数的中的较大数:#define MAX(a,b) a>b?a:b这里大家就来想一下我们这里相对于函数有什么好处呢?首先我们能想到的一点就是我们的宏他是直接将符号替换成后面的内容,这个阶段是在预处理的时候就完成了,所以我们的宏就相当于一个小型的计算代码一样不需要消耗很多的时间,而我们的函数则不同他需要调用函数,并且从函数里面返回一些值等一些列的操作这个操作可以看看我们的函数栈帧里面有详细的讲解,那么他相比于我们的宏则需要消耗更多的时间以及规模,所以宏比函数在程序的规模和速度方面更胜一筹。还有一点就是我们在使用宏的时候大家有没有关注过我们需要传什么样类型的参数没?好像没有把!但是我们的函数呢?是不是得严格的按照这个函数的需求来传参数啊,这个函数要求的是传一个整型过来,如果你传的是浮点型,那么他就会报错,但是我们的宏那就完全没有这个问题,所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。还有一点就是我们的宏在某些方面能够实现一些我们函数无法实现的功能,比如说我们的宏是可以传类型的,但是我们函数就不可以我们可以看看这个代码:

    #define MALLOC(num, type) (type *)malloc(num * sizeof(type))
    
    • 1

    宏的劣势

    相对于宏的优势我们宏的劣势还是非常的多的具体有下面的:

    1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。这个就很好理解嘛,因为我们的宏就是复制粘贴嘛,所以当你宏定义的代码过长的话,我们这里整体的代码就会特别长,那么这就是一个劣势。
    2. 宏是没法调试的。那么这里相对于我们的函数我们就可以在调试的按F11进入我们函数的内部观察调试的细节。
    3. 宏由于类型无关,也就不够严谨。
    4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

    宏和函数的对比

    在这里插入图片描述

    五.#undef的作用

    这条指令用于移除一个宏定义。比如说我们在代码的前面定义别一个标识符让他的值等于了100,那么在后面的代码我们就可以用#undef来讲这个标识符的作用给取消,比如说下面的代码:

    #include
    #define helloworld 100
    int main()
    {
    	printf("%d", helloworld);
    #undef helloworld//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
    	printf("%d", helloworld);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们这里就可以将定义的helloworld的给移除如果我们下面还要用这个helloworld的话我们这里就会报错:
    在这里插入图片描述

    六.命令行的定义

    许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
    例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性就有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)那么我们这里如何来解决这个问题呢?哎我们就可以在命令行里面来定义这个数组的大小这样的话我们在不同的设备里面进行测试的时候就可以不用不停的修改代码了,比如说下面的代码:
    在这里插入图片描述
    我们这个代码里面是没有定义这个sz的大小的,所以我们编译的时候就会报错在这里插入图片描述
    那么这里我们就可以在输入命令编译这个代码的时候添加上去一个定义的过程
    在这里插入图片描述
    这里的-D就是定义一个符号的意思,而后面就是我们定义的符号和他的值,那么我们这里就可以在编译的时候来顺便定义一个值,比如说我们定义这个值为10,这样的话就可以提高我们代码的灵活性,我们来看看输出的结果:
    在这里插入图片描述
    我们这里就没有报错输出的结果也是0到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

    我们这里要是想观察这里的每一个值的话,我们就可以在程序的开头添加上一个代码#define __DEBUG__,因为在我们的main函数里面就有这么一个代码:#ifdef __DEBUG__这个代码的意思就是如果你定义了__DEBUG__这个标识符我们就执行了下面的代码,一直执行到#endif这里来结束,如果我们这里不想进行测试了我们就可以将开头的#define __DEBUG__给去掉这样的话我们就不会执行#ifdef __DEBUG__#endif之间的代码了,那么我们这里也就看不到测试的结果,这里就是我们的条件编译,当然除了上面的这种形式我们还有这样的:

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

    那么这个的作用就和我们的if语句是一样的,如果if后面的常量表达式的结果为真的话就会执行下面的语句一直执行到#endif为止,我们的if语句中还存在着多分支的情况,那么我们这里也就同样的存在我们可以看看这样的形式:

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

    那么这里的运行原理就和我们if语句的多分支是一模一样的。我们还有一种形式就是判断一个符号是否被定义如果这个符号被定义了那么我们就执行下面的代码,那么这个形式就是这样的:

    判断是否被定义
    #if defined(symbol)
    #ifdef symbol
    
    • 1
    • 2
    • 3

    那么这就是两个用于判断是否被定义的代码,两种写法都可以看大家喜欢哪种,那么与之想法既然有判断一个代码是否被定义的形式,那么也就有判断一个代码是否没有被判断的形式,那么我们这里的形式就如下:

    判断是否没有被定义
    #if !defined(symbol)
    #ifndef symbol
    
    • 1
    • 2
    • 3

    那么这里的规则就是如果后面的符号没有被定义的话我们就执行下面的代码,那么我们这样的形式我们还可以对其进行嵌套使用比如下面的代码:

    #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

    那么这里就很简单跟我们的多个if语句的嵌套类似。

    八.文件包含

    我们在写代码的时候经常会写这样的代码:#include那么这个代码的意思就是引用其他文件的内容,将源代码(#include)进行替换,那么这时有伙伴们就要说啊我咋遇见过这样的代码:#include"stdio.h"那么这个双引号和我们这里的尖括号有什么区别呢?这里的双引号里面的文件就是会先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。而我们的尖括号则会直接去查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
    答案是肯定的,可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。那么这里大家要注意的一点就是如果我们这里多次引用同一个头文件的话就会导致内容的大量重复,样一个源文件被包含10次,那就实际被编译10次。所以我们在写代码的时候就得避免反复包含头文件,但是有时候事情就会发生的很巧妙我不知道多次包含了头文件,那么这里我们就得写一些代码加以预防了。比如说我们可以在每个头文件的开头这么写:

    #ifndef __TEST_H__
    #define __TEST_H__
    //头文件的内容
    #endif   //__TEST_H__
    
    • 1
    • 2
    • 3
    • 4

    当我们第一应用该头文件的时候我们是没有定义这个__TEST_H__,那么我们这里就会执行这个代码,定义一个__TEST_H__,然后再写入头文件里面的内容,最后结束,等你再使用这个头文件的时候,我们有会重新的将这个代码编译一次,而此时我们在第一次编译的时候就以及定义了__TEST_H__这个表示符,所以我们这里就不会执行下面的代码了,所以文件的内容就不会被引用进去,那么这里就是我们的方法之一,当然还有一个方法更加的简单就是直接在文件的开头写上这个代码就ok完事了:

    #pragma once
    
    • 1

    这个代码就可以帮我们防止文件的多次引用。
    点击此处获取代码

  • 相关阅读:
    【阿旭机器学习实战】【26】逻辑斯蒂回归----糖尿病预测实战
    Web(二)html5基础-表格基本结构
    Java基础20221105
    springboot2.7.x 集成log4j2配置写入日志到mysql自定义表格
    vs2013调试的时候我想查看某个变量的值的窗口被我删了,我需要找到那个查看局部变量的值的窗口
    如何在 Vue.js 中使用 Axios
    力扣-448.找到所有数组中消失的数字
    vulnhub blogger: 1
    LeetCode-19. Remove Nth Node From End of List [C++][Java]
    008.分隔符、循环、比较
  • 原文地址:https://blog.csdn.net/qq_68695298/article/details/126489301