Skip to content

问题现象

使用 printfcout 输出同一个变量时,结果不一致。

问题复现

测试代码

cpp
#include <iostream>

int main(int argc, char const *argv[]) {
    double d = 10 * 1.0 / 3;
    printf("(%%ld)d: %ld\n", d);
    printf("(%%ld)(long)d: %ld\n", (long)d);
    return 0;
}

实际输出

(%ld)d: 140722676870488
(%ld)(long)d: 3

预期输出

两行都应该输出 310/3 的整数部分)

问题原因

printf 的实现机制

printf 通过 可变长参数函数(Variable Argument Functions) 实现对任意类型和数量参数的读取。

函数原型

cpp
int printf(const char* format, ...);

内存布局

可变长参数(... 部分)在内存中按顺序首尾相连存储。

示例: printf("test %d %ld", (int)1, (long)2);

内存布局如下:

+----------+-------+----------+
| "test..."| int 1 | long 2   |
+----------+-------+----------+
    ^         ^        ^
  format    4 bytes  8 bytes (64位系统)

核心问题

printf 无法自动识别参数的实际类型和长度,它完全依赖于格式化字符串(%d, %ld, %f 等)来决定:

  • 读取多少字节
  • 如何解释这些字节

类型不匹配的后果

当格式化符号与实际参数类型不匹配时:

问题场景 实际类型 格式符 后果
本例 double (8 字节) %ld 读取 8 字节,但按整数解释,导致数据乱码
轻微错误 int (4 字节) %ld 读取 8 字节,后 4 字节是栈上的垃圾数据
严重错误 long (8 字节) %d 只读取 4 字节,后续参数位置错乱

为什么 cout 没有这个问题?

C++ 的 cout 通过 运算符重载编译期类型推导 实现类型安全:

cpp
double d = 10 * 1.0 / 3;
std::cout << d;  // 编译器知道 d 是 double,自动调用正确的重载函数
  • 编译器在编译期就确定了参数的实际类型
  • 自动调用对应类型的 operator<< 重载函数
  • 不依赖程序员提供的格式字符串

解决方案

方案一:显式类型转换(推荐)

在传递参数前,将变量转换为格式符对应的类型:

cpp
double d = 10 * 1.0 / 3;

// 正确:先转换为 long,再用 %ld 输出
printf("(long)d: %ld\n", (long)d);  // 输出: 3

// 正确:用 %f 输出 double
printf("d: %f\n", d);  // 输出: 3.333333

方案二:使用正确的格式化符号

确保格式符与参数类型严格匹配:

类型 32 位系统 64 位系统 示例
int %d %d printf("%d", 42);
long %ld %ld printf("%ld", 42L);
long long %lld %lld printf("%lld", 42LL);
unsigned long %lu %lu printf("%lu", 42UL);
float / double %f %f printf("%f", 3.14);
size_t %zu %zu printf("%zu", sizeof(int));
void* %p %p printf("%p", ptr);

方案三:使用 C++ 流式输出(最安全)

cpp
double d = 10 * 1.0 / 3;
std::cout << "d: " << d << std::endl;  // 类型安全,自动处理

最佳实践

✅ 推荐做法

  1. C++ 项目优先使用 std::cout:类型安全,更易维护
  2. 必须用 printf 时严格匹配类型
    cpp
    int i = 42;
    long l = 42L;
    double d = 3.14;
    printf("%d %ld %f\n", i, l, d);  // 严格匹配
  3. 使用编译器警告
    bash
    g++ -Wall -Wformat -Wformat-security test.cpp
    GCC 会检查 printf 格式符与参数类型是否匹配

❌ 常见错误

cpp
// 错误:double 用 %ld
double d = 3.14;
printf("%ld\n", d);  // ❌ 输出垃圾值

// 错误:忘记转换
int a = 10, b = 3;
printf("%f\n", a / b);  // ❌ 输出垃圾值(a/b 是 int)

// 正确:先转换类型
printf("%f\n", (double)a / b);  // ✅ 输出 3.333333

🛡️ 跨平台注意事项

不同平台上,基础类型的大小可能不同:

cpp
// ❌ 不可移植
printf("%d\n", sizeof(long));  // 32位: 4, 64位: 8

// ✅ 使用固定宽度类型 (C99/C++11)
#include <stdint.h>
int32_t i32 = 42;
int64_t i64 = 42;
printf("%" PRId32 " %" PRId64 "\n", i32, i64);

调试技巧

使用编译器诊断

bash
# GCC/Clang 启用格式检查
g++ -Wall -Wformat -Wextra test.cpp

# 示例警告输出
warning: format '%ld' expects argument of type 'long int', 
         but argument 2 has type 'double' [-Wformat=]

使用静态分析工具

bash
# cppcheck
cppcheck --enable=warning test.cpp

# clang-tidy
clang-tidy test.cpp -- -std=c++17

参考资料

基于 VitePress 构建