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;
elsemax = 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) #nint 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 OPTION1unix_version_option1();
#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_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.hqwc.cn/news/597000.html

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

相关文章

JS详解-class-类的核心语法关于ES6与ES5

class基本核心语法&#xff1a; //定义类class Person{//公有属性(推荐此处定义)name// 定义属性并设置默认值price 100//构造函数constructor(name,age){// 构造函数内部的this实例化对象this.name namethis.age age// 动态添加属性(不推荐&#xff0c;查找麻烦)this.rank …

JavaScript-1(变量+数据类型+数据类型转换)

目录​​​​​​​ 1.计算机 编程语言 计算机基础 2.JS JS是什么 浏览器执行JS JS组成 ECMAScript DOM——文档对象模型 BOM——浏览器对象模型 JS写法 JS注释 JS输入输出语句 3.变量 变量的使用 变量使用注意点 变量小结 4.数据类型 变量的数据类型 基本…

Python免滑块验证脚本

说明 这是一个用于自动登录京东并更新Cookie到青龙管理系统的Python脚本。脚本支持通过账号密码自动登录,适用于需要定期更新京东Cookie的情况,特别是在使用青龙等自动化工具进行京东相关活动脚本运行时。 代码展示 全代码如下 # -*- coding: utf-8 -*- jdck.ini配置文件…

达托机器人(DRB)平台的安全性和前景是否可靠?

在当今数字化时代&#xff0c;技术创新不仅是企业成功的关键&#xff0c;也是整个行业的驱动力。在这个背景下&#xff0c;达托机器人&#xff08;DRB&#xff09;脱颖而出&#xff0c;以其创世团队的坚实基础和平台的可靠前景&#xff0c;引起了业界的广泛关注。 首先&#xf…

解锁金融数据中心场景,实现国产化AD替代,宁盾身份域管为信创电脑、应用提供统一管理

随着信创国产化改造持续推进&#xff0c;越来越多的金融机构不断采购信创服务器、PC、办公软件等&#xff0c;其 IT 基础设施逐渐迁移至国产化 IT 架构下。为支撑国产化 IT 基础设施的正常使用和集中管理运维&#xff0c;某金融机构数据中心的微软Active Directory&#xff08;…

专题【双指针】【学习题】刷题日记

题目列表 11. 盛最多水的容器 42. 接雨水 15. 三数之和 16. 最接近的三数之和 18. 四数之和 26. 删除有序数组中的重复项 27. 移除元素 75. 颜色分类 167. 两数之和 II - 输入有序数组 2024.04.06 11. 盛最多水的容器 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂…

信息论基础:生日问题

生日问题 我的小学同班同学小琦&#xff0c;和我是同一天生日。每次聚会&#xff0c;我俩同时在场时&#xff0c;我们都会向其他人提起这一点。我们觉得这是很幸运的。 这个概率大吗&#xff1f;要回答这样的问题&#xff0c;需要把几个问题区分清楚。 问题1&#xff1a;对于…

EXCEL地理数据处理工具(地图任务)

版本号 作者 修订内容 发布日期 1.0 小O 更新至0705版 2022-4-28 1.1 小O 更新至0772版 2024年4月3日 一、概述 小O地图EXCEL插件版提供基于EXCEL表格进行地理数据处理、地图可视化、地图绘图等功能&#xff0c;地理工具是用户使用频率很高的功能模块。地理工具能…

2024HW-->Wireshark攻击流量分析

在HW中&#xff0c;最离不开的&#xff0c;肯定是看监控了&#xff0c;那么就要去了解一些wireshark的基础用法以及攻击的流量&#xff01;&#xff01;&#xff01;&#xff01; 1.Wireshark的基本用法 比如人家面试官给你一段流量包&#xff0c;你要会用 1.分组详情 对于我…

构建未来数字化世界的统一用户中心产品架构

随着数字化时代的到来&#xff0c;用户数据管理变得愈发复杂&#xff0c;各类应用和服务的涌现使得用户信息分散存储&#xff0c;导致了数据孤岛和体验碎片化的问题。在这样的背景下&#xff0c;统一用户中心产品架构应运而生&#xff0c;为构建数字化世界提供了全新的解决方案…

Linux 常用指令及其理论知识

个人主页&#xff1a;仍有未知等待探索-CSDN博客 专题分栏&#xff1a;http://t.csdnimg.cn/Tvyou 欢迎各位指教&#xff01;&#xff01;&#xff01; 目录 一、理论知识 二、基础指令 1、ls指令&#xff08;列出该目录下的所有子目录和文件&#xff09; 语法&#xff1a; …

基于单片机电容介电常数测量显示系统设计

**单片机设计介绍&#xff0c;基于单片机电容介电常数测量显示系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机电容介电常数测量显示系统的设计&#xff0c;是一个集成了电子技术、单片机编程、电容测量以及显…