4

In a portion of my program, I'll have to manage signed 8-bit integers. I have no problem to display them as decimals using printf and %d formatting.

However, when it comes to hexadecimal representation, the formatting of a negative int8_t variable is not shown as a 8-bit hexadecimal (0xXX) but as a 32-bit hexadecimal (0xFFFFFFXX).

Here is the snippet of code, describing my problem:

#include <stdio.h>
#include <stdint.h>

int main()
{
    int8_t value = 0;
    
    printf("t = %d = 0x%02X\n", value, value);
    t = 127;
    printf("t = %d = 0x%02X\n", value, value);
    t = -128;
    printf("t = %d = 0x%02X\n", value, value);
    
    return 0;
}

Compilation and execution give:

t = 0 = 0x00
t = 127 = 0x7F
t = -128 = 0xFFFFFF80

I would like to get 0x80 and not 0xFFFFFF80. What did I do wrong? How to display the negative signed 8-bit integer as a 8-bit hexadecimal?

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
axel_joly
  • 43
  • 3

5 Answers5

7

The problematic 'sign extension' is happening because, for your %X format specifier, the expected argument type is of int size, so your int_8 argument, after being suitably promoted (and sign-extended), is then printed as a 'full-size' unsigned integer.

You can prevent the latter part by adding the hh length modifier to the format, which indicates that the corresponding argument is of char size, so only the least-significant byte will be printed:

#include <stdio.h>
#include <stdint.h>

int main()
{
    int8_t value = 0;

    printf("t = %d = 0x%02hhX\n", value, value);
    value = 127;
    printf("t = %d = 0x%02hhX\n", value, value);
    value = -128;
    printf("t = %d = 0x%02hhX\n", value, value);

    return 0;
}

Further Reference


Note: As pointed out in the comments here, and in other answers, the use of the %X format specifier (with or without a length modifier) for an argument of signed type is, formally, undefined behaviour (though it will likely work on the vast majority of modern systems).

To avoid such potential UB, a better way to achieve your goal is to explicitly cast your int8_t argument(s) to the unsigned equivalent (of the same bit size – or uint8_t, in your case). Then, when the "default argument promotion" is applied, it will be performed without sign extension (as all possible values of a uint8_t are representable in an int); thus, there will then be no need to add the hh length modifier to your format, as the upper (added) bits of the resultant unsigned int values will not be set.

This code gives your desired result in a well-defined way:

int main()
{
    int8_t value = 0;
    printf("t = %d = 0x%02X\n", value, (uint8_t)value);
    value = 127;
    printf("t = %d = 0x%02X\n", value, (uint8_t)value);
    value = -128;
    printf("t = %d = 0x%02X\n", value, (uint8_t)value);
    return 0;
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • %x actually expects unsigned int but that's not why. Integer promotion happens as part of "the default argument promotions", regardless of what conversion specifier you gave printf. – Lundin May 25 '21 at 12:01
  • @Lundin Correct on the *unsigned* part, which is why I said "... of *int* **size** ..." – Adrian Mole May 25 '21 at 12:03
  • 1
    `%hhx` is not correct format for `uint8_t` . `PRIx8` is. – 0___________ May 25 '21 at 12:06
  • The first paragraph is not correct. The promotion of arguments is not affected by the format string. (The format may not even be known by the compiler at this point) . Instead the *default argument promotions* are applied to each argument, regardless of the format string – M.M May 25 '21 at 12:15
  • @M.M Yes - you are correct that my answer is (partially) incorrect. I shall edit the first part, but I am looking for a way that doesn't (overtly) copy stuff from your answer. – Adrian Mole May 25 '21 at 12:22
  • 1
    "for your %X format specifier, the expected argument type is of int size" --> C specifies that `"%X"` needs to match an `unsigned`, not a "type is of int size". OP's code and this are UB - a nice UB. – chux - Reinstate Monica May 25 '21 at 13:03
  • @chux-ReinstateMonica Well, as we don't like UB on Stack Overflow, I added an 'appendix'. Hope you approve! :-) – Adrian Mole May 25 '21 at 13:33
3

When integer types smaller than int are passed to a variadic function like printf they are promoted to type int. So a int8_t with value -1 and representation 0xff becomes an int with value -1 and representation 0xffffffff.

That's why you're seeing the values you are while using %x which expects and prints an int. To indicate that you're printing a char value, use %hhx which will convert the value to char before printing.

printf("t = %d = 0x%02hhX\n", value, value);

Alternately, you can cast the value to uint8_t to better match what %x expects:

printf("t = %d = 0x%02X\n", value, (uint8_t)value);
dbush
  • 205,898
  • 23
  • 218
  • 273
  • Thanks a lot for your answer. I did not know that there was a type "transformation" when using printf function. I thought about it and tried casting (int8_t) but it was overwritted. You solution works very well, as I wanted. – axel_joly May 25 '21 at 12:03
  • If you feel the edit (added "Note:...") to my question is too much a copy of the last part of yours, just let me know, and I'll try to do something about it. But I hadn't read your answer (fully) when I made the edit to my own. – Adrian Mole May 25 '21 at 14:19
3

This code has undefined behaviour. The behaviour of the %X format specifier is only defined by the language standard for the case of an argument of type unsigned int. You provided an int8_t instead.

In practice you may (but this is not guaranteed) find that the printf function tries to read an unsigned int from the location where integer arguments are stored, and what you are seeing corresponds to how that location gets filled up by an int8_t value.

M.M
  • 138,810
  • 21
  • 208
  • 365
1

This is because of default promotion of integer arguments. You need to use correct format specifiers and cast:

int main(void)
{
    int8_t value = 0;
    
    printf("t = %d = %02" PRIx8 "\n", value, value);
    value = 127;
    printf("t = %d = 0x%02" PRIx8 "\n", value,(uint8_t)value);
    value = -128;
    printf("t = %d = 0x%02" PRIx8 "\n", value, (uint8_t)value);
    
    return 0;
}

https://godbolt.org/z/jMqhz5156

0___________
  • 60,014
  • 4
  • 34
  • 74
  • There is no conversion of arguments corresponding to the ellipsis. There are the *default argument promotions* which may or may not result in a signed integer. For example an `unsigned int` argument is not promoted. – M.M May 25 '21 at 12:11
1

I would like to get 0x80 and not 0xFFFFFF80.

0x80 represents the value +12810.

To print -128 as 0x80 without undefined behavior, do not attempt to print negative numbers with "%X". Convert to a positive number first.

t = -128;
// printf("t = %d = 0x%02X\n", value, value); 
printf("t = %d = 0x%02X\n", value, (uint8_t) value); 

(uint8_t) value converts int8_t negative values by adding 256. Passing the uint8_t to a ... argument converts to an int with values in the range [0...255].

"%X" expects an unsigned. Passing an int to a ... argument that is read as an unsigned is OK as long as the value is representable in both (e.g. positive) per C17dr §6.5.2.2 6.

Pedantically, to not rely on §6.5.2.2 6:

printf("t = %d = 0x%02X\n", value, (unsigned) (uint8_t) value); 
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256