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

news2024/10/7 8:42:12

目录

一、预定义符号

二、#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 5

int main()
{

#if M==1
	printf("hehe\n");
#endif

	return 0;
}

② 多个分支的条件编译

        语法形式:

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

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

#define M 5

int main()
{

#if M==1
	printf("hehe\n");
#elif M==2
	printf("haha\n");
#elif M==3
	printf("heihei\n");
#else	
	printf("哈哈\n");
#endif

	return 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 M

int main()
{
//判断M是否被定义过,关于值是多少,不关心
// 语法形式1
#if defined(M)
	printf("1hehe\n");
#endif

#if !defined(M)
	printf("2hehe\n");
#endif

// 语法形式2
#ifdef M
	printf("3hehe\n");
#endif

#ifndef M
	printf("4hehe\n");
#endif
	
	return 0;
}

④ 嵌套指令

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

        很少用到,只有在很大的项目中常用,比如打开 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.coloradmin.cn/o/2193827.html

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

相关文章

Html批量转word工具2.1

2024年10月7日记录&#xff1a; 有客户反馈&#xff0c;2.0刚运行就提示转换完成 有问题就解决。正好国庆假期这几天有空&#xff0c;2.1版就出炉了。 2.1 更新记录&#xff1a; 修复了1个bug&#xff1a;刚运行就提示转换完成 下载地址&#xff1a;Html 转 word 批量处理工具…

基于Springboot+Vue的线上课堂系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统中…

分析CppCrash(进程崩溃)(一)

一、Cpp Crash异常检测能力 进程崩溃基于posix信号机制&#xff0c;目前主要支持对以下崩溃异常信号的处理&#xff1a; 信号值(signo)信号解释触发原因4SIGILL非法指令。进程执行了非法、格式错误、未知或特权指令。5SIGTRAP断点或陷阱异常。异常或trap指令发生。6SIGABRT进程…

谷歌发布了日语版的 Gemma2 模型——gemma-2-2b-jpn-it

Gemma 是一系列同类最佳的开放式模型&#xff0c;其灵感和技术源自 Gemini 系列模型。 它们是具有开放权重的文本到文本、纯解码器大型语言模型。 Gemma 模型非常适合各种文本生成任务&#xff0c;包括问题解答、摘要和推理。 Gemma-2-JPN 是一个针对日语文本进行微调的 Gemma…

读数据工程之道:设计和构建健壮的数据系统01数据工程概述

1. 数据工程 1.1. 自从公司开始使用数据做事&#xff0c;数据工程就以某种形式存在了 1.1.1. 预测性分析、描述性分析和报告 1.2. 数据工程师获取数据、存储数据&#xff0c;并准备数据供数据科学家、分析师和其他人使用 1.3. 数据工程是系统和流程的开发、实施和维护&…

No.0 笔记 | 从小白到入门:我的渗透测试笔记

嘿&#xff0c;小伙伴们&#xff01;好久不见啊&#xff0c;是不是都以为我失踪了&#xff1f;&#x1f602; 其实呢&#xff0c;最近一直在埋头苦学&#xff0c;感觉自己就像是在技术的海洋里游泳&#xff0c;每天都在吸收新知识。现在终于有时间冒个泡&#xff0c;跟大家分享…

如何通过 Alt 键打出所有特殊字符?

有时我们需要键入键盘上没有的字符&#xff0c;例如版权符号 ©&#xff0c;怎么办呢&#xff1f; 上一篇文章说过&#xff0c;可以用输入法自带的符号表功能。但除此之外&#xff0c;Windows 官方有一个功能&#xff0c;可以让我们可以通过 Alt 键输入任何特殊符号。 ‍…

Python进阶--函数进阶

目录 1. 函数多返回值 2. 函数多种传参方式 (1). 位置参数 (2). 关键字参数 (3). 缺省参数 (4). 不定长参数 3. 匿名函数 (1). 函数作为参数传递 (2). lambda匿名函数 1. 函数多返回值 def return_num():return 1# 返回1之后就不会再向下继续执行函数体return 2 resu…

《Linux从小白到高手》理论篇:Linux软件安装一篇通

List item 本篇介绍Linux软件安装相关的操作命令&#xff0c;看完本文&#xff0c;有关Linux软件安装相关操作的常用命令你就掌握了99%了。 Linux软件安装 RPM RPM软件的安装、删除、更新只有root权限才能使用&#xff1b;查询功能任何用户都可以操作&#xff1b;如果普通用…

ElasticSearch备考 -- Alias

一、题目 1) Create the alias hamlet that maps both hamlet-1 and hamlet-2 Verify that the documents grouped by hamlet are 8 2) Configure hamlet-3 to be the write index of the hamlet alias 二、思考 可以通过指定别名&#xff0c;来指向一个或多个索引&#xff0c…

系统守护者:使用PyCharm与Python实现关键硬件状态的实时监控

目录 前言 系统准备 软件下载与安装 安装相关库 程序准备 主体程序 更改后的程序&#xff1a; 编写.NET程序 前言 在现代生活中&#xff0c;电脑作为核心工具&#xff0c;其性能和稳定性的维护至关重要。为确保电脑高效运行&#xff0c;我们不仅需关注软件优化&#xf…

美国静态住宅IP代理怎么定期更换?

在互联网使用中&#xff0c;P代理被广泛应用于许多方面&#xff0c;如网络安全测试、数据采集、访问受限制内容等。然而&#xff0c;为了维护隐私安全和避免被封禁&#xff0c;定期更换IP地址是必要的。特别是对于每个用户&#xff0c;定期更换IP地址更是至关重要。本文将探讨美…

大数据新视界 --大数据大厂之 Druid 查询性能提升:加速大数据实时分析的深度探索

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Python案例--九九乘法表

乘法口诀表是学习基础数学中不可或缺的工具&#xff0c;它帮助我们快速记忆乘法结果。在这篇文章中&#xff0c;我将向你展示如何使用Python编程语言来生成一个9x9的乘法口诀表。这不仅对教育工作者和学生有用&#xff0c;而且对任何需要快速回顾乘法事实的人来说都是一个有用的…

美团 spiderindefence 滑块 分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我删…

毕设 大数据抖音短视频数据分析与可视化(源码)

文章目录 0 前言1 课题背景2 数据清洗3 数据可视化地区-用户观看时间分界线每周观看观看路径发布地点视频时长整体点赞、完播 4 进阶分析相关性分析留存率 5 深度分析客户价值判断 0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕…

[C++]使用纯opencv部署yolov11-cls图像分类onnx模型

【算法介绍】 在C中使用纯OpenCV部署YOLOv11-cls图像分类ONNX模型是一项具有挑战性的任务&#xff0c;因为YOLOv11通常是用PyTorch等深度学习框架实现的&#xff0c;而OpenCV本身并不直接支持加载和运行PyTorch模型。然而&#xff0c;可以通过一些间接的方法来实现这一目标&am…

IMS添加实体按键流程 - Android14

IMS添加实体按键流程 - Android14 1、实体按键信息&#xff08;Mi 9 左侧实体按键&#xff09;2、硬件添加2.1 内核添加设备节点2.2 Generic.kl映射文件2.3 映射文件文件加载loadKeyMapLocked2.4 addDeviceLocked 添加设备相关对象 3、keycode对应scankode4、KeyEvent.java 添加…

[翻译]ANSI X9.24-1-2009

目录 1 目的 2 范围 2.1 应用 3 参考文献 4 术语和定义 4.1 acceptor 接收器 4.2 acquirer 收单 4.3 algorithm 算法 4.4 archived key 存档密钥 4.5 authentication 认证/鉴别/身份验证 4.6 authentication algorithm 认证算法 4.7 authentication element 认证要…

UE5数字人制作平台使用及3D模型生成

第10章 数字人制作平台使用及3D模型生成 在数字娱乐、虚拟现实&#xff08;VR&#xff09;、增强现实&#xff08;AR&#xff09;等领域&#xff0c;高质量的3D模型是数字内容创作的核心。本章将引导你了解如何使用UE5&#xff08;Unreal Engine 5&#xff09;虚幻引擎这一强大…