预处理指令——一些比较少见的概念

         前言:预处理是我们的c语言源代码成为可执行程序的第一个步骤。而宏和预处理指令都是在这个阶段完成。本节内容就是关于宏和预处理指令相关知识点的解析。

目录

预定义符号

#define定义常量

#define定义符号

#define定义宏

带副作用的宏参数 

宏的替换规则

宏相对于函数的特点  

#和##

#

##

  命名约定

undef 

 命令行定义

条件编译 

头文件包含 

如何防止头文件被多次包含:


        

预定义符号

        在c语言之中, 定义了下面这几个宏定义符号。同样的, 因为预定义符号是宏, 它也是在预处理阶段进行处理。

        __LINE__代表文件当前位置的行号;

        __FILE__代表当前的源文件;

        __DATE__代表文件被编译的日期;

        __TIME__代表了文件被编译的时间;

        __STOC__如果编译器遵循标准c语言也就是ANSI C, 那么这个值就是1, 否则就是未定义。

#define定义常量

        #define可以定义常量, 具体做法如下

#include<iostream>
using namespace std;#define MAX 10//定义常量
int main()
{for (int i = 0; i < MAX; i++) {cout << i << endl;}return 0;
}

        这里就是#define定义常量, 常量值应该在常量名后边 。 

        #define定义常量的本质其实就是一种替换, 预处理阶段, 编译器会将源代码中的#define定义的常量进行替换。 什么意思?如下:

        现在我们还是看上面一串代码:

         当预处理之后, 这串代码就变成了:

#include<iostream>
using namespace std;int main()
{for (int i = 0; i < 10; i++) {cout << i << endl;}return 0;
}

        这里的MAX进行了替换。 

        既然#define定义常量的本质是完成替换, 那么在进行#define定义常量的时候需不需要在末尾加上分号?

        答案是不需要, 因为加上分号可能出现以下这种情况:

        这里就出现了一个问题, 当MAX被替换后, for的括号里变成了: “int i = 0; i < 10;;i++", 第二个判断条件和第三个变化条件之间有了两个分号, 相当于for括号里有了四条语句。这就出现了问题。所以, 当我们使用#define定义常量的时候, 不要再末尾加上分号!

        当然, 这里的常量也包括常量字符串

#include<iostream>
using namespace std;#define STR "sdfsd"int main() 
{cout << STR << endl;return 0;
}

#define定义符号

         #define也可以定义符号, 比如:


#include<iostream>
using namespace std;#define FOR for(;;)int main() 
{FOR;return 0;
}

        注意, 这串代码就是一个死循环。 因为FOR是#define定义的符号。 在预处理的时候, FOR被替换成了for(;;),这里面判断条件为空, 恒为真,所以就是一个死循环。 

        #define也可以定义一串很长的代码:

#include<iostream>
using namespace std;#define MYFILE printf("name:%s, line:%s, data:%s",\__FILE__, __LINE__, __DATE__)
int main() 
{MYFILE;return 0;
}

 

        这个红色箭头指向的其实是续行符, 后面不能加空格, 否则续行符无效。续行符之后直接回车换行。 

#define定义宏

        什么是宏, 宏和上面的定义符号和定义常量有什么区别?

        同样的, 宏也是#define定义的一串代码, 但是它和上面的区别是宏是有参数的。定义宏的时候, 参数列表必须紧紧挨着宏名, 否则, 参数列表会被当成宏体。

        如下就是一个宏定义:

#include<iostream>
using namespace std;#define ADD(x, y) x + yint main() 
{int ret = ADD(1, 2);return 0;
}

        这个宏定义的工作原理是这样的, 首先1先传给x, 2传给y, 然后ADD参数列表的x, y的值传给宏体。 再完成替换。

        替换后就是这样的 

#include<iostream>
using namespace std;int main() 
{int ret = 1 + 2;return 0;
}

         现在想一个问题。既然宏是在预处理阶段就完成替换, 那么他是不是比函数的速度快。 因为函数需要在运行的时候调用, 而宏是在预处理阶段就完成替换, 宏体的代码就在相应的位置展开了。

        答案是是的, 宏确实要比函数快。宏在预处理阶段直接完成替换, 不需要去建立栈帧消耗时间。而且, 宏的参数也没有类型:

        参数没有了类型, 就相当于没有了类型检查。 这样有好处也有缺点, 好处是参数没有了类型, 更加的灵活。 但缺点也是如此, 因为宏的参数没有了类型, 没有了类型检查, 代码就容易出现问题。 这说明了宏不易调试的缺点。

        这里说明宏也不全是优点, 他也是有缺点的。

        另外,宏还有另外一个不可忽视的缺点——优先级问题。 现在我们来看这么一串代码。 

#include<iostream>
using namespace std;#define Mul(x, y) x * yint main() 
{int ret = Mul(1 + 3, 2 + 3);return 0;
}

         这一串代码, 宏替换后是这样的: 1 + 3 * 2 + 3;

         这显然与我们的预期不符。如果这里是一个函数的话。 那么参数传送过去就应该是:4 * 5;这样的优先级问题可以成为内部的优先级问题。

        那么, 外部的优先级问题呢?


#include<iostream>
using namespace std;#define ADD(x, y) x + yint main() 
{int ret = ADD(1, 2) * 4;return 0;
}

         如图就是一个外部的优先级问题。 宏替换后代码是这样的: 1 + 2 * 4; 与我们的预期同样不符。我们的预期是这样的: 1 + 2 等于三, 然后3 * 4;

        所以, 这里也涉及到了优先级的问题。 

        我们要解决上面的问题, 那么定义的宏体就应该解决内部和外部的优先级问题。 可以这样定义:

#define ADD(x, y) ((x) + (y))

带副作用的宏参数 

        有一些表达式是有副作用的。 

        比如说++, --符号。 

int main() 
{int a = 0;int b = 1;int a = ++b;return 0;
}

        看这串代码, 前置++对于b来说就是有副作用的。 虽然给a赋值了一个b + 1, 但是b自身的值也发生了改变。 

        我们定义一个求最大值的宏:

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

 在这个表达式中, 看似是没有问题的。 但是如果我们使用自增自减符号的时候就有问题。 

#include<iostream>
using namespace std;#define MAX(x, y) x > y ? x : y
int main() 
{int a = 3;int b = 4;int ret = MAX(a++, b++);cout << a << endl;cout << b << endl;cout << ret << endl;return 0;
}

        这个参数是如何进行的呢

        其实, 替换之后应该是这样的:a++ > b++ ? a++ : b++;

        这里的a++和b++都出现了两份

        这里都是后置加加, a++ > b++这里是a的值3和b的值4进行比较。 比较完之后a加一编程4, b加一变成5。然后3 小于4, 执行b++, b的值进行返回, 返回的是五, 但是b此时还进行了一次++, 所以b变成了6. 所以ret为5, a为4, b为6.

         所以如果宏的参数在代码中不知出现一次, 而且宏的参数带有副作用。 那么代码就可能出现问题。 因为宏的参数不是计算之后再传进去, 而是直接进行替换。 

宏的替换规则

         1、在调用宏时, 首先对参数进行检查,看是否包含任何#define定义的符号。如果是, 首先        被替换。 如图:

#include<iostream>
using namespace std;#define MAX(x, y) x > y
#define M 10int main() 
{int a = 3;MAX(a, M);return 0;
}

         这里先进行替换的就是M, 将M替换为10之后再替换MAX, 替换后就是a > 10

        

         注意, 宏参数和#define定义中可以出现其他#define定义的符号, 但是宏不能出现递归。 宏不支持递归。

#include<iostream>
using namespace std;#define MAX(x, y) x > y
#define M 10int main() 
{int a = 3;MAX(a, MAX(a, 1));return 0;
}

        注, 这里并不会进行递归。 他只是将a和1先传参, 替换掉里面的宏定义。 然后得到的结果和a再进行传参, 替换掉外面的宏。

        并且, 字符串中的宏并不会被检测为宏。比如:

#include<iostream>
using namespace std;#define MAX(x, y) x > y
#define M 10int main() 
{int a = 3;const char* a = "dsfsM";return 0;
}

这里字符串中的M就不会被检测为宏, 不会被替换掉。

宏相对于函数的特点  

相比于函数, 宏的特点有这些:1、首先宏不能调试

                                                    2、宏不能进行递归

                                                    3、宏的速度很快, 它是直接代码替换,而不是在运行时建立栈帧。

                                                     4、宏的参数没有类型, 不会进行类型检查 

                                                     5、宏展开会增加代码长度。

                                                     6宏的参数可以出现类型, 但是函数做不到,例如:


#include<iostream>
using namespace std;
#include<stdlib.h>#define Malloc(n, type) (type*)malloc(n * sizeof(type))int main() 
{int* ptr = Malloc(1, int);return 0;
}

在这串代码中, 宏Malloc将1和int这个类型传过去, 但是函数一定做不到。 

#和##

#

        #运算符将宏的一个参数, 直接转化为字符串字面值。 它仅允许出现在待参数的宏的替换列表之中。

        #运算符所执行的操作符可以理解为”字符串化“。

意思就是说我们可以这样定义一个宏:


#include<iostream>
using namespace std;
#include<stdlib.h>#define Print(a, format) printf("the value of " #a " is "format, a); int main() 
{int a = 10;Print(a, "%d");return 0;
}

 

        在这个宏中, #a预处理阶段会被转化为“a", #的作用就是这样, 将一个宏参数转变为字符串字面量形式。 

##

        ##可以把位于两边的符号变成一个符号。 但是这样的链接必须是合法的标识符, 否则结果就是未定义的。 

        

如图, 如果我们想要求处某个类型的最大值, 但是int类型和float的类型都要定义一个求取最大值函数。 这样就很麻烦, 这个时候如果我们定义这样的一个宏, 就可以解决问题。 

#define GEN_MAX(type)    \type type##_max(type x, type y)\{   \return x > y ? x : y ;\}

这个宏其实就可以生成一个函数。 而且type##_max中##将两边的链接, 其实就相当于是type_max。

假如我这里这样传参: 

红色箭头就相当于生成了两个函数。 

 这里我们使用这两个函数:

这其实就像模板一样。 

 
 命名约定

        一般我们宏都是定义为全大写, 不是宏不会定义为全大写。 但这些也不是一定的。 比如offsetof。        

        (offsetof 的作用是结构体成员相较于结构体其实位置的偏移量。)

undef 

undef的功能就是取消undef对应行之后的宏定义。

 命令行定义

        某些c语言编译器,允许在命令行进行定义符号。当我们要使用一个代码的不同版本的时候, 就可能用到这个命令行定义。 一些机器中大一些, 就可以开大一点的数组。 一些机器种小一些, 就可以开小一点的数组。

条件编译 

       条件编译, 最重要的就是这几个命令:1、#if #endif

比如: #define ……

#if

#endif 

2、多分支:

#if

#elif 

#elif

#endif

3、判断是否被定义

#if defined

或者

#ifdef 

或者

if !defined

或者

ifndef

条件编译就是满足条件就进行编译, 如果不满足条件,就不要进行编译。 

比如if和endif的使用

#if 0#define MAX 10int main()
{printf("%d", MAX);#undef MAXprintf("%d", MAX);return 0;
}#endif

这里面这一串代码在预处理阶段就会被销毁, 相当于被注释掉了。 

#if 1#define MAX 10int main()
{printf("%d", MAX);#undef MAXprintf("%d", MAX);return 0;
}#endif

如果改成1就又会变回来。

 又或者if, elif, endif的使用

#define MAX 0int main()
{#if MAX == 0printf("%d", MAX);#elif MAX != 0printf("%d", MAX);return 0;
}#endif

根据MAX的值, 就会选择编译第一个打印还是第二个打印。

       

头文件包含 

        头文件的包含有两种形式。 一种是双引号“”的形式进行头文件包含。 一种是<>的形式进行头文件包含。 

        双引号的头文件包含形式,是先从原文件目录处寻找, 如果未找到头文件, 那么编译器就像查找库函数头文件一样在标准位置查找头文件。 如果再找不到, 那么就会报错。 

        标准头文件的路径, 再linux环境下, linux标准库的头文件是在/user/include/的路径底下。

        vs2022环境的标准头文件路径是略杂乱的。 一般它是在windowssdk路径之下,这里放的一般是贴近操作系统相关的头文件。 还有一些和c语言语法比较贴近的头文件, 放在了vs2022的路径底下。

        库文件直接去标准路径底下去查找, 如果找不到, 直接报错。

        为什么不让库文件也用“”包含呢?因为库文件是放在标准库里面的, 虽然库文件也可以使用“”进行包含, 但是这样效率就会变低,而且这样不容易区分本地文件和库文件了。

如何防止头文件被多次包含:

        当一个项目中文件数过多, 可能出现头文件重复包含的情况。 如何处理这种情况呢?有两种方法。

        一种就是#pragma once。其实这就是vs之中当我们创建一个头文件时自己加在第一行的一个函数。 

        另一种就是使用刚刚讲过的条件编译。

如图:

#ifndef __TEST_H__#define __TEST_H__//……代码#endif 

        意思就是如过没有定义__TEST_H__, 那么第一行之后的代码就参与编译。 那么第一次我们包含头文件的时候就没有包含__TEST_H__。 这个时候他就会参与编译。然后__TEST_H__被定义。 那么下一次我们再进行这个头文件的包含的时候, 因为__TEST_H__已经被编译过了。 那么第一行代码就为假, 第一行以后的代码就不会被编译。 所以就实现了头文件只包含一次的情况。  

以上, 就是预处理指令的全部内容

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

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

相关文章

SMTP服务器搭建关键步骤?如何配置服务器?

SMTP服务器搭建的注意事项&#xff1f;怎么快速搭建SMTP服务器&#xff1f; 电子邮件已经成为我们日常工作和生活中不可或缺的一部分。SMTP服务器作为电子邮件发送的核心组件&#xff0c;其搭建过程至关重要。下面&#xff0c;AokSend就来详细探讨一下SMTP服务器搭建的关键步骤…

web学习笔记(五十)

目录 1. nodemon 1.1 什么是nodemon 1.2 安装并使用Nodemon 2. Express 路由 2.1 路由的匹配过程 2.2 简单路由 2.3 模块化路由 2.4 注册路由模块 2.5 路由模块添加前缀 3. Express 中间件 3.1 中间件的格式 3.2 中间件的作用 3.3 局部生效的中间件 3.4 中间件…

云计算面临的威胁

目录 一、概述 二、威胁建模分析 2.1 威胁建模的概念 2.2 威胁建模起到的作用 2.3 威胁建模的流程 2.3.1 威胁建模流程图 2.3.2 威胁建模流程内容 2.3.2.1 绘制数据流图 2.3.2.2 威胁识别与分析 2.3.2.2.1 STRIDE威胁分析方法论 2.3.2.3 制定消减措施 2.3.2.3.1 消减…

SBCFormer:能够在单板计算机上以每秒1帧的速度进行全尺寸ImageNet分类的轻量级网络

文章目录 摘要1、引言2、 相关工作2.1、用于移动设备的卷积网络2.2、移动设备上的ViT和CNN-ViT混合模型2.3、评估指标 3、CNN-ViT 混合模型在低端CPU上的应用3.1、设计原则3.2、SBCFormer的整体设计3.3、SBCFormer块3.4、改进的注意力机制 4、实验结果4.1、实验设置4.2、ImageN…

手机一键换ip地址,解锁网络自由

在数字化时代&#xff0c;手机已经成为我们生活中不可或缺的一部分。随着移动互联网的快速发展&#xff0c;手机用户对于网络安全和隐私保护的需求也日益增强。其中&#xff0c;IP地址作为手机在网络中的标识&#xff0c;扮演着重要的角色。有时&#xff0c;出于隐私保护或网络…

【数据结构】顺序表的实现——动态分配

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;数据结构 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进…

第23篇:使能异步复位D触发器

Q&#xff1a;在上篇的异步复位D触发器中添加一个使能信号来实现带使能功能的异步复位D触发器。 A&#xff1a;只要复位信号为高电平&#xff08;RST1)且CLK为时钟上升沿&#xff0c; 如果使能信号也为高电平&#xff08;EN1&#xff09;&#xff0c;输入数据才会被存储。 带…

MFC(一)搭建空项目

安装MFC支持库 创建空白桌面程序 项目相关设置 复制以下代码 // mfc.h #pragma once #include <afxwin.h>class MyApp : public CWinApp { public:virtual BOOL InitInstance(); };class MyFrame : public CFrameWnd { public:MyFrame();// 消息映射机制DECLARE_…

高度不同的流体瀑布css实现方法

商城商品列表 实现瀑布流展示&#xff0c;通过flex或grid实现会导致每行中的列高度一致&#xff0c;无法达到错落有致的感觉&#xff1b; 为此需要用到&#xff1a; CSS columns 属性 columns 属性是一个简写属性&#xff0c;用于设置列宽和列数。 CSS 语法 columns: column-wi…

【Jmeter+Influxdb+Grafana性能监控平台安装与部署】

JmeterInfluxdbGrafana性能监控平台安装与部署 前言Influxdb安装与连接Jmeternfluxdb下载&#xff08;winodws&#xff09;Grafana安装与配置 前言 我们在性能测试过程中&#xff0c;在需要较大并发时&#xff0c;为了尽量避免使用GUI界面来节省资源&#xff0c;通常使用命令行…

VR全景赋能智慧农业,打造沉浸式种植体验平台

随着人口的增长&#xff0c;传统农业也正在面临着不一样的挑战&#xff0c;加上很多人对农业的固有印象&#xff0c;很少有年轻人愿意下到农田里&#xff0c;那么该如何提高产量、降低成本以及引导年轻人深刻感受现代农业成为了急需解决的问题。 随着城市化脚步的推进&#xff…

用Typora+picgo+cloudflare+Telegraph-image的免费,无需服务器,无限空间的图床搭建(避坑指南)

用TyporapicgocloudflareTelegraph-image的免费&#xff0c;无需服务器&#xff0c;无限空间的图床搭建&#xff08;避坑指南&#xff09; 前提&#xff1a;有github何cloudflare (没有的话注册也很快) 首先&#xff0c;是一个别人写的详细的配置流程&#xff0c;傻瓜式教程&am…

基于SSM框架云趣科技客户管理系统论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本客户管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&am…

【问题处理】银河麒麟操作系统实例分享,鲲鹏服务器GaussDB测试ping延迟过高问题

1.问题环境 系统环境 物理机 网络环境 私有网络 硬件环境 机型 TaiShan 200 (Model 2280) (VD) 处理器 HUAWEI Kunpeng 920 5250 内存 32GB*16 显卡 无 主板型号 BC82AMDDRE 架构 ARM 固件版本 iBMC固件版本 3.03.00.31 (U82) 单板ID 0x00a9 BIOS版本 1.8…

应用案例分享|3D视觉引导汽车铅蓄电池自动化拆垛

在汽车制造及相关配套产业链中&#xff0c;铅蓄电池作为关键零部件之一&#xff0c;其生产和处理环节对效率和精准度都有着极高的要求。传统的铅蓄电池拆垛作业往往依赖于人工操作&#xff0c;不仅效率低下&#xff0c;还存在安全隐患。 项目背景 某大型蓄电池企业&#xff0c…

基于UML的系统分析与设计

统一建模语言(Unified Modeling Language&#xff0c;UML)是一种为面向对象系统的产品进行说明、可视化和编制文档的一种标准语言&#xff0c;是非专利的第三代建模和规约语言。UML是面向对象设计的建模工具&#xff0c;独立于任何具体程序设计语言。 毕业设计是实现本科教学培…

OpenHarmony实战:用IPOP调试 OpenHarmony 内核

前言 我使用的是 IPOP V4.1&#xff0c;基于 OpenHarmony 开源系统和 RK3568 开发板&#xff0c;在 PC 上运行此软件&#xff0c;查看运行、错误日志来调试内核。作为网络、嵌入式式内核调试的必备工具&#xff0c;建议同学珍藏。IPOP 运行在 PC 上&#xff0c;操作系统是 Win…

蓝桥杯刷题day13——玩游戏【算法赛】

一、问题描述 小 A 和小 B 两个人在海边找到了 n 个石子&#xff0c;准备开始进行一些游戏&#xff0c;具体规则如下&#xff1a;小 B 首先将 n 个石子分成若干堆&#xff0c;接下来从小 A 开始小 A 和小 B 轮流取石子&#xff0c;每次可以任选一堆石子取走任意个&#xff0c;…

剑指offer--替换空格

一.题目描述 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。例如"We are happy."替换为"We%20are%20happy. 算法一&#xff1a; 算法1:从头到尾遍历,遇到空格把它替换为%20.时间O(n^2),空间O(1) void replaceSpace(char* s)//…

FreeRTOS 任务挂起和恢复API函数

FreeRTOS 任务挂起和恢复API函数使用 挂起的作用就是当我们需要暂停某任务时候&#xff0c;等过一段时间在运行&#xff0c;这个时候要是使用删除和重建的方法就会当时任务进行时候的变量保存的值。当需要将这个任务停止运行一段时间的将这个任务挂起&#xff0c;当重新进行运…