使用 .* 宽度说明符调用 sprintf 时出现奇怪的警告

Strange warning when calling sprintf with .* width specifier

对于以下代码:

https://godbolt.org/z/WcGf9hEs3

#include <stdio.h>

int main() { 
    
    char temp_buffer[8];
    double val = 25.3;

    sprintf(temp_buffer, "%.*g", sizeof(temp_buffer), val);
    printf("%s", temp_buffer);
}

我在带有 -Wall 标志的 gcc 11.3 中收到警告:

<source>:8:29: warning: field precision specifier '.*' expects argument of type 'int', but argument 3 has type 'long unsigned int' [-Wformat=]
    8 |     sprintf(temp_buffer, "%.*g", sizeof(temp_buffer), val);
      |                           ~~^~   ~~~~~~~~~~~~~~~~~~~
      |                             |    |
      |                             int  long unsigned int
<source>:8:27: warning: '%.*g' directive writing between 1 and 310 bytes into a region of size 8 [-Wformat-overflow=]
    8 |     sprintf(temp_buffer, "%.*g", sizeof(temp_buffer), val);
      |                           ^~~~
<source>:8:26: note: assuming directive output of 12 bytes
    8 |     sprintf(temp_buffer, "%.*g", sizeof(temp_buffer), val);
      |                          ^~~~~~
<source>:8:5: note: 'sprintf' output between 2 and 311 bytes into a destination of size 8
    8 |     sprintf(temp_buffer, "%.*g", sizeof(temp_buffer), val);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

事实上,目标缓冲区的大小太小,无法存储给定大小参数的值,但是警告 'sprintf' output between 2 and 311 bytes into a destination of size 8 是怎么回事?那 311 字节的值从何而来?

如果我将小数位数转换为 int (int)sizeof(temp_buffer) 潜在的溢出数字会急剧下降:

'sprintf' output between 2 and 16 bytes into a destination of size 8

代码中存在多个问题:

  • sprintf 需要 * 占位符的 int 值,而您传递的 size_t 可能具有不同的大小和表示形式。
  • 调用可能有未定义的行为,因为您请求的精度是目标数组的长度,这可能会产生超过所述长度的输出。

传递 sizeof(temp_buffer) 是一个错误,编译器似乎对实际参数值感到困惑,并且没有对精度值或要转换的数字做出特定假设。然而,当他们记录输出可以是 2 到 311 字节时,他们似乎错了:

  • 对于值 25.3,最接近的 IEEE 754 编号的精确表示是 25.300000000000000710542735760100185871124267578125,需要 52 个字节。
  • 通过printf("%.1000g", -0x1.fffffffffffffp+1023)输出的最大数字有310个字符,因此需要311个字节,这似乎是2 to 311 bytes.
  • 的原因
  • 然而 %.*g 转换实际上可以产生超过 311 个字节:printf("%.1000g", -5e-324) 在 macOS 和 linux.
  • 上产生 758 个字符

当您将 sizeof(temp_buffer) 转换为 (int) 时,编译器确定精度为 8(非平凡的优化)并确定输出可以小至 2 字节(一个数字和一个空终止符)但不超过 16 个(-,一个数字,.,7 个小数,e- 和多达 3 个指数数字加上一个空终止符。对于 8 字节数组来说,这仍然可能太多了。

警告程序员这种潜在的未定义行为做得很好!

使用更大的数组 snprintf() 并传递 (int)(sizeof(temp_buffer) - 9) 作为精度,以获得最坏情况下能容纳的尽可能多的小数。很难产生适合所有情况的尽可能多的小数,并且可能需要多次尝试或复杂的后处理。

基本上,它试图告诉您的是,最终可能会得到比 space 存储的数字更多的数字。如果 val 是一个非常大的数字会怎样?

Where does that 311 bytes value comes from?

编译器很困惑。

"%.*g", 8 可能输出 15*1 + 1(对于 空字符 )字符,但不是 311.

我怀疑编译器错误地预测打印 -DBL_MAX 将使用 310 + 1 个字符,期望 %g 切换到极值的指数表示法 - 这就是美"%g",输出有限。如果它不切换到指数表示法,那么极端输出将像 "%.*f", (int) 0.

int main(void) {
  char buffer[1000];
  char temp_buffer[8];
  int len = sprintf(buffer, "%.*g", (int) sizeof(temp_buffer), -DBL_MAX);
  printf("%d <%s>\n", len, buffer);
  len = sprintf(buffer, "%.*f", (int) 0, -DBL_MAX);
  printf("%d <%s>\n", len, buffer);
}

输出

15 <-1.7976931e+308>
310 <-179769313486231570814527423731704356798070565449239578069709514447683386590106403234437238318580897337933920052621128987133479958537240295444402082043505598186819583097828779632178833278408753356733764414284292236482024122876814115690851853178733231033249466834451356736736928870307247739983885979597249860971>

*1

1 1 1   8-1   1 1  3 --> 15
- d . ddddddd e - eee