__file__和__line__是编译期展开的预处理器宏,零开销、全编译器支持;需作为参数传入函数才能记录调用点位置,封装时须用宏而非内联函数,c++20可用std::source_location但有运行时开销和兼容性限制。

用 __FILE__ 和 __LINE__ 是最轻量、最可靠的方式
它们不是函数,是预处理器宏,在编译期就展开成字符串字面量和整数字面量,零运行时开销,所有标准 C++ 编译器都支持。
常见错误是误以为它们能“动态获取”——比如在封装函数里直接写 __FILE__,结果得到的是封装函数所在文件的路径,而不是调用处的文件名。
- 想记录调用点信息?必须把
__FILE__和__LINE__作为参数传进去,不能在函数内部硬编码 -
__FILE__值依赖编译器行为:GCC/Clang 默认输出相对路径(从当前工作目录算起),MSVC 默认输出绝对路径;可用-fmacro-prefix-map(GCC/Clang)或/FC(MSVC)统一规范 - 如果日志要跨平台且路径整洁,建议用
std::filesystem::path(__FILE__).filename().string()提取文件名,但注意 C++17 起才稳定支持
封装成带位置信息的日志宏,避免手动传参出错
手写 log("msg", __FILE__, __LINE__) 很容易漏掉参数或顺序错。宏才是正确解法——它在调用点展开,天然捕获上下文。
典型错误:用内联函数替代宏,结果又掉回“宏展开位置丢失”的坑里。
立即学习“C++免费学习笔记(深入)”;
- 基础安全写法:
#define LOG(msg) do { \ fprintf(stderr, "[%s:%d] %s\n", __FILE__, __LINE__, msg); \ } while(0) - 想支持格式化?C++20 前只能靠可变参数宏:
#define LOGF(fmt, ...) do { \ fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__); \ } while(0) - 注意
__VA_ARGS__在 GCC/Clang 中允许空参数(用##__VA_ARGS__消除逗号),MSVC 需 /Zc:preprocessor 启用 C99 风格宏
调试时行号突然跳变?检查是否启用了编译器优化或宏展开
看到日志里 __LINE__ 显示 123,但实际代码在 456 行——大概率是宏嵌套或头文件包含干扰了行号计数。
更隐蔽的问题:启用 -O2 后,某些调试宏被优化掉(尤其没副作用的空宏),或内联导致日志输出位置偏移。
- 调试阶段务必加
-g -O0,禁用优化并保留调试信息 - 用
#line 100 "fake.cpp"手动重置预处理器行号,可用于测试宏行为,但别留在生产代码里 - IDE 断点失效?检查是否在宏定义行打的断点——实际断点应落在宏展开后的位置,部分编辑器不自动跳转
C++20 的 std::source_location 更类型安全,但别盲目替换
它确实优雅:std::source_location::current() 返回一个结构体,含 file_name()、line() 等成员,还能传给函数参数,类型明确、可拷贝、可调试。
但问题在于:它不是编译期常量,有极小运行时开销(通常一个 const char* + 两个 int),且 GCC 10+、Clang 11+、MSVC 19.29+ 才完整支持。
- 函数签名示例:
void log(const char* msg, const std::source_location& loc = std::source_location::current());
- 和宏方案不互斥:可以共存,比如调试用
std::source_location,发布版切回宏(通过宏开关控制) - 注意:
std::source_location::current()必须在调用点求值,若保存到对象里再延迟使用,拿到的是构造时的位置,不是日志触发时的位置
真正难的不是选哪个机制,而是统一团队对“日志位置该精确到哪一级”的认知——是调用点、封装函数入口,还是实际出错语句?一旦混用宏和 source_location,又没约定好语义,查 bug 时反而更迷。









