3

I was trying to write a function in C to print out the hexadecimal representation of a generic data type value. My first attempt was failed, the second succeeded, why?

  1. First attempt

    #include <stdio.h>
    
    /* prints out the hexadecimal representation of a generic value */
    void show_hex(void*,size_t);
    
    int main() {
        int i = 12345;
        short s = 32767; /* 2^15 - 1 */
        show_hex(&i, sizeof(int));
        show_hex(&s, sizeof(short));
        return 0;
    }
    
    void show_hex(void *x, size_t sz){
         char *cx = x;
         int i;
         printf("0x");
         for (i = 0; i < sz; ++i)
             printf("%.2x", cx[i]);
         printf("\n");
    }
    

    Output: 0x39300000 for the int case as expected on little endian machine (Mac OSX). For the short case, it outputs 0xffffffff7f instead of 0xff7f.

  2. Second attempt:

    typedef unsigned char *byte_pointer;
    
    int main() {
        int i = 12345;
        short s = 32767; /* 2^15 - 1 */
        show_hex((byte_pointer)&i, sizeof(int));
        show_hex((byte_pointer)&s, sizeof(short));
        return 0;
    }
    
    void show_hex(byte_pointer x, size_t sz){
        int i;
        printf("0x");
        for (i = 0; i < sz; ++i)
            printf("%.2x", x[i]);
        printf("\n");
    }
    

    This program outputs as expected for both cases, int: 0x39300000 and short: 0xff7f

  • 5
    For the first case, change `printf("%.2x", cx[i]);` to `printf("%.2x", cx[i] & 0xFF);`. The plain `char` type is signed, so 0xFF is sign-extended to an 8-byte value all F's, as shown in the printout. With `printf()` and other variadic functions, the arguments in the `...` part of the argument list are default promoted, so `char` is promoted to `int` (as is `short`), and `float` to `double`. – Jonathan Leffler Apr 06 '17 at 05:22
  • 4
    @JonathanLeffler: You should put that as an answer, because it's worthwhile. – l'L'l Apr 06 '17 at 05:28
  • 2
    Or just use `unsigned char` in the first attempt like you do in second. (I must say that you hid that subtle difference quite well). – Art Apr 06 '17 at 05:29
  • 1
    shouldn't the output be 0x00003039 and 0x7fff instead, so why not do 'for(i = sizeof(int) - 1; i >= 0; i--)'? – CIsForCookies Apr 06 '17 at 05:47
  • Thank @JonathanLeffler for your answer. It's still vague. Why `int` value got the same output in both cases? As it turns out for `short` value in the first case, it outputs `ffffffff` (4 bytes) in the first iteration `cx[0] = ffffffff`. My argument is: `cx` is `char*`, so it should have given us one byte instead of four when we dereferenced it, right?. –  Apr 06 '17 at 05:50

1 Answers1

4

For the first case, change:

printf("%.2x", cx[i]);

to

printf("%.2x", cx[i] & 0xFF);

The plain char type is signed, so 0xFF is sign-extended to an 8-byte value all F's, as shown in the printout.

The first number worked OK as all the bytes were in the range 0x00..0x7F, so that the value was not converted to a negative integer — it stayed positive. The second number didn't work out because one of the bytes was in the range 0x80..0xFF and so was converted to a negative integer. And the %.2x will always print all the digits, but if there's only one, there'll be a 0 before that digit. The x expects an unsigned int.

With printf() and other variadic functions, the arguments in the ... part of the argument list are default promoted, so char types (all varieties) are promoted to int (as is short), and float is promoted to double.

This is documented in the C standard (ISO/IEC 9899-2011):

6.5.2.2 Function calls

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

7 If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

8 No other conversions are performed implicitly; in particular, the number and types of arguments are not compared with those of the parameters in a function definition that does not include a function prototype declarator.


Semi-tangential aside.

The rest of paragraph 6 includes:

If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or …, the behavior is undefined.

That sentence has to be read in the context of 'If the expression that denotes the called function has a type that does not include a prototype, …' at the beginning of the paragraph. It means that if you make a call to a function that is defined with ellipsis , ... but there isn't a prototype in scope at the time of the call, the behaviour is undefined. You must make sure that you have a prototype in scope whenever you call a variadic function.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278