A double-precision floating-point number is represented internally as 64 bits, divided into three fields: a sign field, an exponent field, and a fraction field. You don’t need to know this to use floating-point numbers, but knowing it can help you understand them. This article shows you how to access those fields in C code, and how to print them — in binary or hex.
Treating a Floating-Point Variable as Raw Bits
To access the raw storage of a double-precision floating-point variable, you need to treat the double not as a number, but as a generic sequence of bits. One way to do this is to cast the double as an array of characters. However, this is not ideal: it exposes endianness, and it does not allow for easy access to the IEEE defined fields — fields which span byte boundaries.
A better solution is to recast the double as an integer — a 64 bit integer. This gives endian-independent, bit-level access to the IEEE representation. That is the solution I use in the C code below.
(Note: My code works on architectures — like Intel — that use the same endianness for both floating point and integer values. My code will not work with mixed-endian encoding, like that used by ARM “soft float.” Also, recasting a double as an integer violates strict aliasing, which may or may not be enforced by your compiler.)
The Code
I wrote three functions:
- print_raw_double_binary(). This function casts the double as an integer and then shifts off its bits one at a time, printing each bit as it goes. A space is added between fields to delineate them.
- parse_double(). This function isolates the three fields of the double with shifts and bit masks and turns them into integers.
- print_raw_double_hex(). This function calls parse_double() to isolate the fields and then calls printf() with the ‘%X’ format specifier to print them in hex (spaces are printed between fields).
I declared and defined these functions in files I named rawdouble.h and rawdouble.c, respectively. I also wrote a test program, rawTest.c, to test the functions on various inputs.
rawdouble.h
/***********************************************************/ /* rawdouble.h: Functions to access and print the raw */ /* contents of an IEEE double floating-point */ /* variable */ /* */ /* Rick Regan, https://www.exploringbinary.com */ /* */ /***********************************************************/ void print_raw_double_binary(double d); void parse_double(double d, unsigned char *sign_field, unsigned short *exponent_field, unsigned long long *fraction_field); void print_raw_double_hex(double d);
rawdouble.c
/***********************************************************/ /* rawdouble.c: Functions to access and print the raw */ /* contents of an IEEE double floating-point */ /* variable */ /* */ /* Rick Regan, https://www.exploringbinary.com */ /* */ /***********************************************************/ #include "stdio.h" #include "rawdouble.h" void print_raw_double_binary(double d) { unsigned long long *double_as_int = (unsigned long long *)&d; int i; for (i=0; i<=63; i++) { if (i==1) printf(" "); // Space after sign field if (i==12) printf(" "); // Space after exponent field if ((*double_as_int >> (63-i)) & 1) printf("1"); else printf("0"); } printf("\n"); } void parse_double(double d, unsigned char *sign_field, unsigned short *exponent_field, unsigned long long *fraction_field) { unsigned long long *double_as_int = (unsigned long long *)&d; *sign_field = (unsigned char)(*double_as_int >> 63); *exponent_field = (unsigned short)(*double_as_int >> 52 & 0x7FF); *fraction_field = *double_as_int & 0x000FFFFFFFFFFFFFULL; } void print_raw_double_hex(double d) { unsigned char sign_field; unsigned short exponent_field; unsigned long long fraction_field; parse_double(d,&sign_field,&exponent_field,&fraction_field); printf("%X %X %llX\n",sign_field,exponent_field,fraction_field); }
rawTest.c
/***********************************************************/ /* rawTest.c: Program to test raw double functions */ /* */ /* Rick Regan, https://www.exploringbinary.com */ /* */ /***********************************************************/ #include "stdio.h" #include "assert.h" #include "rawdouble.h" int main(int argc, char *argv[]) { double d; int i; /* Make sure unsigned long long is 8 bytes */ assert (sizeof(unsigned long long) == sizeof(double)); d = 0.5; printf("0.5:\n"); print_raw_double_binary(d); print_raw_double_hex(d); printf("\n"); d = 0.1; printf("0.1:\n"); print_raw_double_binary(d); print_raw_double_hex(d); printf("\n"); /* d = NaN */ printf("NaN:\n"); d = 0; d/=d; print_raw_double_binary(d); print_raw_double_hex(d); printf("\n"); /* d = +infinity */ printf("+infinity:\n"); d = 1e300; d*=d; print_raw_double_binary(d); print_raw_double_hex(d); printf("\n"); /* d = 2^-1074 */ printf("2^-1074:\n"); d = 1; for (i=1; i<=1074; i++) d/=2; print_raw_double_binary(d); print_raw_double_hex(d); return (0); }
Notes
- The code works for all double values, including negative numbers, not-a-number (NaN), and infinity.
- You could use a union of a double and an integer instead of type casting the double to an integer, but this would require an extra step: copying the double to the union.
- The statement assert (sizeof(unsigned long long) == sizeof(double)) in rawTest.c is there to ensure that long long integers are 64 bits on your platform. You can remove it once the assertion checks out.
Compiling and Running
I compiled and ran this code on both Windows and Linux:
- On Windows, I built a project in Visual C++ and compiled and ran it in there.
- On Linux, I compiled with “gcc rawTest.c rawdouble.c -o rawTest” and then ran with “./rawTest”.
Output
0.5: 0 01111111110 0000000000000000000000000000000000000000000000000000 0 3FE 0 0.1: 0 01111111011 1001100110011001100110011001100110011001100110011010 0 3FB 999999999999A NaN: 1 11111111111 1000000000000000000000000000000000000000000000000000 1 7FF 8000000000000 +infinity: 0 11111111111 0000000000000000000000000000000000000000000000000000 0 7FF 0 2^-1074: 0 00000000000 0000000000000000000000000000000000000000000000000001 0 0 1
Modifying to Print Floats
The code can be modified to print floats; here’s an outline of what to change:
- Use ‘float’ instead of ‘double’.
- Use ‘unsigned long’ instead of ‘unsigned long long’.
- Use the ‘%lX’ printf() format specifier instead of ‘%llX’
- Use bit masks and shift amounts appropriate for the float representation.
- In print_raw_double_binary(), or what would become print_raw_float_binary(), print a space after bit 9 instead of after bit 12.
- Rename the routines and variables to use ‘float’ instead of ‘double’.
Addendum: Printing in Binary Scientific Notation
Another way to display a floating-point number is in binary scientific notation; I wrote a C function called print_double_binsci() to do this.
Hi, I don’t understand, why in function print_raw_double_binary you write:
1. unsigned long long *double_as_int = (unsigned long long *)&d;
and why later in this function you use:
2. if ((*double_as_int >> (63-i)) & 1)
what does it mean? Can we use different method to achieve the same results?
Great article, btw! Thank you.
anon1,
I am “aliasing” the double to treat it as a 64-bit integer so I can use bitwise operations on it. For the line you cited, which is in a loop, the effect is to iterate through all the bits from right to left.
You can use a union of a double and an unsigned long long instead, which is probably cleaner than aliasing.
Hi. Thx for great article and code. What is the licence of your code ? Can I use it in wikibooks ?
@Adam,
My articles are copyrighted, so a link would be appropriate (not copying). Thanks for the interest.
OK : https://pl.wikibooks.org/wiki/C/Zmienne#Specjalne_warto.C5.9Bci
There are two other methods of accessing bits of a float in C/C++:
1. You can make a union of a float/double and an int. This makes the same storage area to be visible through two different interpretations, without the need for explicit casts, and it is quite portable.
2. You can use bit fields in a structure and then access the particular bit sequences in a float/double by their names. This is less portable, though, because the order and packing rules are compiler-dependent, but it works as well, and it does not require any explicit casting nor bit shifting & masking – the compiler does them internally.
More on these techniques and example code here:
https://randomascii.wordpress.com/2012/01/11/tricks-with-the-floating-point-format/
@SasQ,
I did mention the union solution in the article (and comments).
There’s also this useful website with online IEEE representation applet:
http://www.h-schmidt.net/FloatConverter/IEEE754.html