Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
C语言
宏定义define
头文件
【C语言进阶】谈一谈C语言中头文件包含和宏定义的问题
发布于 2022-04-30 22:25:36 浏览:847
订阅该版
[tocm] > **摘要** > 本文结合博主真实的工程案例,由一个C语言常见的头文件包含问题带来的小小困惑。本文将案例进行拆解,分享如何一步步解决这样的疑难问题,希望能给大家带来帮助和启发。 ---- **文章目录** 1 写在前面 2 问题描述 3 场景复现 4 深入分析 4.1 知识点补充 4.2 初步分析 4.3 得出结论 5 修复验证 6 经验总结 7 参考链接 8 更多分享 ---- # 1 写在前面 又有一段时间没有写 **C语言** 的疑难杂症了,今天给大家带来一个日常开发中可能会遇到的 **小问题** ,但大家可千万别对这种小问题掉以轻心,如果一时半会你转不过弯来,可能你就卡在那里了。 当然,这种问题对于C语言新手可能会比较头疼,老司机可能一眼就看穿了。 通过本文的分享,你可以了解到以下的内容: - **C语言的头文件包含问题** - **C语言宏定义的问题** - **分析C语言编译问题的核心方法论** # 2 问题描述 事情是这样,有一次有个应用开发又问了我一个问题,说他的代码编译又遇到了一个莫名的问题: ![image-20220430194348801](https://s2.loli.net/2022/04/30/aBKAb5oHiPnchNs.png) ![image-20220430194616602](https://s2.loli.net/2022/04/30/DaIBhkAqQ7bMe8H.png) 他的核心问题就是,先在某个头文件中定义了一个 **宏定义**,然后使用 **#ifndef** 去编译判断,居然条件成立了。也就是如上图所示的 **QWQQQ** 居然报错了。这个看似 **非常不符合逻辑** !进而影响到了他的一些应用代码逻辑。 # 3 场景复现 为了准确还原他的场景及问题,我重新简化梳理下相关代码,总共有3个头文件,1个C文件: ```c /* a.h */ #ifndef __A_H__ #define __A_H__ #include "b.h" #ifndef PROJECT_DEVICE_MODE //QWQQQ (compile here ...) #define PROJECT_DEVICE_MODE PWM_MODE #endif #define LIGHT_DEVICE_MODE PROJECT_DEVICE_MODE extern int test_a; #endif /* __A_H__ */ ``` ```c /* b.h */ #ifndef __B_H__ #define __B_H__ #include "a.h" typedef enum _device_mode_e { I2C_MODE = 0, PWM_MODE = 1, } device_mode_e; /* slelect I2C_MODE */ #ifndef PROJECT_DEVICE_MODE #define PROJECT_DEVICE_MODE I2C_MODE #endif extern int test_b; #endif /* __B_H__ */ ``` ```c /* c.h */ #ifndef __C_H__ #define __C_H__ #include "b.h" /* define common macros */ #define TEST_MACRO "hello-world" extern int test_c; #endif /* __C_H__ */ ``` ```c /* main.c */ #include
#include "c.h" int main(void) { printf("project device mode : %d (0:I2C 1:PWM)\r\n", LIGHT_DEVICE_MODE); return 0; } ``` 简单概括下: 1)在 **b.h** 中定义了MODE的枚举,然后定义 **PROJECT_DEVICE_MODE** 为 **I2C_MODE** 2)在 **a.h** 中判断 **PROJECT_DEVICE_MODE** 是否定义(line 8),没有定义的话,就重新定义为 **PWM_MODE** 3)同时在 **a.h** 中把 **LIGHT_DEVICE_MODE** 定义为 **PROJECT_DEVICE_MODE** 4)在 **c.h** 中定义一些共用的宏定义 5)最后再 **main.c** 中打印 **LIGHT_DEVICE_MODE** 的值,结果打印出来是 **1**,即 **PWM_MODE**。 ```shell include_macro$ ./build.sh gcc main.c -Wall -Werror -save-temps=obj -o test include_macro$ ./test project device mode : 1 (0:I2C 1:PWM) ``` 如果我把 **b.h** 中那个 **QWQQQ** 注释打开,自然编译都会报错: ```shell include_macro$ ./build.sh gcc main.c -Wall -Werror -save-temps=obj -o test In file included from b.h:7, from c.h:7, from main.c:5: a.h:10:8: error: unknown type name ‘compile’ 10 | QWQQQ (compile here ...) | ^~~~~~~ ``` 这个跟同事的问题现场是吻合的。 # 4 深入分析 这里其实主要涉及C语言的两个知识点:**头文件包含** 和 **宏定义** 。 ## 4.1 知识点补充 - **C语言头文件包含** 在C程序中要使用头文件,需要使用 C 预处理指令 **#include** 来引用它。**#include** 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 **#include** 指令之后的文本输出。 - **C语言宏定义** 宏定义是由源程序中的宏定义命令`#define`完成的,宏替换是由预处理程序完成的。 宏定义的一般形式为: > #define 宏名 字符串 `#`表示这是一条预处理命令,所有的预处理命令都以 # 开头。`define`是预处理命令。`宏名`是标识符的一种,命名规则和标识符相同。`字符串`可以是数字、表达式、if 语句、函数等。**这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。** **#ifndef** 是条件编译控制的一种写法,用于判断某个宏是否被定义;当某个宏没有被定义时,该符号控制的代码块会参与编译。 ## 4.2 初步分析 我们回头再看看上面抽象出来的代码片段,在 **a.h** 中使用 **#ifndef** 判断 **PROJECT_DEVICE_MODE** 是否被定义过,原因是他知道这个宏已经在 **b.h** 中定义过了,只不过在这里补定义了一下,防止宏定义为空。 所以在他的逻辑是 **QWQQQ** 那行压根不会被编译,结果却没有按照预期在走。 问题看样子并不是出在 **a.h**,但我们回过头来看下 **b.h**: ```c /* b.h */ #ifndef __B_H__ #define __B_H__ #include "a.h" typedef enum _device_mode_e { I2C_MODE = 0, PWM_MODE = 1, } device_mode_e; /* slelect I2C_MODE */ #ifndef PROJECT_DEVICE_MODE #define PROJECT_DEVICE_MODE I2C_MODE #endif extern int test_b; #endif /* __B_H__ */ ``` 注意 **第7行** 的内容:`#include "a.h"` 。 什么概念? **b.h** 又包含 **a.h** ? 前面不是 **a.h** 包含 **b.h** 吗? 这不就是 **传说中的递归头文件包含** ? ## 4.3 得出结论 我们顺着上面的思路,捋一捋这些C文件和头文件的依赖关系: > **main.c —>** > > > **c.h —>** > > > > > **b.h —>** > > > > > > > **a.h —>** > > > > > > > > > **b.h —>** > > > > > > > > > > > **a.h —>** > > > > > > > > > > > > > **b.h —>** > > > > > > > > > > > > > > > ...... 很明显在后面的头文件依赖关系中,并不是一条直线的线性依赖,而是有递归的趋势。 那么像这种 **头文件递归包含** 在GCC编译器中如何处理的呢? [【GCC编译优化系列】一文带你了解C代码到底是如何被编译的](https://recan.blog.csdn.net/article/details/121709458) ,这里有介绍,头文件的包含解析是在 **预处理* ** 阶段被处理的,而GCC编译中,默认是不输出预处理这类中间文件的,但有一个选项可以同步输出这类文件: **-save-temps=obj** 在这个选项的作用下,编译过程会同步输出 **.i** 文件,我们可以看下这个文件的内容: ```shell // 前面的内容可以忽略 # 5 "main.c" 2 # 1 "c.h" 1 # 1 "b.h" 1 # 1 "a.h" 1 # 1 "b.h" 1 # 8 "a.h" 2 # 10 "a.h" QWQQQ (compile here ...) extern int test_a; # 8 "b.h" 2 typedef enum _device_mode_e { I2C_MODE = 0, PWM_MODE = 1, } device_mode_e; extern int test_b; # 8 "c.h" 2 extern int test_c; # 6 "main.c" 2 int main(void) { printf("project device mode : %d (0:I2C 1:PWM)\r\n", PWM_MODE); return 0; } ``` 从.i文件中我们可以看到,这个头文件递归依赖并没有一直递归下去,而是递归了几次之后就停止了,同时我们还可以看到, **a.h** 比 **b.h** 优先被解析,所以 **a.h** 中的 **#ifndef PROJECT_DEVICE_MODE** 就被判断成立了。 这就出现了前文提及的问题了。 # 5 修复验证 针对上面的代码场景,既然我们已经发现了是 **头文件递归包含** 引入的问题,自然我们就要打破这一层 **递归包含** 的关系。 从代码的意图,我们可以了解到,其实他本质是想分别在 **a.h** 和 **b.h** 中定义无相关的内容,而 **c.h** 中定义共有的东西,所以我建议调整头文件的内容如下: ```c /* a.h */ #ifndef __A_H__ #define __A_H__ #include "b.h" /* slelect I2C_MODE */ #ifndef PROJECT_DEVICE_MODE #define PROJECT_DEVICE_MODE I2C_MODE #endif #ifndef PROJECT_DEVICE_MODE QWQQQ (compile here ...) #define PROJECT_DEVICE_MODE PWM_MODE #endif #define LIGHT_DEVICE_MODE PROJECT_DEVICE_MODE extern int test_a; #endif /* __A_H__ */ /* b.h */ #ifndef __B_H__ #define __B_H__ typedef enum _device_mode_e { I2C_MODE = 0, PWM_MODE = 1, } device_mode_e; extern int test_b; #endif /* __B_H__ */ /* c.h */ #ifndef __C_H__ #define __C_H__ /* define common macros */ #define TEST_MACRO "hello-world" extern int test_c; #endif /* __C_H__ */ /* main.c */ #include
#include "b.h" #include "a.h" #include "c.h" int main(void) { printf("project device mode : %d (0:I2C 1:PWM)\r\n", LIGHT_DEVICE_MODE); return 0; } ``` 主要的修改就是,把模式定义统一放在 **a.h** ,而 **b.h** 中不再定义模式,仅定义相关枚举变量,同时在C文件中,依次包含 **b.h a.h c.h**,保证头文件包含没有递归的情况出现。 我们再捋一捋这时候的头文件依赖关系: > main.c —> > > > b.h > > > > a.h —> b.h > > > > c.h —> 这个依赖关系就很简洁了,没有交织在一起,造成不必要的困惑。 工程重新编译执行后,得到了预期的结果。 ```shell include_macro$ ./build.sh gcc main.c -Wall -Werror -save-temps=obj -o test include_macro$ ./test project device mode : 0 (0:I2C 1:PWM) ``` **值得一说的是**: 在同事的代码里面,远远比这个demo工程要复杂,所以真实问题场景下,问题排查起来也没有上面说的那么简单。 但是终究有一点就是,代码依赖关系越简单,越不容易出现问题。 我们有理由推测,这个问题代码肯定是由于早期对代码的设计规划不清楚,然后在代码迭代过程中,经过了N个人的手,大家都是修修补补,不从全盘考虑问题,仅仅是满足自我功能需求,造成到如今代码的依赖关系错综复杂,直到问题爆发。 【[本文的例程,可从我的gitee仓库中找到,欢迎指正](https://gitee.com/recan-li/coding-01workstation/tree/master/workspace/c_c++/include_macro)】 # 6 经验总结 - **C语言编译有方法可循,前提是了解清楚C语言编译的基础流程及核心原理** - **头文件的包含是一门学问,不恰当的头文件包含,可能引入意想不到的后果** - **头文件递归包含的问题,应该在日常编程中避免,这样带来的后果是头文件依赖异常混乱** - **分析头文件的依赖,是预编译阶段需要解决的问题,所以看预编译后的文件就能找到的问题的根源** - **头文件的定义需要遵循一定的原则:一个头文件尽量只干一件事或一类事,不要掺杂过多的依赖** - **代码整洁之道,往往从剔除错综复杂的依赖关系开始** - **越是简单的代码,越不容易出问题** # 7 参考链接 - [【GCC编译优化系列】一文带你了解C代码到底是如何被编译的](https://recan.blog.csdn.net/article/details/121709458) - [【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路](https://recan.blog.csdn.net/article/details/122261137) - [【C语言基础】头文件的包含](https://www.runoob.com/cprogramming/c-header-files.html) - [【C语言基础】宏定义的使用](http://c.biancheng.net/cpp/html/65.html) # 8 更多分享 > **[架构师李肯](https://recan.blog.csdn.net/?type=blog)** > > 一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获[CSDN博客专家](https://recan.blog.csdn.net/?type=blog)、CSDN物联网领域优质创作者、[2021年度CSDN&RT-Thread技术社区之星](https://blog.csdn.net/szullc/article/details/123860472)、[RT-Thread官方嵌入式开源社区认证专家](https://club.rt-thread.org/ask/experts.html)、[RT-Thread 2021年度论坛之星TOP4](https://club.rt-thread.org/ask/article/3317.html)、[华为云云享专家(嵌入式物联网架构设计师)](https://bbs.huaweicloud.com/community/usersnew/id_1573655458316259)等荣誉。坚信【知识改变命运,技术改变世界】! > --- 欢迎关注我的[gitee仓库01workstation](https://gitee.com/recan-li/coding-01workstation) ,日常分享一些开发笔记和项目实战,欢迎指正问题。 同时也非常欢迎关注我的CSDN主页和专栏: [【CSDN主页-架构师李肯】](http://yyds.recan-li.cn) [【RT-Thread主页-架构师李肯】](https://club.rt-thread.org/u/18001) [【C/C++语言编程专栏】](https://blog.csdn.net/szullc/category_8450784.html) [【GCC专栏】](https://blog.csdn.net/szullc/category_8626555.html) [【信息安全专栏】](https://blog.csdn.net/szullc/category_8452787.html) [【RT-Thread开发笔记】](https://blog.csdn.net/szullc/category_11461616.html) [【freeRTOS开发笔记】](https://blog.csdn.net/szullc/category_11467856.html) 有问题的话,可以跟我讨论,知无不答,谢谢大家。
2
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
RT-Thread内存和字符串相关函数与C语言自带的内存和字符串相关函数冲突问题
2
嵌入式RT-thread中初始化线程函数中(void *)entry的意义何在
3
cJSON parse 失败,请问怎么解决?
4
请教一个C语言顺序表的问题,不知道有没有大佬帮吗解答
5
小白请教,关于头文件引用,、、哪些场景需要引用它们?
6
使用中断有warn。。。。。。。。。。。
7
局部变量位置被编译器改写
8
RTthread 两个例程结合在一起会出现incompatible type for argument 2 of 'led_matrix_set_color'
9
有没有在单片机编程使用goto语句的?
10
请问如何读取另一个c文件中的温湿度数据啊
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
flashDB
GD32
socket
中断
编译报错
Debug
SFUD
rt_mq_消息队列_msg_queue
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
a1012112796
10
个答案
1
次被采纳
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
YZRD
2
篇文章
5
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部