【C语言】预处理指令详解

目录

一、预定义符号

二、#define 定义常量

三、#define 定义宏

(1)宏定义的使用

(2)带副作用的宏参数

(3)宏替换的规则

(4)宏与函数对比

(5)#和##

① #运算符

② ##运算符

(6)宏的命名规则

(7)#undef

四、命令行定义

五、条件编译

(1)条件编译的使用

(2)常见的条件编译

① 基础的条件编译

② 多个分支的条件编译

③ 判断是否被定义

④ 嵌套指令

六、头文件的包含

(1)头文件被包含的方式

① 本地文件包含

② 库文件包含

(2)嵌套文件包含

① 嵌套文件包含的概念

② 嵌套文件包含的解决方法

七、其它预处理指令


一、预定义符号

        预定义符号,会在预处理阶段,被直接替换为它的内容。

        预定义符号有:

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

        在 VS 环境中演示,预定义符号__STDC__不可使用:

        用 gcc 编译器演示,gcc 遵循标准C:

        执行命令 gcc -E test.c -o test.i(进行预处理),打开 test.i:

二、#define 定义常量

        #define 定义常量,会在预处理阶段,将代码中的名字直接替换为内容。

        语法形式:

#define name stuff
// name: 名字
// stuff: 内容
// 举例:
#define M 100

        在其它场景下的用法:

#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ ) 

        示例代码:

        预处理后的 test.i 文件:

        有些语言的 switch 语句没有 break,使用这些语言的程序员再使用 C 语言就非常不习惯,老忘记加 break,像示例代码一样使用 #define 定义,编码时就不用写 break 了。

        注意:#define 定义标识符,最后不加 ;

        如下情况,#define 定义常量加了 ; ,发生错误:

        预处理阶段,第 52 行被替换成 max = 100;;,表示两条语句。因为 if 语句没加{},if 只跟 一条语句,所以发生了错误。

三、#define 定义宏

(1)宏定义的使用

        #define 定义宏,在预处理阶段,将代码中的 名字(参数),替换为宏的内容,并把参数带入内容中。

        语法形式:

#define name( parament-list ) stuff
// name: 名字
// parament-list: 参数列表,由逗号隔开
// stuff: 内容

        注意:name后应紧跟(,如果之间加了空格,会被认为 ( parament-list ) stuff 是 stuff, 属于#define 定义标识符。

        示例代码1:

        预处理后的结果:

        将参数改为 x+1(宏的参数是直接替换,而不计算):

        预处理后的结果:

        改进代码(为了防止替换后,因操作符优先级等,导致运算顺序不是预料的结果,应尽量加小括号):

        预处理后的结果:

        示例代码2:

        改进代码:

(2)带副作用的宏参数

        若宏参数带有副作用,并且在宏定义中同一个宏参数不止出现一次,那么这个宏可能会出现不可预料的结果。

        赋值符号的右边是示例宏参数:

y = x+1;//执行后,对x不改变,不带副作用
y = x++;//执行后,对x改变,带有副作用

        示例代码,期望获得 X、Y 两者最大值,但出现问题:

        而定义函数,传入带副作用的参数,却不会出现问题:

        结论:应避免使用带副作用的宏参数。

(3)宏替换的规则

  • 调用宏时,首先对宏参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

        例如:

  • 再次对结果文件进行扫描,看它是否包含任何由#define定义的符号。如果是,重复上述处理过程。

        例如:

        注意

  • 宏定义不能出现递归。

        例如下面的错误示范:

        预处理后的代码:

        因为宏只进行一次替换,如果宏定义存在递归,那么替换不完全,会把剩下的宏认为是函数,但这个函数又没被定义,所以出现链接错误。

  • 预处理器搜索#define定义的符号的时,字符串常量的内容并不被搜索。

        例如:

(4)宏与函数对比

        执行简单的计算时,宏比函数更有优势

        相比函数,宏的优势:

  • 函数,需要函数调用、执行计算、函数返回;宏只需要直接执行计算;因此,比函数在程序规模和计算速度方面,更优
  • 函数的参数,必须声明特定的类型;宏的参数,类型无关。因此,宏比函数更灵活

        例如:计算两个数中的较大值。

        函数的实现:

int Max(int x, int y)
{return x > y ? x : y;
}int main()
{int a = 7, b = 5;int m = Max(a, b);printf("m = %d\n", m);return 0;
}

        宏定义实现:

#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))int main()
{int a = 7, b = 5;float m = MAX(a, b);//int m = ((a)>(b)?(a):(b));printf("m = %f\n", m);return 0;
}

        调试,查看函数实现的汇编代码:

        ① 调用函数,执行了19条指令。

        ② 计算,执行了 9 条指令。

        ③ 返回函数,执行了10条指令。

        调试,查看宏定义实现的汇编代码:

        计算,执行了9条指令。

        结论:因为函数的实现方法,需要创建函数栈帧,所以多了调用函数、返回函数的指令,在代码规模和计算速度上,明显比宏的实现方法差。

        宏参数没有类型的限制,也可以传入浮点数:

        相比函数,宏的劣势:

  • 每使用一次宏,都会插入一段宏定义的代码,除非宏很短,否则会大幅增加程序的长度(宏在预处理阶段,会直接被替换成一大段宏的内容;而每次调用的函数,代码只需定义一次)。
  • 无法调试(在预处理阶段,宏就已经被替换掉,而调试是在 .exe 文件生成后执行的操作)。
  • 因为宏类型无关,所以不够严谨
  • 可能产生操作符优先级的问题,导致程序运行结果出乎意料(宏参数为表达式、宏参数带有副作用的情况)。

        宏有时可以做函数做不到的事,比如宏参数可以是类型,但函数做不到。如下面的代码,只需要给宏传入元素个数、元素类型,就能实现动态开辟空间:

#define MALLOC(N, Type) (Type*)malloc(N * sizeof(Type))int main()
{//int* p = (int*)malloc(10 * sizeof(int));int* p = MALLOC(10, int);return 0;
}

        总结宏和函数的对比:

属性#define定义宏函数
代码长度每次使用宏,宏内容被替换到程序中,程序长度会大幅增加。每次使用函数,都调用同一份函数定义的代码。(胜)
执行速度更快。(胜)会有调用函数、返回函数的额外开销。
操作符优先级宏参数求值,在宏内容表达式的上下文环境里,容易产生邻近操作符优先级的问题,导致计算结果不可预料。要多使用圆括号。函数参数求值,只在函数调用时求一次,传给函数,计算结果容易预测。(胜)
带有副作用的参数带有副作用的参数,可能会被替换到宏内容表达式的多个位置,多次求值,产生不可预料的结果。带有副作用的参数,只在函数调用时求值一次,结果容易预测。(胜)
参数类型宏的参数类型无关,更灵活。(胜)函数的参数定义了特定类型。
调试不能调试。可调试。(胜)
递归不能递归。可递归。(胜)

        执行简单计算,使用宏;执行复杂计算,使用函数。当计算复杂时,计算的花销远大于调用函数、返回函数的花销,可以忽略不计。复杂的计算,使用函数,更不易出错。

        在C++中引入了内联函数(inline),它既具备了函数的优势,又具备了宏的优势。

(5)#和##

① #运算符

        作用:在宏定义的内容表达式中使用,可将宏参数转化为字符串

        先了解一个没见过的知识:

        示例,我们想打印3句话,但是代码很重复:

        红框是3句打印不同的部分,将它们作为宏参数,使用宏(由于预处理器不搜索程序中的字符串常量,所以红框中的v并没有被替换):

        此时,在宏定义的表达式中,使用#操作符,将传入的参数转为字符串:

② ##运算符

        作用:在宏定义的内容表达式中使用,可将两个参数合成一个标识符(应是合法的标识符)。这被称为记号粘合。

        示例,使用函数实现计算两个参数的较大值,但参数类型不同,函数的实现也不同,这样的代码很重复(红框中是不同的部分):

        使用宏定义和##,减少编程的繁琐:

        预处理后的结果:

(6)宏的命名规则

        使用宏和函数的语法非常相似,可以从命名规则角度区分它们。

  • 宏名全部大写(如:MAX)。
  • 函数名不要全部大写(如:Max)。

        但这只是一个习惯,并不是定死的,C标准中也有宏定义是小写命名的:

(7)#undef

        作用:移除一个#define定义

        示例:

四、命令行定义

        一些C编译器,允许命令行定义符号。比如,我们有时想要用一个源文件,编译出不同版本程序。

        示例,在程序中声明了 SIZE 长度的数组。在内存有限的机器上,我们想要很小的数组;在内存较大的机器上,我们想要较大的数组。这可以通过命令行定义(VS不支持,gcc 支持)实现,源代码如下:

#include <stdio.h>
int main()
{int array[SIZE];int i = 0;for (i = 0; i < SIZE; i++){array[i] = i;}for (i = 0; i < SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}

        使用如下命令,定义 SIZE:

五、条件编译

(1)条件编译的使用

        作用:选择一组语句编译或者不编译

        示例,调试性的代码,不想执行调试,但想保留代码,使用条件编译:

        因为定义了 __DEBUG__,所以会编译 printf 语句,在预处理阶段,将 printf 语句保留了下来:

        如果不想编译 printf 语句,就注释掉 __DEBUG__ 的定义:

        预处理阶段,去掉了 printf 语句:

(2)常见的条件编译

① 基础的条件编译

        语法形式:

#if 常量表达式//...
#endif

        示例(预处理阶段,M被替换成5,常量表达式 5==1 为假,不编译 printf 语句):

#define M 5int main()
{#if M==1printf("hehe\n");
#endifreturn 0;
}

② 多个分支的条件编译

        语法形式:

#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif

        示例(最终编译 printf("哈哈\n");):

#define M 5int main()
{#if M==1printf("hehe\n");
#elif M==2printf("haha\n");
#elif M==3printf("heihei\n");
#else	printf("哈哈\n");
#endifreturn 0;
}

③ 判断是否被定义

        语法形式1:

// 判断 symbol 是否被定义
#if defined(symbol)
// ...
#ifdef symbol// 判断 symbol 是否没被定义
#if !defined(symbol)
// ...
#ifndef symbol

        语法形式2:

// 判断 symbol 是否被定义
#ifdef symbol
// ...
//#endif// 判断 symbol 是否没被定义
#ifndef symbol
// ...
#endif

        示例代码(最终编译 printf("1hehe\n"); 和 printf("3hehe\n");):

#define Mint main()
{
//判断M是否被定义过,关于值是多少,不关心
// 语法形式1
#if defined(M)printf("1hehe\n");
#endif#if !defined(M)printf("2hehe\n");
#endif// 语法形式2
#ifdef Mprintf("3hehe\n");
#endif#ifndef Mprintf("4hehe\n");
#endifreturn 0;
}

④ 嵌套指令

if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

        很少用到,只有在很大的项目中常用,比如打开 stdio.h 头文件看,就是用了很多条件编译指令(因为代码是跨平台的,根据不同的平台,有不同的代码):

六、头文件的包含

(1)头文件被包含的方式

① 本地文件包含

        语法形式:

#include "filename"

        查找策略:先在源文件所在目录下找,如果没有,再在标准位置找,如果找不到就提示编译错误。

        比如我的项目下的文件目录:

                                

        test.c 中包含了本地头文件 add.h,故先在 test.c 所在目录下找:

        Linux 环境的标准头文件路径:

        VS2013 环境默认的标准头文件路径:

// 根据自己实际的安装路径找
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

② 库文件包含

        语法形式:

#include <filename.h>

        查找策略:直接到标准路径下找,找不到就提示编译错误。

        库文件包含也可以用 " ",但是这种方式会先查找源文件目录,再查找标准路径,会降低查找效率,也不易区分是本地文件还是库文件

(2)嵌套文件包含

① 嵌套文件包含的概念

        在预处理阶段,会把头文件包含的所有内容,替换到使用 #include 的文件中。如果同一个头文件被重复包含多次,在预处理后的文件中就会有许多重复的代码,使编译效率大大降低

        比如,A、B、C 3个文件都包含了头文件 add.h,文件 D 又对A、B、C 进行整合,相当于 D 重复包含了 3 次 add.h 头文件。对于大型工程,会包含3~5万个文件,如果重复包含头文件,而不作处理,其预处理后的文件中重复代码之多,后果不堪设想。

        示例,test.c 重复包含头文件 add.h:

// test.c 中的内容
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"int main()
{return 0;
}// add.h 中的内容
int Add(int x, int y);

       test.c 预处理后的内容:

② 嵌套文件包含的解决方法

        方法1:在头文件 add.h 中,使用条件编译

#ifndef __ADD_H__
#define __ADD_H__
//头⽂件的内容
#endif

        在源代码 test.c 中,第一次包含头文件 add.h,符号 __ADD_H__ 未被定义,编译 #define __ADD_H__ 和头文件的内容。test.c 后面再包含头文件 add.h,因为已经定义过 __ADD_H__,不再编译 #define __ADD_H__ 和头文件的内容。

        示范:

// 更改后,add.h 中的内容
#ifndef __ADD_H__
#define __ADD_H__
int Add(int x, int y);
#endif

        test.c 预处理后的内容:

        方法2:在头文件中加以下内容(在 VS 中创建新的头文件,会自动包含这条语句)

#pragma once

        注:在《高质量C/C++编程指南》中附录的考试试卷,就包含头文件包含相关笔试题目。

七、其它预处理指令

#error
#pragma
#line
...
更多参考《C语言深度解剖》
  • #pragma pack() 参考:【C语言】自定义类型——结构体-CSDN博客 修改默认对齐数部分。
  • #pragma comment() 参考:【C语言】函数-CSDN博客 多个文件,导入静态库部分。

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

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

相关文章

基于单片机的书库环境监测

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机&#xff0c;采用DHT11湿度传感器检测湿度&#xff0c;DS18B20温度传感器检测温度&#xff0c; 采用滑动变阻器连接数模转换器模拟二氧化碳和氧气浓度检测&#xff0c;各项数值通过lc…

SQL第12课——联结表

三点&#xff1a;什么是联结&#xff1f;为什么使用联结&#xff1f;如何编写使用联结的select语句 12.1 联结 SQL最强大的功能之一就是能在数据查询的执行中联结&#xff08;join)表。联结是利用SQL的select能执行的最重要的操作。 在使用联结前&#xff0c;需要了解关系表…

免费高可用软件

高可用软件是指那些能够提供高可用性、高可靠性的软件&#xff0c;它们在各种应用场景下都能确保系统的稳定运行。以下是四款免费的高可用软件&#xff0c;它们在不同领域都表现出色&#xff0c;能够满足各种高可用性需求。 一、PanguHA PanguHA是一款专为Windows平台设计的双…

使用正则表达式删除文本的奇数行或者偶数行

用智谱清言和kimi搜出来的结果都没法在notepad生效&#xff0c;后面在overflow上找到的答案比较靠谱。 查找&#xff1a;^[^\n]*\n([^\n]*) 替换&#xff1a;\1 删除偶数行 查找&#xff1a;^([^\n]*)\n[^\n]* 替换&#xff1a;\1 代码解释 ^&#xff1a;这个符号代表字符…

RabbitMQ 集群

文章目录 集群搭建使用 Docker-Compose 镜像队列搭建步骤工作原理镜像策略主从同步 同步延迟 集群搭建 参考&#xff1a; docker中安装并启动rabbitMQ Docker中搭建RabbitMQ集群 使用 Docker-Compose 这里提供一个脚本来使用 docker-compose 完成RabbitMQ集群的配置及启动…

机器学习-树模型算法

机器学习-树模型算法 一、Bagging1.1 RF1.2 ET 二、Boosting2.1 GBDT2.2 XGB2.3 LGBM 仅个人笔记使用&#xff0c;感谢点赞关注 一、Bagging 1.1 RF 1.2 ET 二、Boosting 2.1 GBDT 2.2 XGB 2.3 LGBM LightGBM&#xff08;Light Gradient Boosting Machine) 基本算法原理…

基于单片机的烧水壶系统设计

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52RC单片机&#xff0c;采用四个按键&#xff0c;通过DS18B20检测温度&#xff0c;开机显示实时温度 第一个按键为切换功能按键&#xff0c;按下后&#xff0c;可以设置烧水温度的大小&…

五子棋双人对战项目(6)——对战模块(解读代码)

目录 一、约定前后端交互接口的参数 1、房间准备就绪 &#xff08;1&#xff09;配置 websocket 连接路径 &#xff08;2&#xff09;构造 游戏就绪 的 响应对象 2、“落子” 的请求和响应 &#xff08;1&#xff09;“落子” 请求对象 &#xff08;2&#xff09;“落子…

【Git】vscode链接github拉去镜像

1.拉取别人的项目到自己的仓库 2.回到自己的仓库拉取文件到vscode里面下载 使用vscode进入虚拟机 推送到自己的仓库上面 在 github 页面将修改的内容 PR 到 Tutorial 创建一个个人仓库 代码如下 cd demo git clone https://github.com/3154067760/Tutorial.git cd Tutorial/…

UGUI(三大现成UI控件)

Rawimage 可以是任意类型的图&#xff0c;所以这里的泛型就更宽泛&#xff0c;不止sprite 相比Image唯二的不同 uvrect有点像平铺 Text suddenly come to a Free island. best fit开启后会有范围选择 Image image 组件是挂在RectTransform的ui下的&#xff0c;换句话说&…

域名续签申请步骤

来此加密-申请3个月使用&#xff08;免费&#xff09; 附上链接&#x1f517; 免费申请SSL证书,支持泛域名和多域名: 来此加密. 使用推荐码注册:E69X5K4D, 立刻获得5个积分. 访问:https://letsencrypt.osfipin.com/jump/share?codeE69X5K4D 登陆网站 https://letsencrypt.…

浅谈新能源电动汽车充电站建设与运营模式分析

摘要&#xff1a;电动汽车是当前新能源汽车中重要的组成部分&#xff0c;具有广阔的发展前景&#xff0c;能够实现“以电代油”&#xff0c;与传统的燃油汽车相比&#xff0c;电动汽车在噪音及废气排放量方面相对较少&#xff0c;具有节能环保的显著特点。而电动汽车充电站则是…

强引用、软引用、弱引用、虚引用用法

强引用、软引用、弱引用、虚引用用法 强引用弱引用弱引用虚引用 强引用 强引用是指程序中在程序代码之中类似“Object obj new Object()”的引用关系&#xff0c;无论任何情况下&#xff0c;只要强引用关系还存在&#xff0c;垃圾回收器就不会回收掉被引用的对象。 强引用是我…

【黑马点评】使用RabbitMQ实现消息队列——3.使用Jmeter压力测试,导入批量token,测试异步秒杀下单

3 批量获取用户token&#xff0c;使用jmeter压力测试 3 批量获取用户token&#xff0c;使用jmeter压力测试3.1 需求3.2 实现3.2.1 环境配置3.2.2 修改登录接口UserController和实现类3.2.3 测试类 3.3 使用jmeter进行测试3.4 测试结果3.5 将用户登录逻辑修改回去 3 批量获取用户…

地图可视化的艺术:深入比较Mapbox、OpenLayers、Leaflet和Cesium,不同场景下应如何选择地图库

目录 地图可视化的艺术&#xff1a;深入比较Mapbox、OpenLayers、Leaflet和Cesium 一、总览 二、定制地图美学的先行者——Mapbox 1、主要功能特点 2、开源情况 3、市场与应用人群 4、安装与基础使用代码 三、开源GIS地图库的全能王——OpenLayers 1、主要功能特点 2…

rabbitmq消费者应答模式

1.应答模式 RabbitMQ 中的消息应答模式主要包括两种&#xff1a;自动应答&#xff08;Automatic Acknowledgement&#xff09;和手动应答&#xff08;Manual Acknowledgement&#xff09;。 自动应答&#xff1a; 不在乎消费者对消息处理是否成功&#xff0c;都会告诉队列删…

ComfyUI增强图像细节只需要一个节点(附工作流),SD1.5、SDXL、FLUX.1 全支持,简单好用!

今天给小伙伴们介绍一个非常简单&#xff0c;但又相当好使的一个插件。 功能很简单&#xff0c;就是增加或者减少图像的细节&#xff0c;节点也很简单&#xff0c;就一个节点&#xff0c;只需要嵌入我们的 ComfyUI 的基础工作流中就可以了&#xff0c;随插随用。 而且该插件不…

springboot mail:如何高效管理邮件服务?

springboot mail发邮件教程&#xff1f;怎么实现spring发信功能&#xff1f; SpringBoot Mail作为Spring Boot框架的一部分&#xff0c;提供了一种简单而强大的方式来集成和管理邮件服务。AokSend将探讨如何高效地使用SpringBoot Mail来管理邮件服务&#xff0c;确保邮件发送的…

Qt实现Halcon窗口显示当前图片坐标

一、前言 Halcon加载图片的窗口&#xff0c;不仅能放大和缩小图片&#xff0c;还可以按住Ctrl键显示鼠标下的灰度值&#xff0c;这种方式很方便我们分析缺陷的灰度和对比度。 二、实现方式 ① 创建显示坐标和灰度的widget窗口 下图的是widget部件&#xff0c;使用了4个label控…

二项式定理学习

1.二项式定理 这个就是二项式定理的重要公式&#xff0c;我们的二项式定理的每一项的系数&#xff0c;代表的意思为从n个里面选出k个 &#xff0c;以下是来自于百度百科上面的解释&#xff08;原谅我实在不会数学定义&#xff09; 因此我们可以去讨论二项式定理中的最特殊的一种…