Bit Fields, Bit Manipulation Idioms, and Packed Structs
What This Concept Is
Sometimes one byte is too coarse. Three C-level tools give you bit-level control:
-
Bit fields: struct members with an explicit bit width. The compiler packs them into a storage unit.
struct Flags {
unsigned int ready : 1;
unsigned int mode : 3;
unsigned int code : 4;
}; -
Bit manipulation idioms using
&,|,^,~,<<,>>:- set bit
n:x |= 1u << n; - clear bit
n:x &= ~(1u << n); - toggle bit
n:x ^= 1u << n; - test bit
n:(x >> n) & 1u - extract bits
[hi:lo]:(x >> lo) & ((1u << (hi - lo + 1)) - 1u)
- set bit
-
Packed structs (compiler extension:
__attribute__((packed))in GCC/Clang,#pragma pack(1)elsewhere): remove all padding, giving you exact byte-level layout at the cost of possibly unaligned field accesses.
Why It Matters Here
These tools are the bridge from C-level types to hardware reality:
- network packets and on-disk formats specify fields down to the bit
- hardware registers (device control, CPU flags) use 1-, 2-, 3-bit fields side by side
- bitmap / bitset data structures let a million flags fit in 128 KB
- feature flags, permission masks, and compression tables all live here
Concrete Example
An IPv4 header's first 32 bits:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL | TOS | Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Two C ways to read Version and IHL from an existing 32-bit word:
uint32_t word = 0x45000034; /* example first 32 bits */
unsigned version = (word >> 28) & 0xFu; /* 4 */
unsigned ihl = (word >> 24) & 0xFu; /* 5 */
or with bit fields (note: endianness and compiler ordering make this non-portable for wire formats):
struct IPv4First {
unsigned ihl : 4;
unsigned version : 4;
unsigned tos : 8;
unsigned total_length : 16;
};
A packed struct for a small file header:
#pragma pack(push, 1)
struct Header {
uint8_t magic[4]; /* offset 0 */
uint16_t version; /* offset 4, no padding */
uint32_t length; /* offset 6, no padding */
}; /* sizeof = 10, not 16 */
#pragma pack(pop)
Common Confusion / Misconception
"Bit fields are portable." The C standard leaves many things implementation-defined: whether fields pack from the most-significant or least-significant end of the storage unit, whether signed fields use two's complement, how int : 0 zero-width fields behave. For a wire format, use explicit shift-and-mask against a uint32_t.
"Packed structs are free." On strict-alignment architectures, misaligned field access may trap or be slow. Modern x86-64 and ARMv8 tolerate it, but the cost is real on hot paths.
"1 << 31 is fine." On a 32-bit int, 1 << 31 is undefined behavior in C (it overflows signed int). Use 1u << 31 or UINT32_C(1) << 31.
How To Use It
When bit-level layout matters:
- For wire formats: read and write through explicit shift and mask on
uint32_t(oruint64_t). - For hardware registers: use
volatileaccesses; document bit fields in comments next to constants. - For bitmaps: use
uint64_twords and defineset,clear,test,popcounthelpers once. - For packed structs: only when
sizeofmust equal a wire size; combine withstatic_assert(sizeof(S) == N).
Check Yourself
- What does
x & -xcompute, and why? - Why is
(x >> 31) & 1uthe right way to get the sign bit of a 32-bit integer? - Why are bit fields often a poor choice for parsing network headers even though they look tidy?
Mini Drill or Application
#include <stdio.h>
#include <stdint.h>
static unsigned popcount32(uint32_t x) {
unsigned c = 0;
while (x) { x &= x - 1; c++; } /* Kernighan's trick */
return c;
}
int main(void) {
uint32_t v = 0xDEADBEEFu;
printf("v=0x%08x popcount=%u lowest-set=%u\n",
v, popcount32(v), __builtin_ctz(v));
uint32_t flags = 0;
flags |= (1u << 3); /* set bit 3 */
flags |= (1u << 7); /* set bit 7 */
flags &= ~(1u << 3); /* clear bit 3 */
printf("flags=0x%08x bit7=%u bit3=%u\n",
flags, (flags >> 7) & 1u, (flags >> 3) & 1u);
return 0;
}
Build: gcc -Wall -Wextra -o bits bits.c. Extend with a popcount test that compares against __builtin_popcount on 1000 random values.