C/C++预处理过程

目录

前言:

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" 有什么区别?

总结:

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

IP地址:是给主机配置的,还是给网卡配置的?

在探索网络的奥秘时&#xff0c;我们经常会遇到一个看似简单但又复杂的问题&#xff1a;IP地址到底是配置在主机上&#xff0c;还是配置在网卡上&#xff1f;为什么我们通常说的是“主机IP地址”呢&#xff1f;让我们一起深入探讨。 1. 网卡与IP地址 &#x1f5a5;️&#x1f…

有同学和我说,深度学习不用特征工程,只有浅层机器学习方法采用特征工程,我说你误会了,我给你好好解释吧!!

1. 通俗解释 浅层机器学习算法&#xff08;如逻辑回归、决策树、支持向量机等&#xff09;和深度学习算法&#xff08;如神经网络&#xff09;在特征工程上的依赖性确实存在一些差异。 浅层机器学习算法的特征工程依赖性&#xff1a; 浅层算法通常需要手工选择和设计特征&…

lua学习笔记5(分支结构和循环的学习)

print("*****************分支结构和循环的学习******************") print("*****************if else语句******************") --if 条件 then end a660 b670 --单分支 if a<b thenprint(a) end --双分支 if a>b thenprint("满足条件")…

RGB三通道和灰度值的理解

本文都是来自于chatGPT的回答!!! 目录 Q1:像素具有什么属性?Q2:图像的色彩是怎么实现的?Q3:灰度值和颜色值是一个概念吗?Q4:是不是像素具有灰度值&#xff0c;也有三个颜色分量RGB&#xff1f;Q5:灰度图像是没有色彩的吗&#xff1f;Q6: 彩色图像是既具有灰度值也具有RGB三…

11_printf函数移植串口通信

printf函数移植串口通信 printf函数移植串口通信串口显示汉字乱码问题解决代码 printf函数移植串口通信 MicroLIB是Keil为嵌入式平台优化的一个精简库 --no-multibyte-chars 串口显示汉字乱码问题解决 法一 法二 代码 主函数 #include "stm32f10x.h" …

[StartingPoint][Tier1]Sequel

Task 1 During our scan, which port do we find serving MySQL? (在扫描过程中&#xff0c;我们发现哪个端口为 MySQL 提供服务&#xff1f;) 3306 Task 2 What community-developed MySQL version is the target running? (目标正在运行哪个社区开发的 MySQL 版本&…

详解 Redis 在 Centos 系统上的安装

详解 Redis 在 Centos 系统上的安装 1. 使用 yum 安装 Redis 5 如果是Centos8&#xff0c;yum 仓库中默认的 redis 版本就是5&#xff0c;直接 yum install 即可 如果是Centos7, yum 仓库中默认的 redis 版本是3系列&#xff0c;版本就比较老 使用yum list | grep redis命令…

【问题解决】电脑突然 总蓝屏,这份火爆全网的452页Linux运维 Framework内核解析

“你的设备遇到问题&#xff0c;需要重启。我们智手机某些错误信息&#xff0c;然后为你重新启动。” 电脑突然就蓝屏了&#xff0c;终止代码显示&#xff1a;UNEXPECTED_STORE_EXCEPTION。 原因分析&解决方案 可能性原因解决方案1出现蓝屏的情况&#xff0c;大部分原因都…

【C++ STL容器适配器】queue 队列

文章目录 【 1. 基本原理 】【 1. queue 的创建 】2.1 使用默认的 deque 基础容器创建一个空的 queue2.2 指定基础容器创建 queue2.3 通过基础容器来初始化 queue 容器适配器2.4 通过一个 queue 初始化另一个 queue 【 3. queue 支持的成员函数 】 【 1. 基本原理 】 STL queu…

ChatGPT(3.5版本)开放无需注册:算力背后的数据之战悄然打响

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

AI Agents产品图谱+网站合集

这个网站收集了市面受欢迎的项目&#xff0c;包括开源项目和闭源项目以及公司 地址&#xff1a;通过浏览列表中的AI代理项目和公司&#xff0c;社区里的创业者可以了解当前市场上的主要玩家和他们的产品特点&#xff0c;进行市场趋势分析和竞争分析。

2024妈妈杯数学建模思路ABCD题思路汇总分析 MathorCup建模思路分享

1 赛题思路 (赛题出来以后第一时间在群内分享&#xff0c;点击下方群名片即可加群) 2 比赛日期和时间 报名截止时间&#xff1a;2024年4月11日&#xff08;周四&#xff09;12:00 比赛开始时间&#xff1a;2024年4月12日&#xff08;周五&#xff09;8:00 比赛结束时间&…

看书学习各类编程效率高吗?

最近给大家争取到一个 深夜福利 保证你在深夜手机刷到 嘎嘎香~ 那就是 官方授权 大流量卡 缺点&#xff1a;月租太便宜 185GB~ 100分钟通话时长~ 长期套餐~ 畅想自由的气息 流量自由的同时还拥有超长通话&#xff0c;而且免费领取。 名额有限&#xff0c;咱们废话不…

青蛙跳杯子【蓝桥杯】/bfs

青蛙跳杯子 bfs 思路&#xff1a;刚开始用的是dfs&#xff0c;但是不太行&#xff0c;DFS 可能会导致搜索深度过深&#xff0c;增加了时间复杂度&#xff0c;BFS 适合求解最短路径问题&#xff0c;BFS 在搜索过程中&#xff0c;首先访问距离初始节点最近的节点&#xff0c;因此…

欧拉函数确定1-n有多少个数和 n 互质详解 附C语言代码 蓝桥杯互质数的个数

唯一分解定理 任意一个大于 1 的正整数都能被唯一地分解为质因数的乘积。 例如 8 2*2*2&#xff0c; 171 3*3*19&#xff0c; 30 2*3*5&#xff0c; 19 19。注意1既不是质数也不是合数。 为什么判断一个数是否是质数只要判断2-√n中有没有因数 24可以分解成 4*6&#x…

时序预测 | Python实现BiGRU-RELM时间序列预测

时序预测 | Python实现BiGRU-RELM时间序列预测 目录 时序预测 | Python实现BiGRU-RELM时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 BiGRU-RELM时间序列预测分析 将BiGRU和RELM两种模型进行了融合&#xff0c;BiGRU进行预测&#xff0c;RELM对BiGRU模型的预…

算法刷题Day24 | 216.组合总和III、17.电话号码的字母组合

目录 0 引言1 组合总和 III1.1 我的解题 2 电话号码的字母组合2.1 我的解题2.2 优秀的题解 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;算法刷题Day24 | 216.组合总和III、17.电话号码的字母组合❣️ 寄…

Linux基础篇:Linux第三方软件仓库——可以让Linux变得有趣的软件仓库

Linux第三方软件仓库——可以让Linux变得有趣的软件仓库 一、epel源介绍 EPEL&#xff08;Extra Packages for Enterprise Linux&#xff09;源是一个由Fedora项目组维护的第三方软件仓库&#xff0c;为企业级Linux发行版&#xff08;如Red Hat Enterprise Linux&#xff08;…

opencv+python(通道的分离与合并)笔记

分割图像通道&#xff1a; 通过函数mvsplit(img)&#xff1b;mv返回的通道&#xff1b; RGB有3个通道&#xff1b;灰度图只有一个通道&#xff1b; b,g,r cv2.split(img)cv2.imshow("b",b)#通道bcv2.imshow("g",g)#通道gcv2.imshow("r",r)#通道…

Flutter 应用数据持久化指南

1. 介绍 1.1 什么是数据持久化&#xff1f; 数据持久化是指将应用程序中的数据保存在持久存储介质&#xff08;如硬盘、数据库等&#xff09;中的过程。在计算机科学领域&#xff0c;持久化数据是指数据在程序退出或系统关机后仍然存在的能力。这种持久性使得数据可以在不同的…