【C语言预处理详解(下)】--#和##运算符,命名约定,命令行定义 ,#undef,条件编译,头文件的包含,嵌套文件包含,其他预处理指令

article/2025/7/3 12:50:32

目录

五.#和##运算符

 5.1--#运算符

5.2--##运算符

六.命名约定,#undef,命令行定义

6.1--命名约定

6.2--#undef 

6.3--命名行定义

七.条件编译 

常见的条件编译指令: 

1.普通的条件编译:

2.多个分支的条件编译(可以利用条件语句来辅助理解):

3.判断是否被定义: 

4.嵌套指令:

八.头文件的包含

8.1--头文件被包含的方式:

 8.1.1--本地文件包含

8.1.2--库文件包含

 8.2--嵌套文件包含

九.其他预处理指令 


🔥个人主页:

🎬作者简介:C++研发方向学习者

📖个人专栏:

⭐️人生格言:生活是默默的坚持,毅力是永久的享受。


五.#和##运算符

 5.1--#运算符

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执⾏的操作可以理解为”字符串化“

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

可以如下这样写:
# define PRINT(n) printf( "the value of " #n " is "  format  "\n" , n);

 具体代码演示:

#include<stdio.h>
#define PRINT(n, format)  printf("the value of "#n" is "format"\n", n)int main()
{int a = 10;//printf("the value of a is %d\n", a);PRINT(a, "%d");float f = 3.14f;//printf("the value of f is %f\n", f);PRINT(f, "%f");return 0;
}

我们在这里主要分析一下变量a,我们定义好宏后,用上面的方式去调用它,我们把a替换到了宏的体内,就出现了#a,而#a转换为"a"这一字符串,然后format也被"%d"替换。

 我们还是看看PRINT(a, "%d");这一代码预处理之后的形式:

printf ( "the value of " "a" " is "  "%d" "\n" , a);

运行代码就得出来我们想要的结果。

5.2--##运算符

## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称
为记号粘合
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。
比如:
int int_max ( int x, int y)
{
     return x > y ? x : y;
}
float float_max ( float x, float y)
{
     return x > y ? x : y;
}

反复这样写形式差不多的函数太繁琐了,于是我们定义了如下所示的一个宏:

// 宏定义
# 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( 8 , 9 );
     printf ( "%d\n" , m);
     float fm = float_max( 6 .5f , 8 .5f );
     printf ( "%f\n" , fm);
     return 0 ;
}

最终代码实现: 

#include<stdio.h>
#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(8, 9);printf("%d\n", m);float fm = float_max(6.5f, 8.5f);printf("%f\n", fm);return 0;
}

 这里简单的举个例子帮助大家理解一下##运算符的使用,还想要更深入了解的可以去查阅一下相关资料来学习。


六.命名约定,#undef,命令行定义

6.1--命名约定

⼀般来讲函数和宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
我们的习惯写法是:
  • 把宏名全部⼤写
  • 函数名不要全部⼤写

6.2--#undef 

--我们通常用这条指令去移除一个宏定义

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

我们来看两个实际例子直观的了解一下它的使用: 

例一:

#include <stdio.h>#define M 100int main()
{printf("%d\n", M);
#undef M#define M 1000printf("%d\n", M);return 0;
}

 例二:

#include<stdio.h>
#define SQUARE(x) ((x) * (x))int main() 
{int a = 10;int ret=SQUARE(a);printf("%d\n", ret);
#undef SQUARE(x)#define SQUARE(x) ( ( x ) + ( x ) )int ret2 = SQUARE(a);printf("%d\n", ret2);}

6.3--命名行定义

许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)
我们可以通过下面这种方式来实现,这里建议大家使用vs code配置相应的环境来进行操作:
#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环境演示):

//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

七.条件编译 

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

常见的条件编译指令: 

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

八.头文件的包含

8.1--头文件被包含的方式:

 8.1.1--本地文件包含

1.  #include "filename"

查找策略:
  • 先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。
  • 如果找不到就提示编译错误。
Linux环境的标准头⽂件的路径:
1.   /usr/include

VS环境的标准头⽂件的路径: 

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

 2.  // 这是 VS2013 的默认路径

我们这里注意一下,要按照自己的安装路径去找。

8.1.2--库文件包含

1.  #include <filename.h> 

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ " "  的形式包含?
答案是肯定可以的,但是这样做查找的效率就低一些,而且这样也不容易区分是库⽂件还是本地⽂件 了。

 8.2--嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。

这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。

⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤了。

我们来看看实际例子吧~

test.c:

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"int main()
{return 0;
}

test.h: 


void test();struct Stu
{char name[20];int age;float score;};
如果直接这样写,test.c⽂件中将test.h包含8次,那么test.h⽂件的内容将会被拷⻉8份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果不堪设想。
那我们如何解决头⽂件被重复引⼊的问题呢? 答案就是使用条件编译。

这里给大家分享两种方法:

方法一:在每个文件的开头写如下内容

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

这个方式首先判断test.h有没有被定义,如果没有就定义一次,这样之后下次再重复出现test.h也没有用了,不符合这里的条件判断,所有可以有效的避免头文件的重复引入。 

方法二:新式的现代写法 

# pragma once

利用这种方式同样也可以有效的避免头文件的重复引入,更简洁一点。 

注意:这里给大家推荐一下《⾼质量C/C++编程指南》中附录的考试试卷的两个笔试题

  • 头⽂件中的 ifndef/define/endif是⼲什么⽤的?
  • #include <filename.h> 和 #include "filename.h" 有什么区别?

如果大家有兴趣的话可以在评论区留言后私信我,我把这本书分享给大家,这其中附录的考试试卷很有参考价值,比较重要。


九.其他预处理指令 

#error

#pragma

# line
...
这里就不做介绍了,感兴趣的可以⾃⼰去了解一下,建议参考《C语⾔深度解剖》学习。
# pragma pack() //在结构体部分介绍过,这个指令可以改变编译器的默认对齐数

往期回顾: 

【C语言预处理详解(上)】--预定义符号,#define定义常量,#define定义宏,带有副作用的宏参数,宏替换的规则,宏和函数的对比

【C语言编译与链接】--翻译环境和运行环境,预处理,编译,汇编,链接

【通关文件操作(上)】--文件的意义和概念,二进制文件和文本文件,文件的打开和关闭,文件的顺序读写 【通关文件操作(下)】--文件的顺序读写(续),sprintf和sscanf函数,文件的随机读写,文件缓冲区,更新文件

结语:本篇文章就到此结束了,继前面一篇文章后,在此篇文章中给大家分享了预处理详解中的剩余知识点,如#和##运算符,命名约定,命令行定义 ,#undef,条件编译,头文件的包含,嵌套文件包含,其他预处理指令等,到此C语言专栏的知识点分享也差不多结束了,后续笔者还会继续分享其它的内容,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家长久以来的支持。


http://www.hkcw.cn/article/jMRMxILorD.shtml

相关文章

数据资产评估进阶:精读资产评估专家指引第9号——数据资产评估指导【附全文阅读】

这篇文档是有关数据资产评估的专业报告&#xff0c;以下是文档中需要关注的重点内容&#xff1a; 1. 评估对象&#xff1a;文档中提到了数据资产评估的评估对象&#xff0c;即被评估数据资产。需要关注被评估数据资产的信息属性、法律属性、价值属性等&#xff0c;以及其特征对…

btstack协议栈---ESP32底层逻辑分析

目录 循环体 循环体中,怎么读取、处理数据 packet_handler 上面各层如何处理数据 谁触发了数据的传输? 硬件相关的数据有4类 循环体 BTStack针对不同的运行环境,抽象出了对应的btstack_run_loop结构体,共成员为: 比如其中的execute成员很重要,它是一个循环,在循…

碳中和新路径:铁电液晶屏如何破解高性能与节能矛盾?

一、显示技术困局&#xff1a;当 “高刷” 遭遇 “高耗” 在元宇宙、电竞产业蓬勃发展的当下&#xff0c;显示设备的刷新率与能耗成为行业痛点。传统液晶受 “边缘场效应” 制约&#xff0c;刷新率长期停滞在 300Hz 以下&#xff0c;动态画面拖影问题显著&#xff1b;同时&…

408考研逐题详解:2009年第27题

2009年第27题 一个分段存储管理系统中&#xff0c;地址长度为 32 位&#xff0c;其中段号占 8 位&#xff0c;则最大段长是&#xff08; &#xff09; A. 2 8 2^8 28B \qquad B. 2 16 2^{16} 216B \qquad C. 2 24 2^{24} 224B \qquad D. 2 32 2^{32} 232B 解析 本题…

ASC格式惯导数据文件转IMR格式文件

我们使用惯导采集数据之后&#xff0c;如果需要用现有软件进行解算&#xff0c;比如POSMind等等&#xff0c;就会涉及到IMR格式的惯导数据文件。而NovAtel Convert转换软件只能将原始DAT格式的文件转成ASCLL文件&#xff0c;因此我自编程实现了ASC格式文件到IMR格式文件的转换。…

电脑为什么换个ip就上不了网了

在日常使用电脑上网时&#xff0c;很多人可能遇到过这样的问题&#xff1a;当IP地址发生变化后&#xff0c;突然就无法连接网络了。当电脑更换IP地址后无法上网&#xff0c;这一现象可能由多种因素导致&#xff0c;涉及网络配置、硬件限制或运营商策略等层面。以下是系统性分析…

动中通天线跟踪性能指标的测试

卫星通信动中通天线包括天线、卫星信号跟踪接收机、GNSS接收机&#xff08;含天线&#xff09;、组合导航设备、天线控制器、伺服结构以及其他射频组件等。其中&#xff1a; • GNSS接收机提供系统位置信息&#xff1b; • 组合导航设备提供天线所在平台的方位、俯仰、横滚姿态…

从 GPT 的发展看大模型的演进

这是一个技术爆炸的时代。一起来看看 GPT 诞生后&#xff0c;与BERT 的角逐。 BERT 和 GPT 是基于 Transformer 模型架构的两种不同类型的预训练语言模型。它们之间的角逐可以从 Transformer 的编码解码结构角度来分析。 BERT&#xff08;Bidirectional Encoder Representatio…

Charles青花瓷抓取外网数据包

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; Charles有一个问题&#xff0c;开启翻墙工具后会发现无法进行抓包&#xff0c;这是需要做额外的配置才可以 首选选择下图中的External Proxy Settings 然后如下…

在考古方向遥遥领先的高校课程建设-250602

解决方案&#xff1a;全栈自学&#xff0c;全栈自研&#xff0c;独立自主。 全文AI…… 每代人的智商和注意力差异是如何出现的-250602-CSDN博客 网络还是有这些内容的&#xff1a; 考古教育之殇&#xff1a;高校课程建设的滞后与困境 在考古学这一承载着人类文明密码与历史记…

K-匿名模型

K-匿名模型是隐私保护领域的一项基础技术&#xff0c;防止通过链接攻击从公开数据中重新识别特定个体。其核心思想是让每个个体在发布的数据中“隐匿于人群”&#xff0c;确保任意一条记录至少与其他K-1条记录在准标识符&#xff08;Quasi-Identifiers, QIDs&#xff09;上不可…

BUUCTF[极客大挑战 2019]EasySQL 1题解

[极客大挑战 2019]EasySQL题解 分析解题过程漏洞原理分析明确注入点&#xff1a;尝试万能密码法法一法二 总结 分析 从题目分析&#xff0c;这道题应该与SQL注入有关&#xff0c;启动靶机之后&#xff0c;访问url是一个登录界面&#xff0c;随便输入用户名密码之后&#xff0…

8088单板机C语言项目计划表

Prj1 原来第一版8088单板机C语言实现版 用Nmake 和 Makefile编译方式实现的 略显复杂 Prj2 8088单板机C语言实现LED灯闪烁控制 Prj3 8088单板机C语言串口实现“Hellow World&#xff01;” Prj4 8088单板机C语言串口实现格式化sprintf&#xff08;&#x…

【电赛培训课程】测量与信号类赛题知识点讲解与赛题解析

一、三极管基础知识 1.基本运行规则 ICE βIBEUBE 0.7V 2.什么时候选择使用三极管而不是运算放大器 不需要精确的放大倍数&#xff08;交流放大&#xff09;题目指定 3.优点 不容易产生自激振荡&#xff0c;在相同的频率下更不容易失真便宜量大管够 二、三极管放大电路…

学到新的日志方法mp

使用mp技术的时候可以在类上加上注解Slf4j 就可以使用日志 不需要在定义变量log,注意日志只能在方法内使用&#xff0c;不能在方法外进行使用

Linux入门(十三)动态监控系统监控网络状态

top与ps 命令很相似&#xff0c;它们都是用来显示正在执行的进程&#xff0c;top与ps大的区别是top在执行一段时间可以更新正在运行的进程。 #-d 更新秒数 如果不写-d 那默认是3秒更新 # -i 隐藏不活跃进程 top -d 5交互操作 P 按cpu使用大小排序&#xff0c;默认此项 M 按内存…

SolidWorks建模(U盘)- 多实体建模拆图案例

这个U盘模型并不是一个多装配体&#xff0c;它是一个多实体零件&#xff0c;它是在零件模式下创建的这些多实体的零部件。按右键解除爆炸就可以装配到一起&#xff0c;再按右键爆炸&#xff0c;就能按照之前移动的位置进行炸开 爆炸视图直接展示 模型案例和素材或取&#xff08…

【C++高级主题】转换与多个基类

目录 一、多重继承的虚函数表结构&#xff1a;每个基类一个虚表 1.1 单继承与多重继承的虚表差异 1.2 代码示例&#xff1a;多重继承的虚函数覆盖 1.3 虚表结构示意图 二、指针与引用的类型转换&#xff1a;地址调整的底层逻辑 2.1 派生类指针转基类指针的地址偏移 2.2 …

论文写作核心要点

不要只读论文里的motivation和method 论文里的图表和统计特征 在论文里找到具有统计意义的东西&#xff0c;那么在语料里也肯定遵循这样的规律&#xff0c;我们就能用机器学习的方法&#xff0c; 我们再用不同方法解决&#xff0c;哪种方法好&#xff0c;就用哪种 实验分析 …

Hadoop 大数据启蒙:深入解析分布式基石 HDFS

Hadoop 大数据启蒙&#xff1a;深入解析分布式基石 HDFS 分布式存储的本质&#xff1a;用廉价机器集群解决海量数据的存储与容错问题 一、为什么需要 HDFS&#xff1f; 当数据规模突破单机极限&#xff08;如 PB 级&#xff09;&#xff0c;传统存储面临核心瓶颈&#xff1a; …