C / TYPES AND REPRESENTATION
How integers are stored: bits, bytes and endianness
Inspect the individual bytes of an integer in memory, decide whether your machine is little- or big-endian, and pack integers in a fixed byte order.
What you will learn
- Read an integer's bytes in address order through an unsigned char pointer
- Detect host endianness by comparing byte 0 with the value's low-order byte
- Recognise two's complement patterns such as 0xFB standing for -5 in one byte
- Serialise integers with shifts and masks so the byte order is yours, not the CPU's
Understanding How integers are stored: bits, bytes and endianness
An integer is a fixed-size block of bits, and every bit carries a place value: bit 0 is worth 1, bit 1 is worth 2, bit k is worth 2^k. Eight of those bits make a byte, the smallest thing C lets you take the address of, so a 32-bit integer occupies four consecutive bytes starting at &n. In a signed type the top bit's place value is negative instead of positive (-128 in an 8-bit type), which is why two's complement needs no separate sign flag and why the pattern 0xFB reads as -5 in a signed char and as 251 in an unsigned one.
Endianness is a different question from the bit pattern: given that a value's bytes are 0A, 0B, 0C, 0D, which one sits at the lowest address? A little-endian machine stores the least significant byte first, so 0x0A0B0C0D appears in memory as 0D 0C 0B 0A; a big-endian machine stores it in the order you wrote it. The number itself is unchanged, only its layout, so the same arithmetic prints the same results on both, and only byte-level access exposes the difference.
The model worth keeping is that the value belongs to the language while the layout belongs to the hardware. Operators like +, >>, & and | are defined on the number, so n >> 24 yields the most significant byte and 1 << 3 is 8 on every conforming implementation. Byte order leaks out precisely when you step around the value and touch storage: casting to unsigned char *, memcpy into a buffer, fwrite of a struct, or pushing raw bytes into a socket. Code that builds bytes with shifts and masks is portable by construction; code that copies an int's memory is portable only until it meets a machine with the other order.
<stdio.h>
<stdint.h>
<stddef.h>
int main(void)
{
uint32_t n = 0x0A0B0C0Du;
const unsigned char *b = (const unsigned char *)&n;
size_t i;
printf("value = %u (hex %08X)\n", n, n);
printf("bytes at &n:");
for (i = 0; i < sizeof n; i++)
printf(" [%zu]=%02X", i, b[i]);
putchar('\n');
/* 0x0D is the low-order byte of the value */
printf("lowest address holds 0x%02X -> %s-endian\n",
b[0], b[0] == 0x0D ? "little" : "big");
printf("n >> 24 = 0x%02X, n & 0xFF = 0x%02X\n", n >> 24, n & 0xFF);
return 0;
}
A value comes from the place values of its bits, while endianness only decides which byte of that value sits at which address, so byte order matters only when you read or write storage directly.
Worked examples
Bits of a signed byte
Prints the eight bits of several signed char values so the two's complement pattern is visible.
<stdio.h>
static void print_bits(unsigned char byte)
{
int i;
for (i = 7; i >= 0; i--)
putchar((byte >> i) & 1 ? '1' : '0');
}
int main(void)
{
signed char values[] = { 5, -5, 127, -128 };
int i;
for (i = 0; i < 4; i++) {
unsigned char raw = (unsigned char)values[i];
printf("%4d -> ", values[i]);
print_bits(raw);
printf(" (0x%02X)\n", raw);
}
return 0;
}
Example explained
Line 1(unsigned char)values[i] is defined as the value modulo 256, which on two's complement hardware leaves the byte pattern untouched and only changes how it is read.
Line 2(byte >> i) & 1 isolates the bit whose place value is 2^i, and counting i down from 7 prints the digits in the order a binary numeral is normally written.
Line 3-5 shows as 11111011 because a negative x is stored as the pattern for 256 + x inside one byte: 256 - 5 = 251 = 0xFB.
Line 4-128 has only the top bit set, and since that bit's place value is -128 with nothing added, the range of one signed byte is -128..127 rather than -127..127.
Packing bytes in a fixed order
Writes a 32-bit value into a buffer most significant byte first and reads it back, independently of host byte order.
<stdio.h>
<stdint.h>
static void store_be32(unsigned char *p, uint32_t v)
{
p[0] = (unsigned char)(v >> 24);
p[1] = (unsigned char)(v >> 16);
p[2] = (unsigned char)(v >> 8);
p[3] = (unsigned char)v;
}
static uint32_t load_be32(const unsigned char *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16)
| ((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
int main(void)
{
unsigned char buf[4];
store_be32(buf, 0xDEADBEEFu);
printf("buffer: %02X %02X %02X %02X\n", buf[0], buf[1], buf[2], buf[3]);
printf("read back: %08X\n", load_be32(buf));
return 0;
}
Example explained
Line 1v >> 24 picks the most significant byte by arithmetic, so buf[0] becomes 0xDE on a little-endian and a big-endian host alike.
Line 2The (unsigned char) casts discard the higher bits, which is the masking to eight bits that a byte slot requires.
Line 3(uint32_t)p[0] << 24 widens before shifting on purpose: p[0] alone promotes to int, and 0xDE << 24 overflows a 32-bit signed int, which is undefined behaviour.
Line 4The buffer contents and the recovered value are reproducible everywhere because only shifts and masks were used, never a copy of an int's memory.
Important notes
The output shown comes from a little-endian host such as x86-64 or ARM in its usual configuration; on s390x the byte list is reversed and the detection line reports big-endian.
Examining any object through unsigned char * is explicitly permitted, and a byte is CHAR_BIT bits, 8 on every mainstream platform; casting &n to short * or float * and reading is a different matter that runs into alignment and aliasing rules.
Common mistakes
Dumping bytes through char * instead of unsigned char *: char is signed on x86, so the byte 0xFB promotes to -5 and %02X prints FFFFFFFB, turning the dump into noise.
Thinking endianness reverses bits or changes >>: people byte-swap before shifting and then extract the wrong byte, because shifts act on the value and n >> 24 is already the most significant byte everywhere.
Sending a raw int or struct with fwrite or send: a little-endian writer's 0x00000102 arrives at a big-endian reader as 0x02010000, so lengths and IDs come out absurdly large.
Try it yourself
Change, predict, then run
Store 0x01020304 in a uint32_t, print its four bytes, then use memcpy to copy those bytes in reverse into a second uint32_t and print both values with %08X. State which of the two printed lines would change on a big-endian machine and why.
Open the C workspaceCheck your understanding
On a little-endian machine you write uint32_t n = 0x11223344; unsigned char *p = (unsigned char *)&n; What are p[0] and n >> 24?
- p[0] is 0x44 and n >> 24 is 0x11
- p[0] is 0x11 and n >> 24 is 0x11
- p[0] is 0x44 and n >> 24 is 0x44
- p[0] is 0x11 and n >> 24 is 0x44
Show answer
Little-endian means the least significant byte lives at the lowest address, so p[0] is 0x44. The shift is arithmetic on the value rather than a walk through memory: n >> 24 drops the low 24 bits and leaves 0x11 on any machine. The answer claiming both are 0x44 is the usual trap of assuming byte order reverses the shift as well.