C/C++预处理过程

news2024/11/18 12:34:12

目录

前言:

1. 预定义符号

2. #define定义常量

3. #define定义宏

4. 带有副作用的宏参数

5.  宏替换的规则

6. 宏和函数的对比

7. #和##

8.  命名约定

9.  #undef

10.  命令行定义

11.  条件编译

12.  头文件的包含

13.  其他预处理指令

总结:。


前言:

本篇介绍c/c++的预处理过程,c和c++在预处理阶段基本相同。

1. 预定义符号

c语言定义了一些预处理的符号,这些是可以直接使用的,并且这些符号也是在预处理期间完成的。

  • __FILE__ //进⾏编译的源⽂件
  • __LINE__ //⽂件当前的⾏号
  • __DATE__ //⽂件被编译的⽇期
  • __TIME__ //⽂件被编译的时间
  • __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

2. #define定义常量

例如我们定义100为一个MAX,后面使用MAX都会在预处理阶段替换为100。

#define MAX 100

如果我们觉得定义的东西太长了,也可以分行写,在结尾加上反斜杠\即可:

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ )

现在来考虑一个问题:

在定义常量的时候,要不要加上引号???

#define MAX 1000;
#define MAX 1000

 其实通过#define的性质就可以知道,它在预处理阶段做的只是替换工作,如果将代码放到下面的情况下就会出错:

if(condition)
 max = MAX;
else
 max = 0;

 这样if后面就是两条语句了,而由于if如果不加大括号匹配的只有一条语句,所以这样else就没法匹配了,就出错了。

 

3. #define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

语法为:

#define name( parameter-list ) stuff

注意参数列表的括号必须要与name之间没有空格,不然参数列表就被识别为右边的内容了。

 

例如我们现在实现一个宏来实现乘法:

#define SQUARE( x ) x * x

可这样能表达我们想要的意思吗???

我们要注意宏仅仅是替换,所以在打印下面的情况下,就会出错:

	int a = 5;
	printf("%d\n", SQUARE(a + 1));//结果是11

我们想表达的是(5+1)*(5+1),而结果却是11,这就是因为宏仅仅是替换,只是替换为了a+1*a+1,所以我们加上扩号就能达到预期的效果了。

#define SQUARE(x) (x)*(x)

可这就完了吗?来看下面的代码,我们使用宏了实现一个相加:

#define DOUBLE(x) (x) + (x)

看着我们好像加上了括号好像就不会出现问题了,但是如果是下面的场景:

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));//结果是55,而我们预期的是100

这是因为预处理阶段,宏被替换成了10*(5)+(5),所以我们还需要再加上一个括号:
 

1 #define DOUBLE( x) ( ( x ) + ( x ) )

所以我们得出结论:

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4. 带有副作用的宏参数

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);

这样带有自增自减的是具有副作用的,再给宏传参就出错了。

结果是 x=6,y=10,z=9(尽管最后执行的是z=(y++),后置++--改变的是自己,返回的是一个没有改变的值,所以这里z是9),这样的结果不仅对xyz进行了永久性的改变,而且比较是也不是预期的结果。

所以当宏参数在宏的定义时出现超过了一次(就像上面的案例),如果参数带有副作用,也就是自增自减类的,此时使用宏就可能出错。

 

5.  宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
#define string1 string
#define string "1abcd"

	printf("%s\n", string);
	printf("%s\n", string1);

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索,也就是说定义的符号不能是字符串常量。

6. 宏和函数的对比

宏通常用来执行简单的计算。比如在两个数中找出一个较大的时,写成下面的宏,更有优势:

#define MAX(a, b) ((a)>(b)?(a):(b))

为什么不用函数来实现呢?

1.

用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。
2.
更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是类型无关。

 但是相比函数,宏也有它的劣势:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

但其实宏也可以做到函数做不到的事情,例如宏的参数可以出现类型,但是函数做不到:

#define MALLOC(num, type)\
 (type*)malloc(num*sizeof(type))

//使⽤
 MALLOC(10, int);//类型作为参数
//预处理器替换之后:
 (int*)malloc(10*sizeof(int));

宏和函数的对比:

 

7. #和##

#运算符是将宏的一个参数转换为字符串字面量,它运行出现在带参数的宏的替换列表中。

#define PRINT(n) printf("the value of "#n " is %d", n);

 如果定义有一个a为10,现在使用宏传a过去,就打印出来the value of a is 10 ,注意这里#n要加上一个引号,不然就被识别为printf打印的内容了,就直接打印出来一个#n,而不是预处理得到的了。 就理解为如果#n要在printf里识别为字符串,就要加上""。而直接这样使用就不用:

#define PRINT(n) #n

int a=10;
printf("%s\n",PRINT(a));//结果是a

##运算符

//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}


GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名


int main()
{
 //调⽤函数
 int m = int_max(2, 3);
 printf("%d\n", m);
 float fm = float_max(3.5f, 4.5f);
 printf("%f\n", fm);
 return 0;
}

 实际用的很少,不再过多解释。

8.  命名约定

一般来说函数和宏的使用语法是很相似的,所以语言本身没法帮我们区分二者。

所以命名习惯就是:

宏名全部大写,函数名不要全部大写。

9.  #undef

用于移除一个宏定义。

#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10.  命令行定义

许多C 的编译器提供了⼀种能力,
允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了⼀个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
#include <stdio.h>
int main()
{
 int array [ARRAY_SIZE];
 int i = 0;
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 array[i] = i;
 }
 for(i = 0; i< ARRAY_SIZE; i ++)
 {
 printf("%d " ,array[i]);
 }
 printf("\n" );
 return 0;
}
//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

11.  条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译:
#include <stdio.h>
#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;
}

如果我们不定义__DEBUG__这个宏,就什么也不会输出。

常见的条件编译指令:

1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#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

12.  头文件的包含

首先,查找策略:

先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在
标准位置(标准路径)查找头⽂件。如果找不到就提⽰编译错误。

linux下标准头文件的路径:

/usr/include

windows下(按照自己的安装路径去找):

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

然后就是库文件的包含:

1 #include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

当然库文件也可以使用""的形式包含,答案是可以的,但是这样不容易区分是库文件还是本地文件,查找的效率会低(尖括号是到系统路径下查找头文件,双引号是先在当前目录下查找头文件,如果没有找到,就再到系统的路径下去找)。 

嵌套文件的包含:

#include的替换方式就是预处理器先删除这条指令,并用包含文件的内容替换,但如果一个头文件被包含了10次,那实际就被编译了10次,如果重复包含,对编译的压力就会比较大,因为.h的文件会将内容拷贝10分到.c文件中去,这样预处理后代码量就会激增。如果有工程比较大,用公共使用的头文件,被大家都能使用,又不做处理,工程量会非常大。

如何解决?使用条件编译:

可以在每个头文件中的开头写:

#ifndef __TEST_H__
#define __TEST_H__

//头⽂件的内容

#endif //__TEST_H__,写在头文件结尾

也就是如果.h文件第一次被包含,此时__TEST_H__还没有被定义,所以执行#ifdef与#endif之间的代码,再次包含时,条件为假,因为以及有__TEST_H__的定义了,所以直接走到#endif,也就是什么也不执行。 

或者

 

#pragma once

13.  其他预处理指令

#error
#pragma
#line
......

具体可以用到的时候再查。

注:
推荐《⾼质量C/C++编程指南》中附录的考试试卷(很重要)。
笔试题:
1. 头⽂件中的 ifndef/define/endif是⼲什么⽤的?
2. #include <filename.h> 和 #include "filename.h" 有什么区别?

总结:

对于一些预处理阶段的一些不常用的可以选择性学习,但要知道,重点关注面试题相关的。

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

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

相关文章

最新高自定义化的AI翻译(沉浸式翻译),可翻译网页和PDF等文件或者文献(附翻译API总结,Deeplx的api,Deepl的api)

前序 常见问题&#xff1a; 1.有时候想翻译网页&#xff0c;又翻译文献怎么办&#xff1f;下两个软件&#xff1f; 2.什么软件可以翻译视频字幕&#xff1f; 3.什么软件可以翻译PDF文件&#xff1f; 沉浸式翻译介绍 可以翻译文献可以翻译视频字幕可以翻译PDF文件支持OpenAI翻译…

7 个 iMessage 恢复应用程序/软件可轻松恢复文本

由于误操作、iOS 升级中断、越狱失败、设备损坏等原因&#xff0c;您可能会丢失 iPhone/iPad 上的 iMessages。意外删除很大程度上增加了这种可能性。更糟糕的是&#xff0c;这种情况经常发生在 iDevice 缺乏备份的情况下。 &#xff08;iPhone消息消失还占用空间&#xff1f;&…

很多名人让人们警惕人工智能,这是为何?

很多名人让人们警惕人工智能的原因可以从多个角度来理解。首先&#xff0c;人工智能作为一个快速发展的领域&#xff0c;具有巨大的潜力和未知性。它涉及到机器学习、深度学习、神经网络等多个复杂的技术领域&#xff0c;而这些技术正日益渗透到我们的日常生活中&#xff0c;从…

JWT/JWS/JWE

JWT(JSON Web Token)&#xff1a;一种基于JSON格式&#xff0c;用于在Web应用中安全传递用户身份验证和授权信息的标准令牌&#xff0c;可以包含签名(JWS)和加密(JWE)的信息 MacAlgorithm(Message Authentication Code Algorithm)&#xff1a;消息认证码算法 HMAC(Hash-based…

【51单片机入门记录】RTC(实时时钟)-DS1302应用

目录 一、DS1302相关写函数 &#xff08;1&#xff09;Write&#xff3f;Ds1302 &#xff08;2&#xff09;Write&#xff3f;Ds1302&#xff3f;Byte 二、DS130相关数据操作流程及相关代码 &#xff08;1&#xff09;DS1302初始化数据操作流程及相关代码 (shijian[i]/10&…

阿里云4核8G服务器ECS通用算力型u1实例优惠价格

阿里云4核8G服务器优惠价格955元一年&#xff0c;配置为ECS通用算力型u1实例&#xff08;ecs.u1-c1m2.xlarge&#xff09;4核8G配置、1M到3M带宽可选、ESSD Entry系统盘20G到40G可选&#xff0c;CPU采用Intel(R) Xeon(R) Platinum处理器&#xff0c;阿里云活动链接 aliyunfuwuq…

2023年度总结:允许迷茫,破除迷茫;专注自身,把握当下

0、前言 &#x1f4dc;为什么24年已经过了几个月&#xff0c;才提笔写这年度总结呢&#xff1f;毫不羞愧直问我的内心&#xff0c;其实就是懒罢了。直到前几天朋友看到了我去年写的总结&#xff0c;我自己点进那篇总结&#xff0c;完完整整的看了一遍&#xff0c;又翻看我23年…

C#使用Selenium驱动Chrome浏览器

1.Selenium库依赖安装 Selenium WebDriver是Selenium项目的一部分&#xff0c;用于模拟用户在Web应用程序中的交互操作。它支持多种浏览器&#xff0c;如Chrome、Firefox、IE等&#xff0c;且与各种编程语言&#xff08;如Java、Python、C#等&#xff09;兼容&#xff0c;具有…

探索Linux的挂载操作

在Linux这个强大的操作系统中&#xff0c;挂载操作是一个基本而重要的概念。它涉及到文件系统、设备和数据访问&#xff0c;对于理解Linux的工作方式至关重要。那么&#xff0c;挂载操作究竟是什么&#xff0c;为什么我们需要它&#xff0c;如果没有它&#xff0c;我们将面临什…

SQL注入---POST注入

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一. POST提交概述 在Webshell文章中介绍过post提交和get提交的区别&#xff0c;这里不再赘述 post提交和get提交的区别&#xff1a; get方式提交URL中的参数信息&#xff0c;post方式则是将信…

【Frida】【Android】 工具篇:查壳工具大赏

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

【最大值线段树】【二分查找】2286. 以组为单位订音乐会的门票

本文涉及知识点 线段树 最大值线段树 二分查找算法合集 LeetCode2286. 以组为单位订音乐会的门票 一个音乐会总共有 n 排座位&#xff0c;编号从 0 到 n - 1 &#xff0c;每一排有 m 个座椅&#xff0c;编号为 0 到 m - 1 。你需要设计一个买票系统&#xff0c;针对以下情况…

简约风个人导航页源码

个人导航页源码&#xff0c;可以用作网站地址发布页&#xff0c;记事本修改html文件里的内容即可 源码下载 简约风个人导航页源码

总结:C/C++中程序内存区域划分

C/C程序内存分配的几个区域&#xff1a; 栈区&#xff08;stack&#xff09;&#xff1a;在执⾏函数时&#xff0c;函数内局部变量的存储单元都可以在栈上创建&#xff0c;函数执行结束时 这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中&#xff0c;效率很⾼&…

2. Django配置信息

第2章 Django配置信息 Django的配置文件settings.py用于配置整个网站的环境和功能, 核心配置必须有项目路径, 密钥配置, 域名访问权限, App列表, 中间件, 资源文件, 模板配置, 数据库的连接方式.* 项目运行时, 如果修改代码, 项目会自动检测发现改动后会重新运行, 除非报错否…

postgresql数据库|数据整合的好工具--Oracle-fdw的部署和使用

概述 Oracle_fdw 是一种postgresql外部表插件&#xff0c;可以读取到Oracle上面的数据。是一种非常方便且常见的pg与Oracle的同步数据的方法 Oracle_fdw 适用场景&#xff1a; Oracle_fdw 是一个开源的 Foreign Data Wrapper (FDW)&#xff0c;主要用于在 PostgreSQL 数据库中…

最新版手机软件App下载排行网站源码/App应用商店源码

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 一款简洁蓝色的手机软件应用app下载排行&#xff0c;app下载平台&#xff0c;最新手机app发布网站响应式织梦模板。 主要有&#xff1a;主页、app列表页、app介绍详情页、新闻资讯列…

软考高级架构师:嵌入式数据库概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

33. UE5 RPG使用增强输入激活GameplayAbility(三)

在前面的文章&#xff0c;我们实现了使用GameplayTag和InputAction的对应绑定的数据&#xff0c;并且添加到了增强输入映射的上下文中&#xff0c;实现了通过按键打印对应的GameplayTag&#xff0c;这只是我们基础需要制作的。目的主要是为了实现在GameplayAblity上面设置对应的…

物联网系统设计 8

1 规划中小型LoRa 中小型分时复用&#xff0c;大型项目需要学习LoRaWAN 1.1 通信记录 1.2 节点能耗 1278芯片 满功率20DMB&#xff0c;增加PA&#xff0c;发送功率 30 DBM 内置天线柔性 棒状 3db 203 休眠、发生、接收 计算链路预算&#xff0c;工作电流&#xff0c;工…