Assuming your code roughly looks like this: godbolt
#include <cstdio>
#include <cstring>
int main() {
float my_fl = 1.00f;
char bytes[4];
memcpy(bytes, &my_fl, sizeof(float));
for (size_t i = 0; i < sizeof(float); ++i)
printf("Byte %zu is 0x%02x.\n", i, (int)bytes[i]);
}
The output would most likely be:
Byte 0 is 0x00.
Byte 1 is 0x00.
Byte 2 is 0xffffff80.
Byte 3 is 0x3f.
The reasons for this are:
1. Sign-extension
In order to correctly keep negative values when converting to larger integral type, the sign needs to be extended.
Example:
char a = 1; // 0x01
int b = a; // 0x00000001
char c = -1; // 0xFF
int d = c; // 0xFFFFFFFF // (not 0x000000FF!)
The process for this is relatively simple: if the highest bit is set a signed number has a negative value.
- If the number is 0 or positive, fill the added bits in front with 0's.
- If the number is negative, fill the added bits in front with 1's.
The reason why this is happening is because variadic arguments (like it is the case with printf) promote their integral parameters - so bool, char, short, etc... will all be promoted to ints.
You can easily fix this in one of 2 ways:
1.1 Make the char array unsigned
unsigned numbers don't have negative values, so no sign extension happens for them.
e.g.: godbolt
#include <cstdio>
#include <cstring>
int main() {
float my_fl = 1.00f;
unsigned char bytes[4];
memcpy(bytes, &my_fl, sizeof(float));
for (size_t i = 0; i < sizeof(float); ++i)
printf("Byte %zu is 0x%02x.\n", i, (int)bytes[i]);
}
1.2 tell printf that it's actually dealing with a char
printf doesn't know that you passed a char, since it gets promoted to int before the call.
But you can use a size-specifier to let it know that you want to treat it as a char, not an int.
For char the size-specifier would be hh, e.g.: godbolt
#include <cstdio>
#include <cstring>
int main() {
float my_fl = 1.00f;
char bytes[4];
memcpy(bytes, &my_fl, sizeof(float));
for (size_t i = 0; i < sizeof(float); ++i)
printf("Byte %zu is 0x%02hhx.\n", i, (int)bytes[i]);
}
2. Endianness
Depending on the architecture your PC is running on it could be little-endian or big-endian.
(or middle-endian)
e.g.:
- IA-32, x86-64, ... are little-endian
- AVR32, OpenRISC, SPARC, ... are big-endian
- AArch64, RISC-V, ... can be operated either as little- or big-endian
Depending on the endianness the output of your program can vary:
- Little Endian:
Byte 0 is 0x00.
Byte 1 is 0x00.
Byte 2 is 0x80.
Byte 3 is 0x3f.
- Big Endian:
Byte 0 is 0x3f.
Byte 1 is 0x80.
Byte 2 is 0x00.
Byte 3 is 0x00.
- Mid-little Endian (one example of the Middle-Endian group):
Byte 0 is 0x00.
Byte 1 is 0x00.
Byte 2 is 0x3f.
Byte 3 is 0x80.
Since C++20 there's a language built-in way now to check for which endianness you're compiling for:
if constexpr (std::endian::native == std::endian::big) {
// big endian
} else if constexpr(std::endian::native == std::endian::little) {
// little endian
} else {
// some form of middle endian
}
bytestounsigned char bytes[ sizeof(float) ];reinterpret_cast<unsigned char*>(&f)- note this only works for (unsigned) char (andstd::byte).charis signed in your C implementation, so the byte is negative and more bits are set when it is automatically promoted tointto pass toprintf. Generally, you should useunsigned charwhen working with the bytes that represent objects.