预定义宏:__FILE____LINE____DATE____TIME____STDC__

说实话,预定义宏这玩意儿,刚学C语言的时候我根本没当回事。觉得不就是几个下划线加几个字母嘛,能有多大用?直到有一次,我在一个嵌入式项目里被一个野指针折腾了整整三天,最后靠__FILE____LINE__定位到了问题。嗯,从那以后,我再也不敢小看这几个宏了。

今天咱们就来聊聊C标准规定的这几个预定义宏。它们不是你自己定义的,是编译器提前给你准备好的。说白了,就是编译器在编译时自动填入的一些信息。你只管用,不用管它们从哪来。

1. __FILE____LINE__:调试的左右手

__FILE__展开后是一个字符串常量,表示当前源文件的文件名。__LINE__是一个整数常量,表示当前代码所在的行号。这两个宏在调试时特别好用。

我个人习惯在断言宏里用它们。比如这样:

#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            printf("断言失败: %s, 文件: %s, 行号: %d\n", \
                   #cond, __FILE__, __LINE__); \
            while(1); \
        } \
    } while(0)

我在项目中遇到过好几次这样的情况:程序跑飞了,但不知道在哪飞的。加上这个断言宏,一旦条件不满足,立刻打印出文件名和行号。你想想看,是不是比盲猜强多了?

重要提示:__LINE__的值是动态的。你在文件第10行写__LINE__,它就是10;在第100行写,它就是100。别指望它是个固定值。

2. __DATE____TIME__:给代码打上时间戳

__DATE__展开为编译日期,格式是"Mmm dd yyyy"(比如"Feb 18 2025")。__TIME__展开为编译时间,格式是"hh:mm:ss"。

这两个宏有什么用?我告诉你,在嵌入式固件里特别有用。你给客户发了一个固件,客户说有问题,你第一反应是:他刷的是哪个版本的固件?什么时候编译的?

我以前的做法是在程序启动时打印出来:

printf("固件版本: %s %s\n", __DATE__, __TIME__);

这样,客户只要把串口日志发过来,我一眼就能看出他用的固件是不是最新的。省去了很多扯皮的时间。

小技巧:如果你用Git管理代码,可以把__DATE____TIME__跟Git的commit hash结合起来,做成一个更完整的版本字符串。我个人习惯在Makefile里自动生成一个版本头文件。

3. __STDC__:检查编译器是否遵循ANSI C标准

__STDC__这个宏有点意思。如果编译器遵循ANSI C标准,它就定义为1;否则未定义。说白了,就是用来判断当前编译器是不是“正经”的C编译器。

我在跨平台开发时经常用它。比如有些嵌入式编译器对C标准的支持不完整,我就用这个宏来做条件编译:

#ifdef __STDC__
    /* 标准C编译器,放心使用ANSI C特性 */
    inline int max(int a, int b) { return a > b ? a : b; }
#else
    /* 非标准编译器,用宏代替 */
    #define max(a, b) ((a) > (b) ? (a) : (b))
#endif

嗯,这里要注意:有些编译器虽然定义了__STDC__,但值为0,表示不完全符合标准。所以更严谨的写法是检查__STDC__ == 1

避坑指南:我曾经在一个老旧的8051编译器上吃过亏。它定义了__STDC__,但实际不支持inline关键字。从那以后,我学会了不仅要检查__STDC__,还要结合编译器特定的宏(比如__STDC_VERSION__)来做判断。

4. 知识体系总览

下面这张图把今天讲的几个预定义宏的关系梳理了一下。你一看就明白:

C标准预定义宏 __FILE__ 当前源文件名 __LINE__ 当前行号 __DATE__ 编译日期 __TIME__ 编译时间 __STDC__ ANSI C标准检查 调试断言 日志定位 错误报告 行号追踪 固件版本标识 构建时间记录 编译时间戳 性能基准 跨平台兼容 条件编译 核心价值:调试 + 版本管理 + 兼容性检查 编译时信息 → 运行时可见

5. 综合示例:一个实用的调试头文件

最后,我把自己常用的一个调试头文件分享给你。它把上面几个宏都用上了:

#ifndef __DEBUG_H__
#define __DEBUG_H__

#include <stdio.h>

/* 调试打印宏:自动带上文件名和行号 */
#define DEBUG_PRINT(fmt, ...) \
    printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

/* 断言宏:失败时打印详细信息 */
#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "ASSERT FAILED: %s\n", #cond); \
            fprintf(stderr, "  File: %s\n", __FILE__); \
            fprintf(stderr, "  Line: %d\n", __LINE__); \
            fprintf(stderr, "  Date: %s\n", __DATE__); \
            fprintf(stderr, "  Time: %s\n", __TIME__); \
            while(1); \
        } \
    } while(0)

/* 版本信息 */
#define VERSION_STRING "Build: " __DATE__ " " __TIME__

#endif /* __DEBUG_H__ */

你看,一个头文件就把__FILE____LINE____DATE____TIME__全用上了。实际项目中,我还会加上__FUNCTION__(GCC扩展)来打印函数名。不过那是后话了。

总结一下:预定义宏是编译器给你的免费礼物。不用白不用。尤其是__FILE____LINE__,在调试嵌入式程序时简直是救命稻草。我建议你在每个项目里都加上类似的调试宏,养成习惯。别等到出了问题才想起来用。


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