S Mahesh Kumar
S Mahesh Kumar

Reputation: 45

Undefined behavior of " %*d " in C programing language

case 1:

printf("%*d", 10, 5)

output :

_________5

(I am using _ to denote blank spaces )


case 2:

printf("%*d", 10.4, 5)

expected output:

______0005

But goes for infinite loop


why is this behavior being showed by %*d for decimal "field width precision prefix" ?

Upvotes: 1

Views: 141

Answers (4)

Yun
Yun

Reputation: 3812

The second line, printf("%*d",10.4,5) leads to undefined behavior. The function printf expects an int but is given the double 10.4 instead.
The handling of the variable number of parameters of printf, the format string and the corresponding types is complex, and goes deep into compiler construction. It's difficult to trace what happens exactly and can vary from compiler to compiler.

See here for the exact specifications of the printf format specifiers.

Addition:

Let's see if we can trace what exactly happens in GNU's implementation of the standard library. First, looking at the source for printf. It uses varargs and a function called __vfprintf_internal. The latter is defined here (line 1318). On line 1363, a sanity check is performed using a macro, but it only checks if the format string pointer is not NULL. Line 1443:

int prec = -1;  /* Precision of output; -1 means none specified.  */

Line 1582, specifying the argument as an int in case the * modifier was used:

prec = va_arg (ap, int);

From here on, the precision is processed as an int. Let's look at the implementation of va_arg to see what happens if it is given a double. It is part of the stdarg header of GCC, see here, line 49:

#define va_arg(v,l) __builtin_va_arg(v,l)

And now it gets complex. __builtin_va_arg isn't explicitly defined anyway, but is part of GCC's internal representation of the C language. See the parser file. That means that we cannot read concrete types in the source files anymore. We can obtain some more information on the processing of varargs in the builtins.c file. From now on I can only guess what happens. The processing appears to start in expand_builtin_va_start which takes a tree parameter and returns an rtx (RTL Expression) object. This object is a constant and probably has the double type mode. I'm assuming the compiler processes double expression until it knows what (machine specific) bit values it has to write in the executable. Since, evidently, the floating point number is not truncated to an int, I wouldn't be surprised if the value would actually correspond, and later will be interpreted as, a more or less random value (e.g. 77975616). It may be also conceivable that the memory of the program would misaligned when, e.g., the type of a double (usually 8 bytes) is larger than an int (usually 4 bytes). More on the implementation of varargs here. Whatever sort-of random value the integer could take would be processed back in the process_arg(fspec) back in vfprintf-internal.c.

Additional curiosity:

If printf is given a float as specifier, even if it is explicitly cast, GCC will still give a warning that the value is a double:

warning: field width specifier ‘*’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat=]
   10 |     printf("%*d\n", (float) 12.3F, 5);
      |             ~^~     ~~~~~~~~~~~~~
      |              |      |
      |              int    double

Upvotes: 2

0___________
0___________

Reputation: 67546

Use -Wall -Wextra to see more warnings. You will discover:

<source>:30:12: warning: field width specifier '*' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
   30 |   printf("%*d", 10.4, 5);
      |           ~^~   ~~~~
      |            |    |
      |            int  double

It is undefined behaviour.

Now an example what is happening in the printf function:

int foo(const char *fmt, ...)
{
    va_list va;
    int retval = 0;

    va_start(va, fmt);
    retval = va_arg(va, int);
    return retval;
}

unsigned bar(const char *fmt, ...)
{
    va_list va;
    unsigned retval = 0;

    va_start(va, fmt);
    retval = va_arg(va, unsigned);
    return retval;
}

int main(void)
{
    printf("as int %d\n", foo("", 10.4));
    printf("as uint %u\n", bar("", 10.4));
}

And let's execute it: https://godbolt.org/z/EsMaM8EPs

enter image description here

Upvotes: 1

templatetypedef
templatetypedef

Reputation: 372814

When you pass 10.4 into printf, it’s treated as a double. However, when printf sees the * character, it tries to interpret the argument containing the number of characters to print as an int. Despite the fact that you and I can intuitively see how to treat 10.4 as an integer by rounding down to 10, that’s not how C sees things. This results in what's called undefined behavior. On some platforms, C might treats the bytes of the double 10.4 as an integer, producing an absolutely colossal integer rather than the expected 10. On other platforms, it might read other data expecting to find an int argument, but instead which holds some other unexpected value. In either case, the result is unlikely to be the nice "interpret the value as 10" that you expect it to be.

Upvotes: 1

Amadan
Amadan

Reputation: 198334

You told printf using the * format to expect a width integer in the argument list. You gave it a floating-point argument, 10.4. C gets confused when it expects an integer and a float is there instead. You likely intended this:

printf("%*.*d", 10, 4, 5);

with width and precision being represented each by its own separate integral argument.

Upvotes: 4

Related Questions