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__ (C99标准) __FUNCTION__ (编译器扩展) 隐式局部静态变量 可移植性高 历史遗留,非标准 可被 #ifdef 检测 典型应用场景 调试日志 (LOG_DEBUG) 自定义断言 (ASSERT) 错误追踪 (Error Report)

五、避坑指南:我踩过的几个坑

这些年用 __func____FUNCTION__,我总结了几条经验,分享给你:

  1. 不要在头文件里用 __func__ 定义静态变量。我曾经在一个头文件里写了 static const char *name = __func__;,结果每个包含这个头文件的 .c 文件都有一份独立的拷贝,而且 __func__ 的值是空字符串(因为不在函数内)。嗯,那 bug 查了我两个小时。
  2. 注意 __func__ 在宏展开中的行为。如果你在宏里用 __func__,它展开后是宏被调用的函数名,不是宏定义所在的函数名。这个特性其实很有用,但新手容易误解。
  3. 裸机环境下慎用 printf__func__。有些低端 MCU 的 printf 实现很占栈空间,加上长函数名可能导致栈溢出。我建议用 puts 或自定义的轻量输出函数。
  4. C++ 中优先用 __PRETTY_FUNCTION__。如果你在写 C++ 嵌入式代码,__PRETTY_FUNCTION__ 会输出完整的函数签名,包括类名和参数类型,对模板函数尤其有用。

我的个人习惯: 在项目里统一一个 debug.h 头文件,里面定义好所有日志和断言宏。然后整个团队都用同一套。这样代码风格统一,调试效率也高。你想想看,如果每个人写日志的格式都不一样,那 grep 起来多痛苦。

好了,关于 __func____FUNCTION__ 就讲这么多。这两个小工具虽然简单,但用好了能让你的调试效率翻倍。下次写日志或断言时,记得带上函数名——你一定会感谢这个习惯的。


公众号:蓝海资料掘金营,微信deep3321