19、__func__ 与 __FUNCTION__:函数名宏、调试日志、断言信息
一、两个“函数名”宏的来龙去脉
做嵌入式开发,调试是家常便饭。我个人习惯在日志里加上当前函数名,这样定位问题快得多。C99 标准给我们提供了 __func__,而很多编译器(尤其是 GCC、MSVC)还额外支持 __FUNCTION__。这两个东西到底有什么区别?
说白了,__func__ 是标准规定的,__FUNCTION__ 是历史遗留的“方言”。C99 标准里明确写了:__func__ 是一个隐式定义的标识符,它在每个函数作用域内被自动声明为:
static const char __func__[] = "函数名";
嗯,这里要注意——它不是宏,是一个隐式的局部静态变量。而 __FUNCTION__ 是很多编译器自己加的宏,行为类似,但不在标准里。
我在项目中遇到过一个问题:某同事在 IAR 环境下用了 __FUNCTION__,代码移植到 Keil 时编译报错。后来我统一改成 __func__,两边都认。所以我的建议是:新代码优先用 __func__,兼容性更好。
核心区别一览:
| 特性 | __func__ | __FUNCTION__ |
|---|---|---|
| 标准来源 | C99 标准 | 编译器扩展(GCC/MSVC) |
| 本质 | 隐式局部静态变量 | 宏(或预定义标识符) |
| 可移植性 | 高(所有 C99+ 编译器) | 低(依赖编译器) |
| 能否被 #ifdef 检测 | 不能(不是宏) | 可以(部分编译器) |
二、调试日志:让每一行输出都知道自己是谁
你想想看,一个嵌入式系统跑起来,串口日志哗哗地刷。如果没有函数名信息,你看到一行 "Error: timeout",根本不知道是哪个模块报的。我曾经在一个多任务项目中,花了整整一个下午才定位到一个 SPI 超时问题——就是因为日志里没打函数名。
从那以后,我写日志宏一定会带上 __func__:
#define LOG_DEBUG(fmt, ...) \
printf("[DEBUG] [%s] " fmt "\n", __func__, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
printf("[ERROR] [%s:%d] " fmt "\n", __func__, __LINE__, ##__VA_ARGS__)
用法很简单:
void uart_send(uint8_t *data, uint32_t len) {
if (data == NULL) {
LOG_ERROR("data pointer is NULL");
return;
}
LOG_DEBUG("sending %u bytes", len);
// 实际发送逻辑...
}
输出效果:
[ERROR] [uart_send:42] data pointer is NULL
[DEBUG] [uart_send] sending 128 bytes
你看,哪一行出的错,一目了然。我个人习惯在 LOG_ERROR 里额外加上 __LINE__,这样连行号都有了,调试效率翻倍。
小技巧: 如果你用 GCC,还可以加上 __PRETTY_FUNCTION__,它会输出完整的函数签名(包括返回值类型和参数类型),对 C++ 重载函数特别有用。
三、断言信息:让 assert 不再“哑巴”
标准库的 assert() 其实已经包含了 __func__ 信息(在 C99 之后)。但很多嵌入式环境会自己实现断言,因为标准 assert 在 NDEBUG 定义下会被完全移除,而我们有时希望即使在 Release 版中也保留部分检查。
我曾经在一个电机控制项目里,自己写了一个“永不消失”的断言宏:
#define ASSERT(cond, msg) do { \
if (!(cond)) { \
printf("ASSERT FAILED: %s\n", msg); \
printf(" Function: %s\n", __func__); \
printf(" File: %s, Line: %d\n", __FILE__, __LINE__); \
while(1); /* 死机,方便调试器抓现场 */ \
} \
} while(0)
用法:
void pwm_set_duty(uint32_t duty) {
ASSERT(duty <= 1000, "PWM duty cycle out of range");
// 设置 PWM 寄存器...
}
当断言触发时,输出:
ASSERT FAILED: PWM duty cycle out of range
Function: pwm_set_duty
File: pwm_control.c, Line: 87
嗯,这里要注意——while(1) 死循环是故意的。在嵌入式调试中,让 CPU 停下来比让它继续乱跑要好得多。你可以用调试器挂上去,看调用栈、看寄存器,问题瞬间暴露。
警告: 不要在产品代码里用 while(1) 死机。至少加一个条件编译开关,让 Release 版只记录日志而不死机。我曾经见过一个同事把调试断言直接带到量产固件里,结果现场设备动不动就死机……嗯,那场面挺尴尬的。
四、知识体系:一张图看懂
下面这张图总结了 __func__ 和 __FUNCTION__ 的核心用法和关系:
五、避坑指南:我踩过的几个坑
这些年用 __func__ 和 __FUNCTION__,我总结了几条经验,分享给你:
- 不要在头文件里用
__func__定义静态变量。我曾经在一个头文件里写了static const char *name = __func__;,结果每个包含这个头文件的 .c 文件都有一份独立的拷贝,而且__func__的值是空字符串(因为不在函数内)。嗯,那 bug 查了我两个小时。 - 注意
__func__在宏展开中的行为。如果你在宏里用__func__,它展开后是宏被调用的函数名,不是宏定义所在的函数名。这个特性其实很有用,但新手容易误解。 - 裸机环境下慎用
printf加__func__。有些低端 MCU 的 printf 实现很占栈空间,加上长函数名可能导致栈溢出。我建议用puts或自定义的轻量输出函数。 - C++ 中优先用
__PRETTY_FUNCTION__。如果你在写 C++ 嵌入式代码,__PRETTY_FUNCTION__会输出完整的函数签名,包括类名和参数类型,对模板函数尤其有用。
我的个人习惯: 在项目里统一一个 debug.h 头文件,里面定义好所有日志和断言宏。然后整个团队都用同一套。这样代码风格统一,调试效率也高。你想想看,如果每个人写日志的格式都不一样,那 grep 起来多痛苦。
好了,关于 __func__ 和 __FUNCTION__ 就讲这么多。这两个小工具虽然简单,但用好了能让你的调试效率翻倍。下次写日志或断言时,记得带上函数名——你一定会感谢这个习惯的。
公众号:蓝海资料掘金营,微信deep3321