Docs
README
Bit Fields in C
Table of Contents
- β’Introduction
- β’Bit Field Declaration
- β’Memory Layout
- β’Accessing Bit Fields
- β’Unnamed Bit Fields
- β’Zero-Width Bit Fields
- β’Practical Applications
- β’Bit Fields with Unions
- β’Portability Considerations
- β’Best Practices
- β’Common Pitfalls
- β’Summary
Introduction
Bit fields are a feature in C that allows you to specify the exact number of bits a structure member should occupy. This enables:
- β’Memory optimization: Pack multiple values into single bytes/words
- β’Hardware interface: Map to hardware registers exactly
- β’Protocol implementation: Match binary protocol formats
- β’Flag management: Store multiple boolean flags efficiently
Basic Concept
// Without bit fields: uses at least 12 bytes
struct NormalFlags {
int isActive; // 4 bytes for one bit of info
int isVisible; // 4 bytes for one bit of info
int isEnabled; // 4 bytes for one bit of info
};
// With bit fields: uses only 4 bytes (or less)
struct BitFlags {
unsigned int isActive : 1; // 1 bit
unsigned int isVisible : 1; // 1 bit
unsigned int isEnabled : 1; // 1 bit
};
Bit Field Declaration
Syntax
struct StructureName {
type member_name : width;
type member_name : width;
// ...
};
Where:
- β’
type: Must beint,unsigned int,signed int, or_Bool(C99+) - β’
member_name: Name of the bit field - β’
width: Number of bits (1 to bit-width of type)
Examples
// Boolean flags (1 bit each)
struct Flags {
unsigned int flag1 : 1;
unsigned int flag2 : 1;
unsigned int flag3 : 1;
};
// Small integers
struct Date {
unsigned int day : 5; // 1-31 (needs 5 bits)
unsigned int month : 4; // 1-12 (needs 4 bits)
unsigned int year : 12; // 0-4095 (12 bits)
};
// Mixed sizes
struct Packet {
unsigned int version : 4;
unsigned int type : 4;
unsigned int length : 16;
unsigned int flags : 8;
};
Value Ranges
| Bits | Unsigned Range | Signed Range |
|---|---|---|
| 1 | 0-1 | -1 to 0 |
| 2 | 0-3 | -2 to 1 |
| 3 | 0-7 | -4 to 3 |
| 4 | 0-15 | -8 to 7 |
| 5 | 0-31 | -16 to 15 |
| 8 | 0-255 | -128 to 127 |
| n | 0 to (2^n - 1) | -(2^(n-1)) to (2^(n-1) - 1) |
Memory Layout
Packing Within Storage Units
Bit fields are packed into storage units (typically int-sized):
struct Example {
unsigned int a : 4; // Bits 0-3
unsigned int b : 4; // Bits 4-7
unsigned int c : 4; // Bits 8-11
unsigned int d : 4; // Bits 12-15
};
Memory (16 bits used, in 32-bit int):
βββββββββββββββββββββββββββββββββ
β unused β d β c β b β a β
β 16 bits β4b β4b β4b β4b β
βββββββββββββββββββββββββββββββββ
Bit: 31...16 15 11 7 3 0
Size Calculation
struct Compact {
unsigned int a : 3; // 3 bits
unsigned int b : 5; // 5 bits
unsigned int c : 8; // 8 bits
}; // Total: 16 bits, fits in one int
sizeof(struct Compact) = 4 bytes (size of int, minimum storage unit)
When Bit Fields Cross Boundaries
struct CrossBoundary {
unsigned int a : 30; // 30 bits
unsigned int b : 10; // 10 bits - may start new unit!
};
// Implementation-dependent:
// - Some compilers pack b starting at bit 30
// - Others start b in a new storage unit
Accessing Bit Fields
Assignment and Reading
struct Flags {
unsigned int active : 1;
unsigned int visible : 1;
unsigned int level : 4;
};
struct Flags f;
// Set values
f.active = 1;
f.visible = 0;
f.level = 7;
// Read values
printf("Active: %u\n", f.active);
printf("Level: %u\n", f.level);
// Values are automatically masked
f.level = 20; // Only lower 4 bits stored (20 & 0xF = 4)
printf("Level: %u\n", f.level); // Prints 4, not 20!
Increment and Decrement
struct Counter {
unsigned int count : 4; // 0-15
};
struct Counter c = {0};
c.count++; // Now 1
c.count += 5; // Now 6
c.count = 15;
c.count++; // Wraps to 0 (overflow)
Comparison
struct Flags f1, f2;
f1.active = 1;
f2.active = 1;
if (f1.active == f2.active) {
printf("Same active state\n");
}
Unnamed Bit Fields
Purpose
Unnamed bit fields create padding/gaps in the bit layout:
struct Register {
unsigned int flag1 : 1;
unsigned int : 3; // 3-bit gap (padding)
unsigned int flag2 : 1;
unsigned int : 0; // Force next field to new unit
unsigned int data : 8;
};
Visual Layout
struct Register {
unsigned int enable : 1; // Bit 0
unsigned int : 2; // Bits 1-2 (unused)
unsigned int mode : 3; // Bits 3-5
unsigned int : 2; // Bits 6-7 (unused)
unsigned int channel : 4; // Bits 8-11
};
Layout:
βββββββββββββββββββββββββββββββββββββββββββββββ
β unused β channel β XX β mode β XX βenableβ
β β 4b β 2b β 3b β 2b β 1b β
βββββββββββββββββββββββββββββββββββββββββββββββ
Bit: 11 7 5 2 0
Zero-Width Bit Fields
Purpose
A zero-width bit field forces the next bit field to start at the next storage unit boundary:
struct Aligned {
unsigned int a : 6;
unsigned int : 0; // Force alignment
unsigned int b : 6; // Starts in new storage unit
};
Memory Layout
struct Aligned {
unsigned int a : 6; // Bits 0-5 of first int
unsigned int : 0; // Force next unit
unsigned int b : 6; // Bits 0-5 of second int
};
sizeof(struct Aligned) = 8 bytes (two ints)
Memory:
βββββββββββββββββββββββ¬ββββββββββββββββββββββ
β unused β a β unused β b β
β 26 bits β 6 bits β 26 bits β 6 bits β
βββββββββββββββββββββββ΄ββββββββββββββββββββββ
First int Second int
Practical Applications
Application 1: Date Storage
// Compact date: only 3 bytes needed (but uses 4 for alignment)
struct CompactDate {
unsigned int day : 5; // 1-31
unsigned int month : 4; // 1-12
unsigned int year : 11; // 0-2047
};
struct CompactDate date;
date.day = 25;
date.month = 12;
date.year = 2024;
printf("Date: %u/%u/%u\n", date.month, date.day, date.year);
// Output: 12/25/2024
Application 2: Network Packet Header
// IPv4 header first 32 bits
struct IPv4Header {
unsigned int version : 4;
unsigned int ihl : 4; // Internet Header Length
unsigned int dscp : 6; // Differentiated Services
unsigned int ecn : 2; // Explicit Congestion Notification
unsigned int totalLength : 16;
};
Application 3: Status Register
struct StatusRegister {
unsigned int carry : 1;
unsigned int zero : 1;
unsigned int negative : 1;
unsigned int overflow : 1;
unsigned int interrupt : 1;
unsigned int : 3; // Reserved
};
void checkStatus(struct StatusRegister sr) {
if (sr.zero) printf("Result is zero\n");
if (sr.negative) printf("Result is negative\n");
if (sr.overflow) printf("Overflow occurred\n");
}
Application 4: File Permissions
struct FilePermissions {
unsigned int ownerRead : 1;
unsigned int ownerWrite : 1;
unsigned int ownerExecute : 1;
unsigned int groupRead : 1;
unsigned int groupWrite : 1;
unsigned int groupExecute : 1;
unsigned int otherRead : 1;
unsigned int otherWrite : 1;
unsigned int otherExecute : 1;
};
void printPermissions(struct FilePermissions p) {
printf("%c%c%c%c%c%c%c%c%c\n",
p.ownerRead ? 'r' : '-',
p.ownerWrite ? 'w' : '-',
p.ownerExecute ? 'x' : '-',
p.groupRead ? 'r' : '-',
p.groupWrite ? 'w' : '-',
p.groupExecute ? 'x' : '-',
p.otherRead ? 'r' : '-',
p.otherWrite ? 'w' : '-',
p.otherExecute ? 'x' : '-');
}
// Output: rwxr-xr-x
Bit Fields with Unions
Accessing as Bits and Whole Value
union RegisterAccess {
unsigned int value;
struct {
unsigned int bit0 : 1;
unsigned int bit1 : 1;
unsigned int bit2 : 1;
unsigned int bit3 : 1;
unsigned int bit4 : 1;
unsigned int bit5 : 1;
unsigned int bit6 : 1;
unsigned int bit7 : 1;
} bits;
};
union RegisterAccess reg;
// Set individual bits
reg.bits.bit0 = 1;
reg.bits.bit3 = 1;
reg.bits.bit7 = 1;
printf("Value: 0x%02X\n", reg.value); // 0x89
// Set whole value and read bits
reg.value = 0x55; // Binary: 01010101
printf("Bit 0: %u\n", reg.bits.bit0); // 1
printf("Bit 1: %u\n", reg.bits.bit1); // 0
printf("Bit 2: %u\n", reg.bits.bit2); // 1
Hardware Register Mapping
union ControlRegister {
unsigned char value;
struct {
unsigned char enable : 1;
unsigned char direction : 1;
unsigned char speed : 2;
unsigned char mode : 2;
unsigned char reserved : 2;
} fields;
};
// Read from hardware
union ControlRegister ctrl;
ctrl.value = read_hardware_register(0x1234);
// Check fields
if (ctrl.fields.enable) {
printf("Device enabled, speed=%u, mode=%u\n",
ctrl.fields.speed, ctrl.fields.mode);
}
// Modify and write back
ctrl.fields.speed = 3;
write_hardware_register(0x1234, ctrl.value);
Portability Considerations
Implementation-Defined Behavior
Several aspects of bit fields are implementation-defined:
- β’Bit ordering: Left-to-right or right-to-left
- β’Storage unit size: May vary by compiler/platform
- β’Signedness of plain
int: May be signed or unsigned - β’Packing across unit boundaries: May or may not split fields
Endianness Issues
struct NetworkHeader {
unsigned int version : 4;
unsigned int length : 4;
};
// On little-endian: version in lower bits
// On big-endian: version in upper bits
// May not match network byte order!
Portable Alternative
// Instead of bit fields for portable code:
#define VERSION_MASK 0x0F
#define VERSION_SHIFT 0
#define LENGTH_MASK 0xF0
#define LENGTH_SHIFT 4
unsigned char header;
// Set version
header = (header & ~VERSION_MASK) | ((version << VERSION_SHIFT) & VERSION_MASK);
// Get version
int version = (header & VERSION_MASK) >> VERSION_SHIFT;
Safe Practices for Portability
// 1. Always use unsigned int for bit fields
struct Good {
unsigned int field : 4; // Predictable behavior
};
// 2. Don't rely on specific layout for cross-platform data
// 3. Use explicit byte/bit manipulation for protocols
// 4. Document assumptions about bit ordering
Best Practices
1. Use Unsigned Types
// GOOD: Predictable behavior
struct Flags {
unsigned int flag : 1; // 0 or 1
};
// BAD: Sign extension issues
struct BadFlags {
int flag : 1; // Could be 0 or -1!
};
2. Match Hardware Documentation
// When mapping to hardware registers, match the datasheet exactly
struct HardwareReg {
unsigned int enable : 1; // Bit 0 as per datasheet
unsigned int : 3; // Bits 1-3 reserved
unsigned int mode : 4; // Bits 4-7 as per datasheet
};
3. Consider Alignment
// Group bit fields by storage unit for efficiency
struct Efficient {
unsigned int a : 8;
unsigned int b : 8;
unsigned int c : 8;
unsigned int d : 8; // All fit in one 32-bit int
};
4. Document the Layout
/*
* Status Register Layout:
* Bit 0: Enable flag
* Bit 1: Ready flag
* Bits 2-3: Reserved
* Bits 4-7: Mode selector
*/
struct StatusReg {
unsigned int enable : 1;
unsigned int ready : 1;
unsigned int : 2;
unsigned int mode : 4;
};
5. Don't Take Addresses
struct Flags {
unsigned int a : 4;
unsigned int b : 4;
};
struct Flags f;
// unsigned int *ptr = &f.a; // ERROR! Can't take address of bit field
unsigned int *ptr = (unsigned int*)&f; // OK: address of whole struct
Common Pitfalls
Pitfall 1: Overflow Without Warning
struct Counter {
unsigned int count : 4; // Max value: 15
};
struct Counter c;
c.count = 20; // Silently becomes 4 (20 & 0xF)
Pitfall 2: Sign Extension
struct Signed {
int value : 4; // Signed 4-bit: -8 to 7
};
struct Signed s;
s.value = 8; // Becomes -8 (overflow)
printf("%d\n", s.value); // Prints -8
Pitfall 3: Assuming Specific Layout
// This structure may have different layouts on different systems!
struct Protocol {
unsigned int version : 4;
unsigned int type : 4;
unsigned int length : 16;
};
// Don't use for network protocols or file formats!
Pitfall 4: Width Exceeding Type Size
// ERROR: width larger than type allows
struct Invalid {
unsigned int value : 64; // Error if int is 32 bits!
};
Pitfall 5: Using with Arrays or Pointers
struct BitField {
unsigned int field : 4;
};
struct BitField arr[10];
// Can't create: unsigned int (*p) : 4; // Invalid syntax
Summary
Key Concepts
| Concept | Description |
|---|---|
| Bit Field | Structure member with specified bit width |
| Width | Number of bits (1 to type bit-width) |
| Packing | Bit fields packed into storage units |
| Unnamed | Unnamed bit fields for padding |
| Zero-Width | Forces alignment to next storage unit |
Syntax Reference
// Basic declaration
struct Name {
unsigned int field : width;
};
// With padding
struct Padded {
unsigned int a : 4;
unsigned int : 4; // 4-bit padding
unsigned int b : 4;
};
// Force alignment
struct Aligned {
unsigned int a : 10;
unsigned int : 0; // Next starts on new unit
unsigned int b : 10;
};
When to Use Bit Fields
β Good use cases:
- β’Hardware register mapping (same platform only)
- β’Memory-constrained embedded systems
- β’Compact internal data structures
- β’Flag collections
β Avoid for:
- β’Cross-platform binary formats
- β’Network protocols
- β’File formats
- β’When you need field addresses
Quick Size Reference
struct Sizes {
unsigned int a : 1; // 1 bit: 0-1
unsigned int b : 4; // 4 bits: 0-15
unsigned int c : 8; // 8 bits: 0-255
unsigned int d : 16; // 16 bits: 0-65535
};
Navigation
| Previous | Up | Next |
|---|---|---|
| Unions | Structures and Unions | File Handling |