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

news2024/12/23 13:51:18

         前言:预处理是我们的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 + y

int 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 * y

int 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 + y

int 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 10

int 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 10

int 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 10

int 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 10

int main()
{
	printf("%d", MAX);
	
#undef MAX
	printf("%d", MAX);

	return 0;
}

#endif

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

#if 1

#define MAX 10

int main()
{
	printf("%d", MAX);
	
#undef MAX
	printf("%d", MAX);

	return 0;
}

#endif

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

 又或者if, elif, endif的使用

#define MAX 0


int main()
{

#if MAX == 0

	printf("%d", MAX);

#elif MAX != 0
	printf("%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.coloradmin.cn/o/1562264.html

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

相关文章

【C语言基础】:自定义类型(二) -->联合和枚举

文章目录 一、联合体1.1 联合体类型的声明1.2 联合体的特点1.3 相同成员的结构体和联合体对比1.4 联合体大小的计算1.5 联合体练习 二、枚举类型2.1 枚举类型的声明2.2 枚举的优点 书山有路勤为径&#xff0c;学海无涯苦作舟。 创作不易&#xff0c;宝子们&#xff01;如果这篇…

嵌入式介绍

1、嵌入式系统学习的三条路线 单片机入门HAL 单片机核心/RTOS Liuxc 2.学习嵌入式的三条路线的优缺点 2.1.单片机入门(HAL) 简单、快速&#xff0c;实际上工作中涉及单片机编程时&#xff0c;也提倡使用HAL库。 对于学习来说&#xff0c;HAL封装了很多技术细节&#xff0c…

使用虚拟引擎为AR体验提供动力

Powering AR Experiences with Unreal Engine ​​​​​​​ 目录 1. 虚拟引擎概述 2. 虚拟引擎如何为AR体验提供动力 3. 虚拟引擎中AR体验的组成部分是什么&#xff1f; 4. 使用虚拟引擎创建AR体验 5. 虚拟引擎中AR的优化提示 6. 将互动性融入AR与虚拟引擎 7. 在AR中…

Python反爬案例——验证码的识别

验证码的识别 使用打码平台识别验证码 利用打码平台可以轻松识别各种各样的验证码&#xff0c;图形验证码、滑动验证码、点选验证码和逻辑推理验证码。打码平台提供了一系列API&#xff0c;只需要向API上传验证码图片&#xff0c;它便会返回对应的识别结果。 使用超级鹰平台…

Qt 实现的万能采集库( 屏幕/相机/扬声器/麦克风采集)

【写在前面】 之前应公司需要&#xff0c;给公司写过一整套直播的库( 推拉流&#xff0c;编解码)&#xff0c;类似于 libobs。 结果后来因为没有相关项目&#xff0c;便停止开发&维护了。 不过里面很多有用的组件&#xff0c;然后也挺好用的&#xff0c;遂开源出来一部分。…

软件测试(测试用例详解)(三)

1. 测试用例的概念 测试用例&#xff08;Test Case&#xff09;是为了实施测试而向被测试的系统提供的一组集合。 测试环境操作步骤测试数据预取结果 测试用例的评价标准&#xff1a; 用例表达清楚&#xff0c;无二义性。。用例可操作性强。用例的输入与输出明确。一条用例只有…

怎么在UE游戏中加入原生振动效果

我是做振动触感的。人类的五感“视听嗅味触”&#xff0c;其中的“触”就是触觉&#xff0c;是指皮肤、毛发与物体接触时的感觉。触感可以带来更加逼真的沉浸式体验。但也许过于司空见惯&#xff0c;也是习以为常&#xff0c;很多人漠视了触感的价值。大家对触感的认知还远远不…

skywalking idea中启动调试报错Output path is shared between the same module error

报错信息 简单描述&#xff1a;就是多个moudle一样用了一样的输出路径&#xff0c;这样容易造成冲突 Output path is shared between the same module error 参考&#xff1a;scala - Output path is shared between the same module error - Stack Overflow 解决方法&…

VScode使用Prettier格式化代码

1、安装Prettier插件 2、扩展设置 3、设置.prettierrc.json配置文件路径 4、.prettierrc 配置文件 .prettierrc.json 是 Prettier 格式化工具的配置文件&#xff0c;用于指定代码格式化的规则和风格。下面是一些可能的配置选项&#xff0c;请自行选择&#xff1a; {"prin…

spring boot自动配置原理-怎样回答这个问题

首先我们说一下自动配置的概念。 自动配置&#xff1a;遵循约定大约配置的原则&#xff0c;在boot程序启动后&#xff0c;起步依赖中的一些bean对象会自动注入到ioc容器 例子 程序引入spring-boot-starter-web 起步依赖&#xff0c;启动后&#xff0c;会自动往ioc容器中注入…

一文了解微带天线

微带天线介绍 微带天线的结构一般由介质基板、辐射体及接地板构成。介质基板的厚度远小于波长&#xff0c;基板底部的金属薄层与接地板相接&#xff0c;正面则通过 光刻工艺 制作具有特定形状的金属薄层作为辐射体。 辐射片的形状根据要求可进行多种变化。微波集成技术和新型制…

高频小信号放大器概述

高频放大器与低频&#xff08;音频&#xff09;放大器的主要区别是&#xff1a;工作频率范围和所需通过的频带宽度都有所不同。其采用的负载也不相同。 低频放大器的工作频率低&#xff0c;但工作频带宽度很宽&#xff0c;所以负载采用无调谐负载&#xff0c;例如电阻、有铁心…

Docker容器监控之CAdvisor+InfluxDB+Granfana

介绍&#xff1a;CAdvisor监控收集InfluxDB存储数据Granfana展示图表 目录 1、新建3件套组合的docker-compose.yml 2、查看三个服务容器是否启动 3、浏览cAdvisor收集服务&#xff0c;http://ip:8080/ 4、浏览influxdb存储服务&#xff0c;http://ip:8083/ 5、浏览grafan…

flink: 将接收到的tcp文本流写入HBase

一、依赖&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.o…

总结:微信小程序中跨组件的通信、状态管理的方案

在微信小程序中实现跨组件通信和状态管理,有以下几种主要方案: 事件机制 通过事件机制可以实现父子组件、兄弟组件的通信。 示例: 父组件向子组件传递数据: 父组件: <child binddata"handleChildData" /> 子组件: Component({..., methods: { handleChildData(…

VTK 简介

VTK 简介 VTK 简介什么是 VTK&#xff1f;VTK 能做什么&#xff1f;VTK 的基本组成VTK 的框架结构VTK 的数据结构VTK 的可视化流程参考 VTK 简介 什么是 VTK&#xff1f; VTK&#xff0c;全称是Visualization Toolkit&#xff0c;即可视化工具包。是一个开源、跨平台、可自由…

C++面向对象程序设计 - 访问对象中成员的3种方法

在C程序中访问对象的成员变量和成员函数&#xff0c;有三种方法&#xff1a; 通过对象名和成员运算符访问对象中的成员&#xff1b;通过指向对象的指针访问对象中的成员&#xff1b;通过对象的引用变量访问对象中的成员 在了解访问对象中成员的3种方法前&#xff0c;先了解下C…

uniapp 小程序和app map地图上显示多个酷炫动态的标点,头像后端传过来,真机测试有效

展示效果 二、引入地图 如果需要搜索需要去腾讯地图官网上看文档&#xff0c;找到对应的内容 1.申请开发者密钥&#xff08;key&#xff09;&#xff1a;申请密钥 2.开通webserviceAPI服务&#xff1a;控制台 ->应用管理 -> 我的应用 ->添加key-> 勾选WebService…

LCD TP触摸屏调试方法

一、硬件连接 I2C总线&#xff1a;I2C-SDA和i2C-SCL 中断信号&#xff1a;touch-gpio 复位信号&#xff1a;reset-gpio 电源信号&#xff1a;power-gpio 二、驱动调试 2.1 确认从设备地址 在给TP供电正常后&#xff0c;检测其I2C设备从地址&#xff0c;或者通过datashee…

香港科技大学广州|数据科学与分析学域硕博招生宣讲会—天津大学专场

时间&#xff1a;2024年4月12日&#xff08;星期五&#xff09;14:00 地点&#xff1a;天津大学北洋园校区55楼B204 报名链接&#xff1a;https://www.wjx.top/vm/Q0cKTUI.aspx# 跨学科研究领域 *数据驱动的人工智能和机器学习 *统计学习和建模 工业和商业分析 *特定行业的数…