为什么 printf (“% f”,0)给出未定义行为?

声明

printf("%f\n",0.0f);

指纹为0。

但是,那份声明呢

printf("%f\n",0);

打印随机值。

我意识到我表现出了某种不明确的行为,但我不知道具体为什么。

所有位都为0的浮点值仍然是值为0的有效 float
floatint在我的机器上是相同的尺寸(如果这是相关的)。

为什么在 printf中使用整数文字而不是浮点文字会导致这种行为?

另外,如果我使用

int i = 0;
printf("%f\n", i);
8178 次浏览

I'm not sure what's confusing.

Your format string expects a double; you provide instead an int.

Whether the two types have the same bit width is utterly irrelevant, except that it may help you avoid getting hard memory violation exceptions from broken code like this.

The "%f" format requires an argument of type double. You're giving it an argument of type int. That's why the behavior is undefined.

The standard does not guarantee that all-bits-zero is a valid representation of 0.0 (though it often is), or of any double value, or that int and double are the same size (remember it's double, not float), or, even if they are the same size, that they're passed as arguments to a variadic function in the same way.

It might happen to "work" on your system. That's the worst possible symptom of undefined behavior, because it makes it difficult to diagnose the error.

N1570 7.21.6.1 paragraph 9:

... If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

Arguments of type float are promoted to double, which is why printf("%f\n",0.0f) works. Arguments of integer types narrower than int are promoted to int or to unsigned int. These promotion rules (specified by N1570 6.5.2.2 paragraph 6) do not help in the case of printf("%f\n", 0).

Note that if you pass a constant 0 to a non-variadic function that expects a double argument, the behavior is well defined, assuming the function's prototype is visible. For example, sqrt(0) (after #include <math.h>) implicitly converts the argument 0 from int to double -- because the compiler can see from the declaration of sqrt that it expects a double argument. It has no such information for printf. Variadic functions like printf are special, and require more care in writing calls to them.

Why does using an integer literal instead of a float literal cause this behavior?

Because printf() doesn't have typed parameters besides the const char* formatstring as the 1st one. It uses a c-style ellipsis (...) for all the rest.

It's just decides how to interpret the values passed there according to the formatting types given in the format string.

You would have the same kind of undefined behavior as when trying

 int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB

Ordinarily when you call a function that expects a double, but you provide an int, the compiler will automatically convert to a double for you. That doesn't happen with printf, because the types of the arguments aren't specified in the function prototype - the compiler doesn't know that a conversion should be applied.

First off, as touched on in several other answers but not, to my mind, spelled out clearly enough: It does work to provide an integer in most contexts where a library function takes a double or float argument. The compiler will automatically insert a conversion. For instance, sqrt(0) is well-defined and will behave exactly as sqrt((double)0), and the same is true for any other integer-type expression used there.

printf is different. It's different because it takes a variable number of arguments. Its function prototype is

extern int printf(const char *fmt, ...);

Therefore, when you write

printf(message, 0);

the compiler does not have any information about what type printf expects that second argument to be. It has only the type of the argument expression, which is int, to go by. Therefore, unlike most library functions, it is on you, the programmer, to make sure the argument list matches the expectations of the format string.

(Modern compilers can look into a format string and tell you that you've got a type mismatch, but they're not going to start inserting conversions to accomplish what you meant, because better your code should break now, when you'll notice, than years later when rebuilt with a less helpful compiler.)

Now, the other half of the question was: Given that (int)0 and (float)0.0 are, on most modern systems, both represented as 32 bits all of which are zero, why doesn't it work anyway, by accident? The C standard just says "this isn't required to work, you're on your own", but let me spell out the two most common reasons why it wouldn't work; that will probably help you understand why it's not required.

First, for historical reasons, when you pass a float through a variable argument list it gets promoted to double, which, on most modern systems, is 64 bits wide. So printf("%f", 0) passes only 32 zero bits to a callee expecting 64 of them.

The second, equally significant reason is that floating-point function arguments may be passed in a different place than integer arguments. For instance, most CPUs have separate register files for integers and floating-point values, so it might be a rule that arguments 0 through 4 go in registers r0 through r4 if they are integers, but f0 through f4 if they are floating-point. So printf("%f", 0) looks in register f1 for that zero, but it's not there at all.

Using a mis-matched printf() specifier "%f"and type (int) 0 leads to undefined behavior.

If a conversion specification is invalid, the behavior is undefined. C11dr §7.21.6.1 9

Candidate causes of UB.

  1. It is UB per spec and the compile is ornery - 'nuf said.

  2. double and int are of different sizes.

  3. double and int may pass their values using different stacks (general vs. FPU stack.)

  4. A double 0.0 might not be defined by an all zero bit pattern. (rare)

"%f\n" guarantees predictable result only when the second printf() parameter has type of double. Next, an extra arguments of variadic functions are subject of default argument promotion. Integer arguments fall under integer promotion, which never results in floating-point typed values. And float parameters are promoted to double.

To top it off: standard allows the second argument to be or float or double and nothing else.

Why it is formally UB has now been discussed in several answers.

The reason why you get specifically this behaviour is platform-dependent, but probably is the following:

  • printf expects its arguments according to standard vararg propagation. That means a float will be a double and anything smaller than an int will be an int.
  • You are passing an int where the function expects a double. Your int is probably 32 bit, your double 64 bit. That means that the four stack bytes starting at the place where the argument is supposed to sit are 0, but the following four bytes have arbitrary content. That's what is used for constructing the value which is displayed.

This is one of those great opportunities to learn from your compiler warnings.

$ gcc -Wall -Wextra -pedantic fnord.c
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
printf("%f\n",0);
^

or

$ clang -Weverything -pedantic fnord.c
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
printf("%f\n",0);
~~    ^
%d
1 warning generated.

So, printf is producing undefined behavior because you are passing it an incompatible type of argument.

The main cause of this "undetermined value" issue stands in the cast of the pointer at the int value passed to the printf variable parameters section to a pointer at double types that va_arg macro carries out.

This causes a referencing to a memory area that was not completely initialized with value passed as parameter to the printf, because double size memory buffer area is greater than int size.

Therefore, when this pointer is dereferenced, it is returned an undetermined value, or better a "value" that contains in part the value passed as parameter to printf, and for the remaining part could came from another stack buffer area or even a code area (raising a memory fault exception), a real buffer overflow.


It can consider these specific portions of semplificated code implementations of "printf" and "va_arg"...

printf

va_list arg;
....
case('%f')
va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
....


the real implementation in vprintf (considering gnu impl.) of double value parameters code case management is:

if (__ldbl_is_dbl)
{
args_value[cnt].pa_double = va_arg (ap_save, double);
...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer


double i2 = *((double *)p); //casting to double because va_arg(arg, double)
p += sizeof (double);



references

  1. gnu project glibc implementation of "printf"(vprintf))
  2. example of semplification code of printf
  3. example of semplification code of va_arg