Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread一般讨论
【C语言进阶 】#if的使用你需要注意的坑
发布于 2022-03-31 21:03:04 浏览:1104
订阅该版
[tocm] **【C语言进阶 】#if的使用你需要注意的坑** 1 写在前面 2 问题描述 3 场景复现 4 深入分析 4.1 编译问题先看预处理文件 4.2 恶补C语言基础知识 4.3 原来是这么回事 5 修复验证 6 经验总结 7 参考链接 8 更多分享 --- # 1 写在前面 大家都知道C语言被广泛应用在嵌入式系统的编程中,一个很大的原因是C语言它是一种偏底层的高级语言,能够对内存、硬件等直接做高效操作;另一个原因,我想可能是还在于它的 **高可移植性** 和 **高可裁剪性**。 今天重点讲一个在可裁剪性上,C语言中用得非常广泛的 **#if** 语句;尽管这个语句大家一定用得非常熟练,但是往往不注意,可能你就会掉进坑里。 下面的问题,你不妨试试看。 # 2 问题描述 事情是这样的,有一天,我又突然被一个同事问蒙了!请看: ![image-20220311150105337](https://oss-club.rt-thread.org/uploads/20220714/399c696d4e64f6911538c213f6dbda2d02ddebf2.png) 啥意思,我们捋一捋。 873行的 **TURNNING_OFF** 值为1;(先不讨论为何OFF值是1而不是0) 874行的 **POWER_GRADUALLY_TURNING** 值为0;(当前配置选的是ON分支) 875-881使用 **#if-#elif-#endif** 做预处理判断,想达到根据值的不同实现相应代码的裁剪。 按照设计的【期望】值,当前配置下,代码逻辑应该是走**4 ---xxx** ,即879行 (ON逻辑); 【结果】:代码编译出来,发现它跑的可是 **3 ---xxx**,即876行 (OFF逻辑)。 难怪同事吓一跳了,当我看到这个问题,着实把我也问蒙了。 到底怎么回事啊? # 3 场景复现 为了更加精准地描述并复现问题,我按照当时的代码场景,重新构造一下代码场景,看下眼尖的看官能否一下子看出问题所在。 很简单,这个demo就两个文件,一个C文件和一个头文件: ```c /* main.c */ #include
#include "config.h" int main(void) { #if TURNNING_OFF == POWER_GRADUALLY_TURNING printf("%s:%d ... turnning off case ...\r\n", __func__, __LINE__); #elif TURNNING_ON == POWER_GRADUALLY_TURNING printf("%s:%d ... turnning on case ...\r\n", __func__, __LINE__); #endif return 0; } ``` ```c /* config.h */ #ifndef __CONFIG_H__ #define __CONFIG_H__ /* define turning on/off state */ enum { TURNNING_OFF = 0, TURNNING_ON = 1, }; /* cur configuration: select turning off */ #define POWER_GRADUALLY_TURNING TURNNING_OFF #endif /* __CONFIG_H__ */ ``` 我们直接使用X64环境的GCC编译跑一下: ![image-20220331075437765](https://oss-club.rt-thread.org/uploads/20220714/fef11ca9b9ad3bd45f2b147bfb464ee39c2977f6.png) 果然编译器是不会骗人的,结果如同事的现象一致。 # 4 深入分析 ## 4.1 编译问题先看预处理文件 我在博文 [编译问题的分析方法汇总](https://blog.csdn.net/szullc/article/details/122261137) 的4.2.2章节有介绍再GCC编译环境下如何打开中间文件输出,得到预处理后的文件。 我们试一下,看看: ```c 省略部分内容。。。 738 # 8 "config.h" 739 enum { 740 TURNNING_OFF = 1, 741 TURNNING_ON = 0, 742 }; 743 # 6 "main.c" 2 744 745 int main(void) 746 { 747 printf("%s:%d ... TURNNING_OFF = %d\r\n", __func__, 9, TURNNING_OFF); 748 printf("%s:%d ... POWER_GRADUALLY_TURNING = %d\r\n", __func__, 10, TURNNING_ON); 749 750 751 printf("%s:%d ... turnning off case ...\r\n", __func__, 13); 752 753 754 755 756 return 0; 757 } ``` 然而,啥也看不到,能看到的只是个结果,看不到过程;究竟 **#if** 是如何展开判断的,已经看不到了。无功而返! ## 4.2 恶补C语言基础知识 - **C 预处理器** C预处理器是C代码在送入正式编译前必须警告的一个部件,通常来说C预处理器只处理 **#** 符号开头的几个特殊的指令语句,具体如下所示: ![image-20220331181918034](https://oss-club.rt-thread.org/uploads/20220714/f9435f3f15de652eb2ad5b955213a0e635f68935.png) 详细的表格,可以参考 [这里](https://www.w3cschool.cn/c/c-preprocessors.html)。 - **条件编译** 条件编译是C语言的一大特色,很多高级语言都没有这玩意。 在C语言里面,可用于条件变量的几个预编译指令有:**#if 、#ifdef 、#ifndef 、#else 、#elif 、#if defined、#endif** 等等几个。 如本案例中使用的是 **#if**,那我重点讲一下这个 **#if** ,但它不能单独使用,往往需要跟其他几个结合来使用。 它的基础语法是: > ``` > #if 表达式1 > code1... > #elif 表达式2 > code2... > #else > code3... > #endif > ``` **预处理器** 会依次计算条件表达式,直到发现结果非 0(也就是 true)的条件表达式。预处理器会保留对应组内的源代码,以供后续处理。如果找不到值为 true 的表达式,并且该条件式编译区域中包含 #else 命令,则保留 #else 命令组内的代码。 code1、code2 等代码段,可以包含任意 C 源代码,也可以包含更多的命令,包括嵌套的条件式编译命令。在 **预处理阶段** 结束时,没有被预处理器保留以用于后续处理的组会从程序中全部删除。 【注意】这一系列的操作都是在 **预处理阶段** 完成的。 - **宏定义** 宏定义也是C语言的另一大特色,熟练使用宏定义的高阶功能,往往能写出很优雅的代码,这一点在Linux内核代码中就体现得淋漓尽致。 宏定义是在预编译阶段被预编译器处理的,一般来说,从语法上分,宏定义分为两种:不带参数的宏定义和带参数的宏定义。 - **不带参数的宏定义** 这种情况就是用一个指定的 **标识符** 来代表一个 **字符串**。它的一般形式为; > #define 标识符 字符串 注意这里的 **“字符串”**,不是我们理解的C语言里面的字符串类型,而是真真正正的一个可读可显示的字符串;有了这个解释你才不会纳闷,我用 **#define XXX 1**不是定义了XXX为一个整数1嘛? 在这里 **1** 对于预处理器来说,就是一个字符串(一串的字符),至于代码里面是用作 整数1还是字符串"1",那是后面编译阶段要解决的问题,预处理可不管这。 - **带参数的宏定义** 带参数的宏定义不是仅仅进行简单的字符串替换,还要进行参数替换。其定义的一般形式为; > #define 标识符(宏名)(参数表) 字符串 其中字符串中包含在括号中所指定的参数。 这种宏定义的写法用得也很广泛,当你看到那些 **你自认为看不懂的宏定义**,回到这个宏定义语法的本质来理解下,也许你能打开思路。 我个人自认为对宏定义还是研究得比较透彻,之前也写过几篇关于 **宏定义** 的文章,感兴趣的可以从 **参考链接** 那里去了解一下。 - **枚举定义** 枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。 枚举语法定义格式为: > ``` > enum 枚举名 {枚举元素1,枚举元素2,……}; > ``` 很好理解,它的设计看起来就是为了更好地取代使用 **#define** 定义一些常量,比如像这样: ```c /* 使用宏定义定义一个星期7天 */ #define MON 1 #define TUE 2 #define WED 3 #define THU 4 #define FRI 5 #define SAT 6 #define SUN 7 /* 使用枚举定义一个星期7天 */ enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; ``` 很显然,使用枚举的写法更加优雅,的确是一种进步。 另外,还需要补充一点是,枚举变量里面的值都是确认范围的。 拿回这个星期定义来说,如果使用枚举变量来定义星期,那么它不应该出现 **MON, TUE, WED, THU, FRI, SAT, SUN** 以外的其他值。 ## 4.3 原来是这么回事 有了上面的知识铺垫,我们再回到问题本身,再仔细分析一下: ```c #if TURNNING_OFF == POWER_GRADUALLY_TURNING ``` 明明POWER_GRADUALLY_TURNING是TURNNING_ON呀,就是不成立的呀,为何保留了它下面的代码? 回到基础语法: - TURNNING_OFF 是一个 **枚举定义**,值为1; - POWER_GRADUALLY_TURNING是一个 **宏定义**,定义为 **TURNNING_ON**,值为0; - **#if** 指令的处理在预编译阶段; - 预编译阶段只会处理宏定义,而不会处理枚举定义。 有了这几条知识,基本就能够知道,为何代码不生效了。 TURNNING_OFF这是**枚举定义**啊,预编译器压根就不知道你是啥,也不会给你展开值。 至于为何 **==** 这条表达式却成立呢?还需要再往下深究。 我做了一个小实验,修改了下main.c的代码: ```c /* main.c */ #include
#include "config.h" //#define EMPTY1 //#define EMPTY2 int main(void) { printf("%s:%d ... TURNNING_OFF = %d\r\n", __func__, __LINE__, TURNNING_OFF); printf("%s:%d ... POWER_GRADUALLY_TURNING = %d\r\n", __func__, __LINE__, POWER_GRADUALLY_TURNING); #if EMPTY1 == EMPTY2 printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #ifdef TURNNING_OFF printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #ifdef TURNNING_ON printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #ifdef POWER_GRADUALLY_TURNING printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #if TURNNING_OFF == TURNNING_ON printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #if TURNNING_OFF == 0 printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #if 1 == POWER_GRADUALLY_TURNING printf("%s:%d ... here work ...\r\n", __func__, __LINE__); #endif #if TURNNING_OFF == POWER_GRADUALLY_TURNING printf("%s:%d ... turnning off case ...\r\n", __func__, __LINE__); #elif TURNNING_ON == POWER_GRADUALLY_TURNING printf("%s:%d ... turnning on case ...\r\n", __func__, __LINE__); #endif return 0; } ``` 很简单,就是想知道一下 **TURNNING_OFF、TURNNING_ON** 有没有被宏定义,以及在没有被定义的时候, **#if ==** 结果会如何? 结果跑出来,出乎了我的意料。 ![image-20220401102833568](https://oss-club.rt-thread.org/uploads/20220714/ad88bff22b0add7e97f26eb72746f7fbd3005fb4.png) 也就是说 16行、28行、32行、36行、44行都被编译进去了,对比C代码我们可知: - **TURNNING_OFF和TURNNING_ON**都没有被宏定义;(20行、24行未打印) - 一个未被宏定义的字符串(标识符)在#if == 里面,它的值一直是 **0**;(26行被打印、40行未被打印) - 当使用未被宏定义的两个字符串(标识符)在#if **==** 比较时,结果总是 **真**。(16行、32行、44行被打印) 有了这几个结论,我们再捋一捋这个代码写法被预处理的流程: ```c #if TURNNING_OFF == POWER_GRADUALLY_TURNING ``` 因为POWER_GRADUALLY_TURNING有被宏定义成TURNNING_ON,所以第一步预处理器会把这句替换成: ```c #if TURNNING_OFF == TURNNING_ON ``` 而根据我们的小实验得出的结论,**TURNNING_OFF和TURNNING_ON** 都没有被宏定义,所以他们的值等价为0,即变成: ```c #if 0 == 0 ``` 这个结果自然总是为真,所以对应的代码块就被编译进去了。 # 5 修复验证 明白了上面提及的C语言基础语法之后,回到问题的本质,就很容易解决问题了。 只需要把config.h中的 TURNNING_OFF、TURNNING_ON改用宏定义即可,修改后的文件如下: ```c /* config_new.h */ #ifndef __CONFIG_H__ #define __CONFIG_H__ /* define turning on/off state */ #define TURNNING_OFF 1 #define TURNNING_ON 0 /* cur configuration: select turning off */ #define POWER_GRADUALLY_TURNING TURNNING_ON #endif /* __CONFIG_H__ */ ``` 简单验证下: ![image-20220331080328237](https://oss-club.rt-thread.org/uploads/20220714/25ab29a8ce521776c48d315e66c7b766a0a95656.png) ![image-20220331080350401](https://oss-club.rt-thread.org/uploads/20220714/5b1f828cd0a2fe7a0e63d80e4944bd732cd108bd.png) 完美!收工! # 6 经验总结 - **#if** 等一系列预处理指令的用法:**一个未被宏定义的标识符,出现在 #if == 中时,它的值总是0**; - **枚举**定义与**宏定义**的本质区别:一个是为了使常量定义变得更加明朗,作用于运行阶段;一个仅仅是字符串替换,作用于预编译阶段和运行阶段。 - 底层**基础语法**的重要性:万丈高楼平地起,万变不离其;掌握核心方能破局。 - 如何分析问题的**根源**很关键:编译问题先看预处理文件准没错,找不到原因的时候,再复习下基础语言,看看能不能找到蛛丝马迹。 - 【注意】:本案例使用编译环境是 **GCC**,相关结论和解决方法都是基于此环境得出的,不保证其他C编译器也有类似的问题,未来得及验证。 # 7 参考链接 - [【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路](https://blog.csdn.net/szullc/article/details/122261137) - [【经验总结】一文带你了解C代码到底是如何被编译的](https://blog.csdn.net/szullc/article/details/121709458) - [【C语言进阶】C语言带返回值的宏定义](https://blog.csdn.net/szullc/article/details/121297749) - [【Linux内核】从小小的宏定义窥探Linux内核的精妙设计](https://blog.csdn.net/szullc/article/details/84779352) - [【C语言面试题】请使用宏定义实现字节对齐](https://blog.csdn.net/szullc/article/details/119391109) - [C语言宏定义的基础用法](https://baijiahao.baidu.com/s?id=1634229214362059225) - [C语言枚举定义的基础用法](https://www.runoob.com/cprogramming/c-enum.html) - [C语言的条件编译](http://c.biancheng.net/view/449.html) # 8 更多分享 欢迎关注我的[github仓库01workstation](https://github.com/recan-li/01workstation/tree/master/workspace/gcc/gc_section),日常分享一些开发笔记和项目实战,欢迎指正问题。 同时也非常欢迎关注我的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) 有问题的话,可以跟我讨论,知无不答,谢谢大家。
8
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
有关动态模块加载的一篇论文
2
最近的调程序总结
3
晕掉了,这么久都不见layer2的踪影啊
4
继续K9ii的历程
5
[GUI相关] FreeType 2
6
[GUI相关]嵌入式系统中文输入法的设计
7
20081101 RT-Thread开发者聚会总结
8
嵌入式系统基础
9
linux2.4.19在at91rm9200 上的寄存器设置
10
[转]基于嵌入式Linux的通用触摸屏校准程序
推荐文章
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
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部