第十四章 预处理器

news/2024/10/22 6:54:43/

目录

  • 写在前面
  • 预处理器
    • 什么是预处理器
    • 预处理器的原理
  • 预处理指令
  • 宏定义
    • 为何要有宏
    • 简单的宏
      • 注意事项
      • 宏的作用
        • 替换
        • 注释
          • 注释的内容经过预处理后变成了什么?
      • 注释和宏替替换哪个先开始
    • 带参数的宏
      • 注意事项
    • 宏定义中的运算符
      • # 运算符
      • ## 运算符
  • 宏的优缺点
  • 预定义宏
    • C99增加的几个预定义宏
  • 例题
  • 条件编译
    • 什么是条件编译
    • 为何要有条件编译
    • 指令
      • #if 和 #endif
      • 作用
      • 运算符 defined
      • #ifdef 和 #ifndef
      • 题外话
  • 其他指令
    • #error
    • #line
    • #pragma
      • #pragma和#error的区别
  • 文件包含
      • 防止头文件被重复引用
      • #pragma和#error的区别
  • 文件包含
      • 防止头文件被重复引用

写在前面

很抱歉,之前说了两天左右一篇的,却拖到现在。这篇博客零零散散耗费的大概十天时间,我一直想和大家分享一个更加完整的博客,同时也提高一下我的编写能力。我看了《C语言程序设计现代方法》中的这一章大概有三遍,其中粗读一遍,精度两遍。有用了一天时间看看比特蛋哥的讲解视频,最终出了这篇博客。我的能力有限,很多时候都是重复观看,也不知道从哪个角度很大家分享。这里面可能会存在一些错误,还请各位原谅。

预处理器

什么是预处理器

我们可能会感觉到很疑惑,什么是预处理器,它和编译器有什么区别?都先不要着急,请听我慢慢道来。预处理器是一个小软件,它在程序编译前处理程序,比如说展开头文件,宏定义中等等,这些就是它的功能。

预处理器的原理

谈到原理,我们就不得不说一下,代码是如何变成一个可执行程序的?由于前面我们已经分享过相关的内容,这里就不浪费大家的时间了。我们在之前的学习种看到了这两种情况 #include <stdio.h>#define ,这两个才是我们今天的主菜。我们会发现,无论是那种情况,我们都会看到 #,它就是预处理指令的开头。

  • #include 是告诉编译器打开一个特定的文件,把它的内容包含到我们的代码中
  • #define 是定义一个

注意,当程序经过预处理器后,这些预处理指令都被去掉了,是替换成了空格,不是简单的删除

预处理指令

多数预处理指令都从属下面三种类型,我们先来看看。后面我都会说到

  1. 宏定义 #define
  2. 条件编译 #ifndef 、#if等等
  3. 文件包含 #include

宏定义

宏定义是一个很简单的东西,我们先来了解一下什么是,所谓的宏就是一种文本替换的模式,没有什么神秘的。我们分两个模块说一下宏的知识点。

为何要有宏

这个就是十分简单了,有时候我们会写很多一摸一样的数据,但是修改这些数据时要一个一个寻找,代码要是多了,我们不可能每次都保证准确无误的修改完比,还有这个情况,我们写圆周率,有可能有会敲出呢给3.14156926、3.1415681…等等,这也是常见的,所以我们与需要宏。

简单的宏

说了这么多,我们来正式看看吧,很简单。

#define PI 3.14159fint main()
{printf("%f\n", PI);return 0;
}

image-20220331130947697

注意事项

宏很好使用,但是我们也需要看看他的一些要求,

  • 和 define 之间可以存在任意个空格字符
#      define PI 3.14159f
  • 要是我们定义的宏过于长,定义时需要换行的话必须加上 \
#define PI \3.14159f
  • 我们允许使用一个宏来定义另一个宏,下面是允许的
#define PI 3.14159f
#define TWOPI 2*PI
  • 允许只定义宏,不给它替换的结果
#define A          // 允许
  • 不建议在宏后面加上==;==,预处理器会把分号当成宏对额一部分
#define A 10;         // 禁止
int arr[A];  == int arr[10;]; //报错 
  • 我定义的的宏的名字和大部分程序员一样,都使用了大写,建议大家使用,这样更加醒目

宏的作用

谈起宏的作用,永远逃不过注释和替换的关系,后面我要先谈一谈什么是注释。

替换

宏的替换是很简单的,就是一个简单的文本替换,发生在预处理阶段。这一点我们没有什么可以疑惑的,不过我还是用图片表示一下吧,方便大家理解。

image-20220402101018414

注释

所谓的注释就是我们给代码一个解释,解释变量的命名或者函数的作用等等,这些我们是都知道的,在C语言中存在两种注释的方法

  1. 行注释 // C++风格
  2. 块注释 / /

这里我用图片表示一下就可以了,下面也是他们的区别

  • 行注释只能注释一行
  • 块注释可以注释多行

image-20220402095438589

注释的内容经过预处理后变成了什么?

这个才是我们希望关注的问题,究竟是被删除了?还是有其他的方法?我们是不是可以按照下面的写代码呢?,这都是我们需要解决的问题。我们在Linux环境下演示。

int main()
{int child = 18;   int par/*a*/ent = 20;return 0;
}

image-20220402100429914

从这里我们就可以看出,所谓的注释,就是把我们解释的内容变成一个空格,这一点是非常重要的。

注释和宏替替换哪个先开始

我们可能会有一些疑惑,既然注释和宏都会发生替换,那么他们哪个是先执行的呢?

这里我们直接给出结论,是先发生去注释 ,后执行宏替换,下面都是举例子来证明这个结论的

#define BSC //      int main()      
{      BSC printf("Hello word\n");    printf("Hello word\n");    return 0;                                                                                              
}                                                                

image-20220402103236721

注意我们开始分析了,要是先发生宏替换,后注释就是应该是下面的结果

  1. 宏BSC 发生替换,变成 //
  2. 第一行hello word由 BSC printf(“Hello word\n”);  变成 // printf(“Hello word\n”);
  3. 发生注释,注释被替换成一个空格

所以这个方法的结论,和我们我们得到结果不一样,那么只能是我们所说的结论"先发生注释替换 后执行宏替换"是正确的,有些人可能还会对这个感到疑惑,我们按照这个结论再来一遍.

  1. #define BSC // 发生注释,//,后面的注释是空的而已,BSC是一个不带替换结果的宏,这是允许的
  2. 后面发生宏替换,结果就和我们的一样了

刚才我们验证了C++风格的注释,我们是否可以使用 /* /*这样的风格来完成我们的注释呢?

眼尖的我们一眼就可以看出,是先发生了去注释

image-20220402105344880

带参数的宏

带参数的宏就有些麻烦了,我们先来看看例子。

定义一个两个整数最大值的宏

#define MAX(a,b) (a) > (b) ? (a) : (b)int main()
{printf("max = %d\n", MAX(1, 3));return 0;
}

image-20220331134806315

我们对宏的部分有自己的命名

image-20220331135414657

注意事项

带参数的宏也有一些要注意的地方,我们要来看看.

  1. 标识符和替换列表之间一定要有空格
  2. 可以没有参数,
  3. 标识符最好带上括号,我下面重点说

运算符的优先级一直是一个大问题,有时候我们不能确定用户传入什么样的参数,优先级的高低可能会造成巨大为错误,而且编译器还检测不出来

我们想要的结果是 15,可惜了

#define MUL(a,b) a * bint main()
{printf("%d", MUL(2 + 1,2+3)); //代码  2 + 1 * 2 + 3 = 7return 0;
}

image-20220331150615650

宏定义中的运算符

这里我来简绍一下宏定义中的两个运算符,这两运算符有很大的作用

  1. # 字符串化运算符
  2. ## 粘合运算符

# 运算符

#是字符串化运算符,它可以将数据字符串化,在宏中我们使用#1 可以把1转化成字符串 “1”.下面有一个例子

我们希望得到这样的结果,每输入一个变量,既能打印出变量的值,也能显示变量名

image-20220331151523009

要是我们要求的数据多了,一点一点写可能不是一个好办法,函数也完不成我们想要的任务,但是借助 # 我们是可以的

#define PRINT(n) printf(#n " is value %d\n",n)
int main()
{int a = 10;int b = 20;int c = 30;PRINT(a);PRINT(b);PRINT(c);return 0;
}

image-20220331151523009

## 运算符

名如其人,##就是一个可以粘合多个字符的运算符

#define A(n) n##n
int main()
{printf("%d", A(1));return 0;
}

image-20220331152314428

注意一下,##好象不支持嵌套

#define A(n) n##n
int main()
{printf("%d", A (A(1)) );return 0;
}

image-20220331152703993

参数可以为空或者少一点

#define N(a,b,c) a##b##cint main()
{printf("%d \n", N(1, 1, 1));printf("%d \n", N(1,,1));printf("%d \n", N(1,,));printf("===============\n");printf("%d \n", N(1));printf("%d \n", N(1,1));printf("%d \n", N(1,1,1));return 0;
}

image-20220402193254988

宏的优缺点

我虽然很推崇宏,但是很少使用宏的特点,一般就是就是定义一个数组的容量.不过大家还是要知道这些优点和缺点,以便我们未来可能会使用到

优点

  1. 相对于函数,宏的速度可能更快一点,函数需要开辟栈帧
  2. 宏容易修改数据
  3. 宏可以出现在代码的任何位置
  4. 宏不像变量一样有作用域,他的作用范围是在被定义之后

宏可以出现在代码的任何位置

int main()
{#define A 10printf("%d", A);return 0;
}

image-20220331150117914

作用范围

image-20220402111537356

缺点

  1. 编译后代码可能会变大,每次使用宏,都会发生替换
  2. 无法使用一个指针指向宏
  3. 宏没有参数类型检查
  4. 宏可能会不止一次计算它的参数

这里重点说一下第四条.我们使用一下代码,计算两个数的最大值

#define MAX(a,b) (a) > (b) ? (a) : (b)int main()
{int i = 2;int j = 1;int max = MAX(i++,j);printf("max = %d\n", max);printf("i = %d\n", i);return 0;
}

image-20220331145443926

是不是很奇怪,我们得到了最大值,但是i的值却不是我们想要的,实际上代码经过预处理后面,编程了下面的代码

i被错误的执行了两次++

int main()
{int i = 2;int j = 1;int max = (i++) > (j) ? (i++) : (j);printf("max = %d\n", max);printf("i = %d\n", i);return 0;
}

预定义宏

C语言也自己定义了一部分宏,这些我们是可以直接使用的

名字说明
_FILE_打印文件位置
_LINE_打印这一行代码所在的行数
_DATE_编译的日期
_TIME_编译的时间
_STDC_如果编译器符合C标准(C89/C99) ,就是 1,否则就是 就是未定义
int main()
{printf("%s\n", __FILE__);printf("%d\n", __LINE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);//printf("%d\n", __STDC__); // VS2020 支持部分C语言return 0;
}

image-20220331155358112

C99增加的几个预定义宏

除此之外C99标准下有增加了几个宏,简单简绍一下,了解就可以了。

image-20220331160926955

例题

我们要是知道这些代码会出现什么现象,就可以理解宏了

// demo1
int main()
{
#define X 3
#define Y X * 2
#undef X
#define X 2int z = Y;printf("%d\n", z);return 0;
}

image-20220402175436206

// demo2
void show()
{printf("%d",X);
}
int main()
{   
#define X 3//#undef Xshow();return 0;
}

image-20220402175519645

// demo3
#define X 3
void show()
{printf("%d",X);
}
int main()
{   #undef Xshow();return 0;
}

image-20220402175552333

条件编译

总算来到这里,要是你看累了,可以稍微休息你会,我下面接着分享,关于条件编译我们还要说很多.

什么是条件编译

所谓的条件编译就是我们根据预处理的结果来修改代码,就像我们手持一把手术刀,看到哪里由顽疾,就把它切除一样.

为何要有条件编译

我们通过条件编译的本质是可以完成代码的裁剪工作,这就意味着我们可以通过裁剪代码来使程序可以在不同的环境下运行,也可以使一些功能开放给用户,一些开放给付费用户

  1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小
  2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现

关于使用条件编译,举一个例子吧.
我们经常听说过,某某版代码是完全版/精简版,某某版代码是商用版/校园版,某某软件是基础版/扩展版等。
其实这些软件在公司内部都是项目,而项目本质是有多个源文件构成的。所以,所谓的不同版本,本质其实就是功能的有
无,在技术层面上,公司为了好维护,可以维护多种版本,当然,也可以使用条件编译,你想用哪个版本,就使用哪种条件
进行裁剪就行。

指令

我们大该会有两组指令要谈.

  1. #if 和 #endif
  2. #ifdef 和 #ifndef

#if 和 #endif

#if   常量表达式1<代码块1>
#elif 常量表达式2<代码块2>
#else <代码块2>
#endif

我们直接来看看它的作用吧,一目了然.

#include <stdio.h>#define M 10int main()
{
#if Mprintf("M已经被定义\n");
#endifprintf("Hello word");return 0;
}

image-20220402114209649

作用

**这里和 if else判断语句作用差不多,我们也可以添加#elif, #else等指令,使用的方法和作用都是和 if else一样.**不过它们是在预处理阶段进行的,我先翻译一下.

我们是不是定义了宏 M ,要是执行printf函数,并且把指令删除 ,要是没有定义M,就把这部分代码删除

#if Mprintf("M已经被定义\n");
#endif

image-20220402131940722

我们假设一个不存定义的宏

image-20220402132130134

这里我想说一下,只要宏被定义了并且给了值才可以才可以进行替换,这个命令的本质是判断#if 后面的值,0 就是假,非零就是真

image-20220402132551958

image-20220402134059676

image-20220402134125418

运算符 defined

我们前面说了# 和##运算符,不过那是在宏中使用的,现在这个运算符defined,是在条件编译中作用.我们一起俩看看他的作用.我们上面的指令是有一定的缺陷的,他的本质是替换,而我们下面所所说就可以很好的区别开来

它会判断我们的宏是不是定义了,不管有没有值,也不管值是多少,只要定义了就是1,否则就是0

image-20220402134414563

#ifdef 和 #ifndef

这是另一组条件编译,我们可以认为它是上面的简写,我翻译一下就可以了

  • ifdef M 如果定义了宏 M 保留它该保留的代码
  • #ifndef 如果没有定义宏 M, 保留它该保留的代码.这个主要用于 防止头文件被重复引用

题外话

  1. 宏也可以在命令行中定义
  2. 条件编译支持嵌套,规则和if else一样

其他指令

除了上面的一些,我们还有其他的一些指令,我们来来了解一下,这些指令都是为了预处理服务的,我们平常很少使用

#error

核心作用是可以进行自定义编译报错

int main()
{#ifndef __CPP
#error 老铁,你用的不是C++的编译器哦
#endifreturn 0;
}

image-20220402180036872

#line

本质其实是可以定制化你的文件名称和代码行号,很少使用

int main()
{printf("%s, %d\n", __FILE__, __LINE__); 
#line 60 "hehe.h" //定制化完成printf("%s, %d\n", __FILE__, __LINE__);return 0;
}

image-20220402180336151

#pragma

#pragma message()作用:可以用来进行对代码中特定的符号(比如其他宏定义)进行是否存在进行编译时消息提醒

#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endifreturn 0;
}

image-20220402180703393

//#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endifreturn 0;
}

image-20220402180750107

#pragma和#error的区别

  1. #pragma只是提示,程序仍旧执行
  2. #error是报错,程序中断

文件包含

这个挺简单的,就是代码在经过预处理后将文件纳入代码中,关于文件我们有时会出现==<>====“”==这两种情况,我们希望能够了解他们的不同

  1. <> 直接去标准库中寻找头文件 适合标准库
  2. “” 先去当前目录下寻找,找不到的话再去标准库中寻找 适合自己定义的头文件

防止头文件被重复引用

我们避免多次引用头文件,代码块变大,所以使用条件编译来防止头文件被重复引用

#ifndef __TEST_H__
#define __TEST_H__#include <stdio.h>
#include <string.h>#endif// 如果没有定义 \__TEST_H__宏,那就定义它,把头文件包含在这一中间
// 后面我们要是再次判断这个宏,就会发现已经被定义了,所以就删除#ifndef __TEST_H__
#define __TEST_H__#include <stdio.h>#endif

{
#ifdef M
#pragma message(“M宏已经被定义了”)
#endif
return 0;
}

[外链图片转存中…(img-MPN8Z3UZ-1648899254515)]

//#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endifreturn 0;
}

[外链图片转存中…(img-W2NQ2374-1648899254515)]

#pragma和#error的区别

  1. #pragma只是提示,程序仍旧执行
  2. #error是报错,程序中断

文件包含

这个挺简单的,就是代码在经过预处理后将文件纳入代码中,关于文件我们有时会出现==<>====“”==这两种情况,我们希望能够了解他们的不同

  1. <> 直接去标准库中寻找头文件 适合标准库
  2. “” 先去当前目录下寻找,找不到的话再去标准库中寻找 适合自己定义的头文件

防止头文件被重复引用

我们避免多次引用头文件,代码块变大,所以使用条件编译来防止头文件被重复引用

#ifndef __TEST_H__
#define __TEST_H__#include <stdio.h>
#include <string.h>#endif// 如果没有定义 \__TEST_H__宏,那就定义它,把头文件包含在这一中间
// 后面我们要是再次判断这个宏,就会发现已经被定义了,所以就删除#ifndef __TEST_H__
#define __TEST_H__#include <stdio.h>#endif

http://www.ppmy.cn/news/191660.html

相关文章

计算机酷睿处理器排行,英特尔酷睿处理器哪个型号好?2018年4月电脑CPU性能排名...

英特尔酷睿是英特尔旗下的高端处理器&#xff0c;目前出到i9系列&#xff0c;处理和运算更加快速&#xff0c;性能更强&#xff0c;那么哪个型号的处理器最好?什么处理器玩游戏最好?排行榜123为大家整合了英特尔酷睿电脑CPU性能排名&#xff0c;告诉您英特尔酷睿高端CPU产品有…

AP与BB(应用处理器和基带处理器)

FCC&#xff08;美国联邦通信委员会&#xff09;认证要求将AP和BP分开&#xff0c;因为射频控制相关的功能&#xff08;信号调制、编码、射频位移等&#xff09;都是高度的时间相关的&#xff0c;最好能将这些函数放在一个CPU核上运行&#xff0c;并在这个CPU核上运行一个实时的…

springboot自定义参数处理器和返回值处理器

参数处理器(ArgumentResolvers)和返回参数处理器(ReturnValueHandlers) 在我们调用controller层组件时&#xff0c;Springboot实际上是使用代理模式进行调用&#xff0c;springmvc定义了一个DispatcherServlet实现HttpServlet方法&#xff0c;通过DispatcherServlet的doservice…

初识 Arm 处理器

英国ARM公司是全球领先的半导体知识产权&#xff08;IP&#xff09;提供商。全世界超过95%的智能手机和平板电脑都采用ARM架构。ARM设计了大量高性价比、耗能低的RISC处理器、相关技术及软件。2014年基于ARM技术的全年全球出货量是120亿颗&#xff0c;从诞生到现在为止基于ARM技…

嵌入式处理器选型指南

1. 微处理器分类 根据通用计算机和嵌入式系统的分类&#xff0c;把微处理器分为&#xff1a;通用处理器 嵌入式处理器 1.通用处理器 以x86体系架构的产品为代表,目前基本为Intel和AMD两家公司所垄断。 2.嵌入式处理器 嵌入式系统领域有少量通用处理器&#xff0c;但以嵌入…

世界上计算机最好的处理器,世界上最厉害的电脑处理器是什么?

如果这里的“电脑”限定为个人计算机&#xff0c;那么最厉害的处理器&#xff0c;就绝对性能而言是Intel的酷睿i9-7980XE&#xff0c;综合考虑价格那么是AMD的锐龙ThreadRipper 1950X。 Intel一直是电脑处理器的王者&#xff0c;前些年因为AMD竞争发力&#xff0c;Intel自己也懒…

安腾处理器 oracle,英特尔展示下一代安腾处理器Poulson

英特尔即将推出的Poulson处理器将比目前安腾9300处理器具有更强的性能、更高的稳定性和更低的功耗。但目前英特尔官方拒绝透露该处理器的具体发布日期。 英特尔官方表示&#xff0c;即将推出的Poulson安腾处理器将会采用一个全新的架构&#xff0c;该架构会成为日后处理器产品的…

关于Windows10虚拟机处理器数量的问题

本人使用VMware 16.1安装了Windows 10 64位虚拟机&#xff1a; 最近发现安装的Windows 10虚拟机提示一个错误&#xff1a; 虚拟机配置使用的虚拟处理器插槽数量多于客户机所支持的数量。 对这个问题很疑惑。 按照我之前的理解&#xff0c;这里的处理器数量是CPU的核数&#x…