C++ 为什么当缺少宽度参数时,fprintf会导致内存泄漏和行为不可预测

C++ 为什么当缺少宽度参数时,fprintf会导致内存泄漏和行为不可预测,c++,c,memory-leaks,printf,C++,C,Memory Leaks,Printf,以下简单程序的行为不可预测。有时它打印“0.00000”,有时它打印的“0”比我能数的还要多。有时,它会在系统终止某个进程或因错误分配而失败之前耗尽系统上的所有内存 #include "stdio.h" int main() { fprintf(stdout, "%.*f", 0.0); } 我知道这是fprintf的错误用法。应该有另一个参数指定格式的宽度。令人惊讶的是,这种行为如此不可预测。有时它似乎使用默认宽度,而有时它失败得非常严重。难道不能让它总是失败或总是使用一些默认行为吗

以下简单程序的行为不可预测。有时它打印“0.00000”,有时它打印的“0”比我能数的还要多。有时,它会在系统终止某个进程或因错误分配而失败之前耗尽系统上的所有内存

#include "stdio.h"

int main() {
  fprintf(stdout, "%.*f", 0.0);
}
我知道这是fprintf的错误用法。应该有另一个参数指定格式的宽度。令人惊讶的是,这种行为如此不可预测。有时它似乎使用默认宽度,而有时它失败得非常严重。难道不能让它总是失败或总是使用一些默认行为吗

我在工作中的一些代码中发现了类似的用法,并花了很多时间弄清楚发生了什么。这似乎只在调试构建时发生,但在使用gdb调试时不会发生。另一个令人好奇的是,在valgrind中运行它会一直导致许多“0”案例的打印,否则这种情况很少发生,但内存使用问题也不会发生


我正在运行Red Hat Enterprise Linux 7,并使用gcc 4.8.5编译。

格式字符串与参数不匹配,因此,
fprintf
的行为未定义。谷歌“undefined Behavior C”获取更多关于“undefined bahaviour”的信息

这是正确的:

// printf 0.0 with 7 decimals
fprintf(stdout, "%.*f", 7, 0.0);
或者你只是想要这个:

// printf 0.0 with de default format
fprintf(stdout, "%f", 0.0);

关于问题的这一部分:有时它似乎使用默认宽度,而有时它失败得非常严重。难道不能让它总是失败或总是使用一些默认行为吗

不能有任何默认行为,
fprintf
正在根据格式字符串读取参数。如果参数不匹配,
fprintf
将以非常随机的值结束


关于问题的这一部分:另一个好奇的是,在valgrind中运行它会始终打印出许多“0”的大小写,否则这种情况很少发生,但内存使用问题也永远不会发生


这只是未定义行为的另一种表现形式,使用valgrind时,条件完全不同,因此实际的未定义行为可能不同。

正式地说,这是未定义行为

至于你在实践中观察到的:
我猜,
fprintf
最终使用未初始化的整数作为输出的小数位数。这是因为它将尝试从调用者没有写入任何特定值的位置读取一个数字,因此您将只获得存储在那里的任何比特。如果这恰好是一个巨大的数字,
fprintf
将尝试分配大量内存以在内部存储结果字符串。这就解释了“内存不足”的部分

如果未初始化的值没有那么大,那么分配将成功,最终将得到大量的零

最后,如果随机整数值恰好是
5
,则得到
0.00000

Valgrind可能会始终如一地初始化程序所看到的内存,因此行为变得具有确定性

难道这不能永远失败吗


我敢肯定,如果您使用
gcc-pedantic-Wall-Wextra-Werror

这是标准中未定义的行为,它甚至不会编译。它的意思是“任何事情都是公平的”,因为你做了错事

最糟糕的是,任何编译器都会警告您,但您忽略了警告。在编译器之外进行某种验证会产生成本,每个人都会为此付出代价,这样你就可以做错事了

这与C和C++所代表的相反:你为你所用的东西付出代价。如果你想付这笔费用,那就由你来检查了

真正发生的事情取决于ABI、编译器和体系结构。这是未定义的行为,因为该语言让实现者可以自由地在每台机器上做更好的事情(这意味着,有时代码更快,有时代码更短)

例如,当你在机器上调用一个函数时,它只意味着你在指示微处理器去某个代码位置

在某些组合程序集和ABI中,则,
printf(“%f*”,5,1)将转换为

mov A, STR_F ; // load into register A the 32 bit address of the string "%.*f"
mov B, 5 ; // load second 32 bit parameter into B 
mov F0, 1.0 ; // load first floating point parameter into register F0
call printf ; // call the function
现在,如果您遗漏了一些参数,在本例中是B,它将接受之前存在的任何值


printf
这样的函数允许参数列表中的任何内容(它是
printf(const char*,…)
,所以任何内容都是有效的)。这就是为什么不应该在C++上使用
printf
:您有更好的选择,比如流
printf
避免检查编译器。流可以更好地了解类型,并且可以扩展到您自己的类型。此外,这也是为什么您的代码应该在没有警告的情况下编译。

未定义的行为是未定义的。

然而,在x86-64 System-V ABI上,众所周知,参数不是在堆栈上传递的,而是在寄存器中传递的。浮点变量在浮点寄存器中传递,整数在通用寄存器中传递。堆栈上没有参数存储,所以参数的宽度无关紧要。由于您从未在变量参数部分传递过任何整数,因此与第一个参数对应的通用寄存器将包含它以前的任何垃圾

该程序将显示浮点值和整数是如何分别传递的:

#include <stdio.h>

int main() {
    fprintf(stdout, "%.*f\n", 42, 0.0);
    fprintf(stdout, "%.*f\n", 0.0, 42);
}

当您有意触发未定义的行为时,您应该查看反汇编。@user202729祝您好运,在反汇编的代码中调查
fprintf
的内部工作。未定义的行为是未定义的。事实上,在一场灾难中,它总是会失败的
0.000000000000000000000000000000000000000000
0.000000000000000000000000000000000000000000