Techniques for directly modifying bitfields of various datatypes in C

Suppose that you are learning about IEEE-754 and you are experimenting with how single-precision values are encoded in memory. Suppose also that you are experimenting with various bitstring representations of floatint point values, and you wish to store one of them in C’s float datatype. Your first thought might be to do something like this:

1const float num = 0b11000000000000000000001000000000;
2printf("%.15f\n", num);

The bitstring constant used above represents the value -2.0001220703125 in IEEE-754 single-precision, but compiling and running this, you get:


That’s because the compiler treats the bistring constant as an integer, and only later converts it to the type of the l-value of the assignment statement, i.e. to a float. There are multiple ways to “work around” this. I am familiar with a few solutions which I will document and explain.

It’s worth nothing that using binary number constants (with the 0b prefix) is a non-standard GCC extension.

Pointer manipulation

1#define BIT_CONSTANT 0b11000000000000000000001000000000
3float num;
4* ((uint32_t *) &num) = BIT_CONSTANT;
6printf("%.15f\n", num); // -2.0001220703125

Here, we first take the pointer (address) of the float in which we desire to store the bit constant (&num), but we cast it to a uint32_t pointer. Dereferencing that uint32_t * pointer will now “trick” the compiler, and since the types of both sides of the assignments now match, no manipulation is done to the final value. The 4 bytes at num are now the same ones written in our constant, and printing out num as a float yields the expected value (-2.0001220703125).

This sort of pointer manipulation has other useful usecases as well. Imagine that you are trying to alter the 2nd least significant byte in a 4-byte integer. You could certaintly accomplish that using bit masks and bitwise operators, but you could also do it, for exmaple, like this:

1uint32_t num = 0;
2* (((char *) &num) + 1) = 2;
3printf("%u\n", num); // prints "512"

Here we set the value of the second lowest-order byte of the integer to 2, i.e. we flip its second least significant bit, which is consequently the 9th bit in the integer as a whole, thus the final value ends up being $2^9 = 512$. On MSB architectures the value would instead become $2^{18} = 262144$.

Using an union

 1#define BIT_CONSTANT 0b11000000000000000000001000000000
 3union compound {
 4    float num;
 5    uint32_t data;
 6} value = {
 7    .data = BIT_CONSTANT
10printf("%.15f\n", value.num); // -2.0001220703125

This one is quite self-explanatory. We create a union with two fields: an unsigned integer, and a float. Both of the types are 4-bytes wide, and setting one will directly modify the bytes actually stored in the union, with no conversions or other manipulations being done.

Using memcpy

1#define BIT_CONSTANT 0b11000000000000000000001000000000
3float num;
4memcpy(&num, & (uint32_t) {BIT_CONSTANT}, sizeof(float));
6printf("%.15f\n", value.num); // -2.0001220703125

As memcpy simply copies raw bytes from one location in memory to the other, this is the most trivial example to understand. Note that the second argument to the function call utilizes a C99 feature called compound literals - it isn’t a cast.

It’s as if I had written this instead:

1const uint32_t value = BIT_CONSTANT;
2memcpy(&num, &value, sizeof(float));

All of these techniques of course work regardless of the datatype of the destination the bytes at which we wish to manipulate directly.