C语言预定义宏(__FILE__、__LINE__、__func__等)详解与工程实践

📅 2026/8/5 4:38:17
C语言预定义宏(__FILE__、__LINE__、__func__等)详解与工程实践
1. 项目概述为什么我们需要这些“内置”的调试助手如果你写过C语言尤其是在调试一个复杂程序时肯定遇到过这样的场景程序在某个地方崩溃了日志里只留下一句“Error occurred!”然后你就得开始满世界找这个错误到底发生在哪个文件、哪一行。那种感觉就像在一片漆黑的森林里找一根掉落的针。这时候C语言提供的一组预定义宏Predefined Macros就成了你手电筒和指南针。它们不是标准库函数而是编译器在编译时就帮你“填好”的常量能直接告诉你当前代码执行到了哪个函数、哪一行、哪个文件甚至编译的时间。今天要聊的这六个宏__func__、__FUNCTION__、__LINE__、__FILE__、__DATE__、__TIME__就是C语言程序员特别是做嵌入式、系统底层开发和大型项目调试的老手们工具箱里的常客。它们看起来简单但用好了能极大提升代码的可维护性和调试效率。很多人可能只在printf调试法里用过__LINE__和__FILE__但对它们的细节、区别以及更高级的用法一知半解。这篇文章我就结合自己十多年摸爬滚打的经验把这几个宏从里到外掰开揉碎了讲清楚让你不仅知道怎么用更明白为什么要这么用以及如何避开那些常见的坑。2. 核心六兄弟功能、标准与本质剖析这六个宏可以分为三组标识代码位置的、标识编译信息的以及那个有点特殊的“函数名”组。我们先来逐个认识它们理解它们能提供什么信息以及背后的标准支持情况。2.1 位置追踪组__FILE__与__LINE__这是最常用、也是最古老的一对宏早在ANSI C标准C89/C90中就被定义了。__FILE__ 它会展开为一个字符串常量内容就是当前源文件的文件名。注意它展开的是你在编译命令中指定的源文件路径。如果你用的是相对路径编译比如gcc ./src/main.c那么__FILE__可能就是main.c。如果用绝对路径它就会是完整的路径。这个宏在追踪错误来源时至关重要。__LINE__ 它会展开为一个整型常量表示这个宏在源代码中出现的行号。行号是从文件开头计数的。它和__FILE__是天生的搭档通常一起使用来精确定位。它们的本质是编译器在预处理阶段进行的“文本替换”。当编译器看到__LINE__时它不是去运行时计算而是直接把这个宏出现的位置的行号比如第42行当作一个数字常量写进代码里。__FILE__同理被替换成对应的字符串常量。所以它们反映的是编译时的静态位置信息而不是程序运行时的动态调用栈。一个简单的例子#include stdio.h int main() { printf(当前文件%s\n, __FILE__); printf(当前行号%d\n, __LINE__); printf(上一行是第%d行这一行是第%d行\n, __LINE__-1, __LINE__); return 0; }输出会类似于当前文件test.c 当前行号5 上一行是第4行这一行是第5行注意第三行两个__LINE__宏因为出现在同一行所以它们展开的值是相同的。这再次证明了它是编译时的静态替换。2.2 编译信息组__DATE__与__TIME__这两个宏同样属于ANSI C标准提供的是编译时刻的信息。__DATE__ 展开为一个形如Mmm dd yyyy的字符串常量。其中Mmm是月份的英文缩写如Jan, Febdd是日期如果是个位数前面会补空格例如 7yyyy是年份。例如May 7 2023。__TIME__ 展开为一个形如hh:mm:ss的字符串常量表示编译开始的时刻24小时制。例如14:30:55。它们的核心用途在于版本标识和构建追踪。你可以把这两个信息编译进你的程序版本字符串中这样当用户报告问题时你能立刻知道他们运行的是哪个时间点构建的版本对于排查是否修复了某个特定提交的问题非常有帮助。#include stdio.h int main() { printf(程序构建于%s %s\n, __DATE__, __TIME__); return 0; }每次编译这个输出都会更新。这在嵌入式固件、库文件.a/.so中尤其有用可以快速确认当前运行的二进制文件是否是你预期的那一个。注意__DATE__和__TIME__记录的是编译开始的时间而不是源代码修改时间或程序运行时间。如果你的编译过程很长如大型项目这个时间可能和文件修改时间有细微差别。另外它们依赖于编译主机的系统时间如果主机时间不准这个信息也就没有参考价值了。2.3 函数名组__func__与__FUNCTION__以及__PRETTY_FUNCTION__这一组用于获取当前所在的函数名但在标准支持上有重要区别也是容易混淆的地方。__func__(C99标准) 这是C语言标准自C99起中正式定义的标识符。注意标准中称它为“预定义标识符”(predefined identifier)而不是“宏”。但在使用体验上它和宏很像。在任何一个函数体内__func__就像一个隐式声明的static const char[]数组其内容为该函数的名称。这是最推荐、最可移植的用法。__FUNCTION__(非标准但广泛支持) 这是一个许多编译器如GCC, Clang, MSVC等提供的扩展宏功能与__func__完全相同。在C99之前它是获取函数名的主要方式。即使在今天为了兼容不支持C99的旧编译器或某些特殊环境你可能会看到它和__func__一起被使用通过条件编译。__PRETTY_FUNCTION__(GCC/Clang扩展) 这是GCC和Clang特有的扩展。在C语言中它通常和__FUNCTION__一样。但在C中它会输出包含参数类型、类名等更详细信息的“漂亮”函数签名这对于调试模板或重载函数非常有用。在纯C项目中我们一般用不到它。关键区别与选择建议可移植性如果你明确要求代码符合C99或更新标准且不需要在古董编译器上运行只使用__func__。兼容性如果你的代码可能需要在不完全支持C99的旧环境比如一些严格的嵌入式编译器下编译可以考虑使用__FUNCTION__或者通过条件编译来同时支持#ifdef __STDC_VERSION__ #define MY_FUNC __func__ #else #define MY_FUNC __FUNCTION__ #endif本质__func__是一个标识符而__FUNCTION__和__PRETTY_FUNCTION__是宏。但在实际输出字符串内容上在C语言中它们没有区别。#include stdio.h void my_function() { printf(函数名(__func__): %s\n, __func__); printf(函数名(__FUNCTION__): %s\n, __FUNCTION__); // printf(漂亮名(__PRETTY_FUNCTION__): %s\n, __PRETTY_FUNCTION__); // 在C中通常与__FUNCTION__相同 } int main() { my_function(); return 0; }输出函数名(__func__): my_function 函数名(__FUNCTION__): my_function3. 实战应用场景从基础调试到高级技巧知道了它们是什么接下来看看在真实项目中怎么用。这些宏的价值远不止于简单的printf。3.1 基础应用增强日志与断言Assert这是最直接的用途。想象一下你的日志从“Error: value out of range”变成了“[ERROR][main.c:25][process_data] Value out of range: 1024”排查效率的提升是指数级的。一个增强版的日志宏示例#include stdio.h #include time.h // 简单的日志宏包含时间、文件、行、函数 #define LOG_INFO(fmt, ...) \ do { \ time_t t time(NULL); \ struct tm *ltm localtime(t); \ printf([%04d-%02d-%02d %02d:%02d:%02d][INFO][%s:%d][%s] fmt \n, \ ltm-tm_year1900, ltm-tm_mon1, ltm-tm_mday, \ ltm-tm_hour, ltm-tm_min, ltm-tm_sec, \ __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ } while(0) void process_data(int val) { if (val 0) { LOG_INFO(Invalid data received: %d, val); return; } // ... 处理数据 LOG_INFO(Processing value %d, val); } int main() { process_data(-5); process_data(42); return 0; }这个LOG_INFO宏集成了运行时间、源代码位置和函数名输出信息非常全面。do { ... } while(0)是定义多语句宏的常用技巧能确保宏在使用时比如放在if语句后面不带花括号依然行为正确。构建自定义断言Assert标准库的assert在失败时只输出表达式、文件名和行号。我们可以做得更好#include stdio.h #include stdlib.h #define MY_ASSERT(expr) \ do { \ if (!(expr)) { \ fprintf(stderr, [ASSERT FAILED] %s:%d (%s) - %s\n, \ __FILE__, __LINE__, __func__, #expr); \ abort(); /* 或执行其他错误处理 */ \ } \ } while(0) void risky_operation(int* ptr) { MY_ASSERT(ptr ! NULL Pointer must not be NULL); // ... 安全地使用ptr } int main() { risky_operation(NULL); // 这将触发断言 return 0; }这个自定义断言不仅指出了位置还把断言表达式本身#expr将表达式字符串化和函数名都打印了出来甚至允许你添加自定义的错误信息如Pointer must not be NULL调试起来一目了然。3.2 中级应用错误码统一管理与调试函数在大型模块化项目中我们经常需要将错误信息传递到上层。单纯返回一个错误码如-1往往信息量不足。结合这些宏我们可以创建一个丰富的错误上下文结构体。// error_ctx.h #ifndef ERROR_CTX_H #define ERROR_CTX_H typedef struct { int code; // 错误码 const char* file; // 出错文件 int line; // 出错行号 const char* func; // 出错函数 const char* msg; // 错误描述信息 } error_ctx_t; // 创建一个错误上下文的辅助宏 #define CREATE_ERROR_CTX(code, message) \ { (code), __FILE__, __LINE__, __func__, (message) } // 打印错误上下文 void print_error(const error_ctx_t* err); #endif// error_ctx.c #include error_ctx.h #include stdio.h void print_error(const error_ctx_t* err) { if (err) { fprintf(stderr, 错误[%d] %s:%d (函数:%s): %s\n, err-code, err-file, err-line, err-func, err-msg); } }// network_module.c #include error_ctx.h error_ctx_t connect_to_server(const char* addr) { if (addr NULL) { return CREATE_ERROR_CTX(-1, 服务器地址不能为NULL); } // 模拟连接失败 int simulated_error 1; if (simulated_error) { return CREATE_ERROR_CTX(-100, 网络连接超时); } return CREATE_ERROR_CTX(0, 成功); // code 0 表示成功 } int main() { error_ctx_t err connect_to_server(NULL); if (err.code ! 0) { print_error(err); } err connect_to_server(192.168.1.1); if (err.code ! 0) { print_error(err); } return 0; }这样任何函数在出错时都能方便地携带完整的“案发现场”信息返回上层处理错误时能获得远超一个简单错误码的洞察力。3.3 高级技巧编译期字符串拼接与唯一标识生成预定义宏是预处理阶段处理的因此可以用于一些编译期的技巧。编译期生成唯一标识符结合__LINE__和__COUNTER__另一个扩展宏每次展开值递增可以生成唯一的名称常用于创建局部变量或类型定义避免名称冲突。// 利用行号生成一个大概率唯一的名称在同一个文件内同一行不会重复 #define UNIQUE_VAR(base) base##__LINE__ // 使用 int UNIQUE_VAR(temp) 0; // 展开为 int temp__42 0; (假设在42行)编译期构建版本字符串将__DATE__、__TIME__和自定义版本号拼接成一个完整的版本字符串。#define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) #define VERSION_MAJOR 1 #define VERSION_MINOR 2 #define VERSION_PATCH 3 // 方法1直接拼接字符串字面量会自动连接 const char version_str1[] App v TOSTRING(VERSION_MAJOR) . TOSTRING(VERSION_MINOR) . TOSTRING(VERSION_PATCH) (Build: __DATE__ __TIME__ ); // 方法2使用宏函数更灵活 #define MAKE_VERSION_STRING(maj, min, pat) \ v TOSTRING(maj) . TOSTRING(min) . TOSTRING(pat) \ (Compiled on __DATE__ at __TIME__ ) const char version_str2[] MAKE_VERSION_STRING(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); #include stdio.h int main() { printf(%s\n, version_str1); printf(%s\n, version_str2); return 0; }注意STRINGIFY和TOSTRING两层宏的用法这是为了将宏参数如VERSION_MAJOR这个数字1先完全展开再转换成字符串1。直接#VERSION_MAJOR会得到字符串VERSION_MAJOR而不是1。4. 深入原理、边界条件与常见“坑点”会用只是第一步理解其原理和边界才能避免踩坑。4.1 预定义宏的本质预处理阶段的文本替换这是最重要的概念。编译器的工作流程大致是预处理 - 编译 - 汇编 - 链接。预定义宏的处理发生在预处理阶段。预处理器会扫描源代码将所有宏包括这些预定义宏展开生成一个纯粹的、没有宏的“翻译单元”.i文件。__LINE__被替换成数字__FILE__被替换成字符串__func__被当作一个静态数组声明插入到函数开头。这意味着它们没有运行时开销展开后就是常量和直接写数字、字符串没有区别。它们不能动态改变你不能写一个函数来返回调用它的函数名因为__func__在预处理时就固定为它所在函数的名称了。在宏内部使用时要小心如果你把__LINE__用在另一个宏的定义里它展开的位置可能和你预期的不一样。#define GET_LINE() __LINE__ int main() { int line1 __LINE__; // 这行是第5行所以 line1 5 int line2 GET_LINE(); // GET_LINE()在预处理时展开为__LINE__而__LINE__出现在第7行所以 line2 7 printf(line1%d, line2%d\n, line1, line2); return 0; }4.2__func__的特殊性作用域与存储__func__被隐式声明为static const char __func__[] function_name;这意味着它是静态的static其作用域仅限于它所在的函数体内。你不能在函数外部引用它。它是常量数组你不能修改其内容尝试修改会导致未定义行为。每个函数都有自己的__func__它是一个局部变量而不是全局变量。4.3 跨文件与头文件中的行为这是一个容易出错的地方。考虑一个头文件utils.h// utils.h #ifndef UTILS_H #define UTILS_H void log_debug(const char* msg); #endif// utils.c #include stdio.h #include utils.h void log_debug(const char* msg) { // 这里的 __FILE__ 是 utils.c // 这里的 __func__ 是 log_debug printf([%s:%d %s] %s\n, __FILE__, __LINE__, __func__, msg); }// main.c #include utils.h int main() { // 调用时我们希望日志能反映 main.c 中调用的位置 log_debug(Starting app); // 但输出会是 [utils.c:xx log_debug] Starting app return 0; }你会发现日志输出的文件名和函数名是log_debug函数本身的位置而不是调用者main的位置。这是因为宏在预处理时是在每个源文件.c中独立展开的。__FILE__和__LINE__在utils.c的log_debug函数体内被展开所以它们记录的是utils.c的信息。如何解决你需要将调用点的信息作为参数传递给日志函数。这正是我们之前定义LOG_INFO宏时所做的事情——宏在调用点展开__FILE__和__LINE__获取的是调用点的位置然后将这些值作为参数传给真正的日志处理函数。4.4 性能与资源考量虽然这些宏展开后是常量几乎没有性能开销但在某些极端资源受限的嵌入式环境单片机仍需注意字符串存储__FILE__、__DATE__、__TIME__和__func__都是字符串常量它们会占用程序的只读数据段通常是Flash/ROM。如果每个函数都使用一个包含长路径__FILE__的日志可能会显著增加二进制文件大小。在发布版本中可以考虑通过条件编译移除这些调试信息。#ifdef DEBUG #define LOG(fmt, ...) printf([%s:%d] fmt, __FILE__, __LINE__, ##__VA_ARGS__) #else #define LOG(fmt, ...) // 定义为空在Release版本中消除所有日志开销 #endif__FILE__的路径长度使用相对路径编译可以减少字符串长度。构建系统如CMake, Makefile可以控制传递给编译器的源文件路径格式。4.5 编译器差异与可移植性实践不同编译器对某些宏的支持有细微差别__FUNCTION__vs__func__如前所述坚持用__func__C99是最佳实践。如果必须兼容旧环境使用条件编译。__LINE__的类型标准规定它展开为整数常量但具体是int还是long等由编译器决定。在格式化输出时使用%d通常是安全的但如果要将其用于需要特定大小整型的场合如序列化最好进行强制类型转换。__STDC_VERSION__宏你可以用这个宏来判断编译器支持的C标准版本从而决定使用哪些特性。#if defined(__STDC_VERSION__) __STDC_VERSION__ 199901L // C99或更新环境使用 __func__ #define CURRENT_FUNCTION __func__ #elif defined(__GNUC__) || defined(_MSC_VER) // GCC或MSVC环境使用它们的扩展 #define CURRENT_FUNCTION __FUNCTION__ #else // 其他编译器可能不支持回退方案 #define CURRENT_FUNCTION (unknown) #endif5. 工程实践构建一个简易的日志系统理论说再多不如一个实际例子。我们来设计一个用于小型C项目的简易日志系统它综合运用了上述所有宏并考虑了级别控制、输出控制等实用功能。// simple_logger.h #ifndef SIMPLE_LOGGER_H #define SIMPLE_LOGGER_H // 日志级别 typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR, LOG_LEVEL_FATAL } log_level_t; // 设置全局日志级别和输出流 void log_set_level(log_level_t level); void log_set_stream(FILE *stream); // 默认为 stderr // 核心日志宏 #define LOG(level, fmt, ...) \ do { \ if (level g_current_log_level) { \ _log_write(level, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__); \ } \ } while(0) // 便捷宏 #define LOG_DEBUG(fmt, ...) LOG(LOG_LEVEL_DEBUG, fmt, ##__VA_ARGS__) #define LOG_INFO(fmt, ...) LOG(LOG_LEVEL_INFO, fmt, ##__VA_ARGS__) #define LOG_WARN(fmt, ...) LOG(LOG_LEVEL_WARN, fmt, ##__VA_ARGS__) #define LOG_ERROR(fmt, ...) LOG(LOG_LEVEL_ERROR, fmt, ##__VA_ARGS__) #define LOG_FATAL(fmt, ...) LOG(LOG_LEVEL_FATAL, fmt, ##__VA_ARGS__) // 内部实现函数用户不直接调用 void _log_write(log_level_t level, const char* file, int line, const char* func, const char* fmt, ...); #endif // SIMPLE_LOGGER_H// simple_logger.c #include simple_logger.h #include stdio.h #include stdlib.h #include stdarg.h #include time.h // 全局状态 static log_level_t g_current_log_level LOG_LEVEL_INFO; static FILE* g_log_stream NULL; static const char* level_strings[] { DEBUG, INFO, WARN, ERROR, FATAL }; static const char* level_colors[] { \x1b[36m, // DEBUG: 青色 \x1b[32m, // INFO: 绿色 \x1b[33m, // WARN: 黄色 \x1b[31m, // ERROR: 红色 \x1b[35m, // FATAL: 品红 }; void log_set_level(log_level_t level) { g_current_log_level level; } void log_set_stream(FILE *stream) { g_log_stream stream; } void _log_write(log_level_t level, const char* file, int line, const char* func, const char* fmt, ...) { if (g_log_stream NULL) { g_log_stream stderr; // 默认输出到标准错误 } time_t t time(NULL); struct tm *ltm localtime(t); char time_buf[20]; strftime(time_buf, sizeof(time_buf), %Y-%m-%d %H:%M:%S, ltm); // 获取文件名不含路径简化输出 const char* base_file file; const char* slash_pos strrchr(file, /); #ifdef _WIN32 const char* backslash_pos strrchr(file, \\); slash_pos (slash_pos backslash_pos) ? (slash_pos backslash_pos ? slash_pos : backslash_pos) : (slash_pos ? slash_pos : backslash_pos); #endif if (slash_pos) { base_file slash_pos 1; } // 输出带颜色的日志头 [时间 级别 文件:行 函数] fprintf(g_log_stream, %s[%s %-5s %s:%d %s] , level_colors[level], time_buf, level_strings[level], base_file, line, func); // 输出用户日志内容 va_list args; va_start(args, fmt); vfprintf(g_log_stream, fmt, args); va_end(args); // 重置颜色并换行 fprintf(g_log_stream, \x1b[0m\n); // 如果是FATAL级别可以选择终止程序 if (level LOG_LEVEL_FATAL) { fflush(g_log_stream); abort(); } }// main.c - 使用示例 #include simple_logger.h #include stdio.h void deep_function(int val) { LOG_DEBUG(进入深度函数参数 val%d, val); if (val 0) { LOG_ERROR(参数 val (%d) 不能为负数, val); // 可能进行错误处理 } LOG_DEBUG(离开深度函数); } int main() { // 设置只输出WARN及以上级别的日志 log_set_level(LOG_LEVEL_WARN); // 也可以输出到文件 // FILE* log_file fopen(app.log, a); // log_set_stream(log_file); LOG_INFO(这条INFO日志不会被打印因为级别是WARN); // 这行不会输出 LOG_WARN(这是一个警告信息); LOG_ERROR(这是一个错误信息); // 临时调整级别以调试某个函数 log_set_level(LOG_LEVEL_DEBUG); deep_function(-5); deep_function(10); log_set_level(LOG_LEVEL_WARN); LOG_INFO(程序结束); // 这行不会输出 return 0; }这个日志系统展示了如何将预定义宏用于实际工程宏封装用户使用简单的LOG_DEBUG()等宏自动注入__FILE__、__LINE__、__func__。级别过滤通过全局级别变量在编译期宏判断决定是否调用底层输出函数减少不必要的函数调用和字符串格式化开销。输出定制可以控制输出到控制台带颜色或文件并简化了文件名输出只取基名。线程安全注意这个简单实现不是线程安全的。在生产环境中对全局变量g_log_stream和g_current_log_level的访问以及_log_write函数内部的vfprintf调用可能需要加锁。6. 对比、选择与替代方案6.1__func__与__FUNCTION__的最终抉择对于新项目我的建议非常明确只使用__func__。原因如下符合标准__func__是C99标准的一部分使用它意味着你的代码在现代编译器上具有最好的可移植性。简洁一致少一个下划线拼写更简单且与其他标准特性保持一致。未来保证所有维护和更新中的编译器都会支持它。唯一需要使用__FUNCTION__的场景是你的项目必须兼容一个明确不支持C99且**只提供__FUNCTION__**的古老编译器。这种情况在现代开发中已经非常罕见。如果你不确定可以在代码中添加一个静态断言或编译检查// 检查编译器是否声称支持C99 #if defined(__STDC_VERSION__) __STDC_VERSION__ 199901L // 安心使用 __func__ #else #warning Compiler may not fully support C99. __func__ might not be available. // 考虑回退方案 #endif6.2 预定义宏 vs 运行时栈回溯预定义宏提供的是编译时确定的静态位置信息。对于需要运行时动态调用链的场景它们就力不从心了。比如你想在程序崩溃时打印出完整的函数调用栈backtrace就需要使用平台相关的运行时库Linux/Unix 可以使用backtrace()和backtrace_symbols()函数在execinfo.h中或者通过信号处理程序获取。Windows 可以使用CaptureStackBackTrace()和SymFromAddr()等Win32 API。嵌入式系统 可能没有完整的栈回溯支持需要手动分析栈帧或依赖硬件调试模块如ARM的Cortex-M的FPU或DWT单元。运行时栈回溯能告诉你函数是如何一层层调用过来的但通常无法直接获取参数值并且函数名可能是经过修饰的mangled尤其在C中。这时预定义宏提供的静态信息文件名、行号可以作为有力的补充。一个常见的模式是在错误捕获点如断言失败、信号处理器同时记录__FILE__和__LINE__并尝试打印调用栈。6.3 在C中的注意事项C完全兼容C语言的这些预定义宏和标识符。此外__func__在C11中成为标准。__FUNCTION__和__PRETTY_FUNCTION__在GCC/Clang中同样可用且__PRETTY_FUNCTION__在C中会输出包含参数类型的完整函数签名非常强大。在类成员函数中__func__会输出成员函数名如MyClass::myMethod。C中还有__cplusplus宏可以用来判断编译环境是C还是C以便进行条件编译。7. 总结与个人经验之谈回顾这六个预定义宏它们就像是嵌入在C语言语法中的“元数据”通道为程序提供了自省introspection的基础能力。从简单的调试打印到复杂的日志系统、错误追踪框架它们都是不可或缺的基石。在我多年的开发经历中尤其是做嵌入式系统和性能分析工具时对它们的使用有几点深刻的体会第一日志格式要尽早统一。在项目初期就定义一个像第5节那样的日志宏并强制团队使用。这比后期在成千上万行代码中统一添加要轻松得多。格式里至少包含[时间 级别 文件:行]__func__视情况添加在小型函数中可能冗余在大型函数中很有用。第二Release版本一定要剥离调试信息。通过条件编译如#ifdef DEBUG将包含__FILE__、__LINE__的详细日志完全移除。这些字符串常量会增大二进制体积而且日志输出本身也可能影响性能。只保留最关键的错误日志。第三小心宏的展开点。这是最容易出bug的地方之一。当你编写一个使用__LINE__的宏时一定要在脑子里预演一遍预处理器的展开过程。复杂的多层宏最好写完后用编译器的-E选项GCC/Clang生成预处理后的文件亲自检查一下展开结果是否符合预期。第四__DATE__和__TIME__用于版本标识很棒但不能用于依赖时间的逻辑。我曾经见过有代码用它们来判断试用版是否过期这是完全不可靠的因为编译时间一旦确定就不会再变。程序运行时间应该使用time()等运行时函数获取。最后虽然这些宏很强大但也不要过度使用。在那些对性能极其敏感、或者资源极其有限的代码段比如中断服务程序、高频调用的内联函数频繁的日志输出可能会成为瓶颈。在这些地方可能需要更轻量级的追踪机制或者直接使用硬件调试器如JTAG/SWD来观察状态。把这些宏理解透彻、用对地方它们就能从简单的语法糖变成你提升代码质量、加速开发调试的利器。