C Bit Fields — Packed Data, Bit-Level Struct Members, and Flags
In this tutorial, you will learn about C Bit Fields. We cover key concepts, practical examples, and best practices to help you master this topic.
C bit fields allow struct members to be specified with exact bit widths, enabling compact data storage, flag packing, and direct hardware register mapping with minimal memory waste.
Why It Matters
Bit fields let you define data structures that use exactly the bits they need. In Embedded Systems where RAM is measured in kilobytes, every bit counts. Network protocol headers specify field sizes in bits, and bit fields map directly to those specifications. Device driver registers often use individual bits for control flags.
Real-World Use
Network packet headers (TCP, IP, Ethernet) have fields measured in bits. The TCP header's data offset field is 4 bits, and flags are individual bits. Hardware control registers use single bits for enable/disable flags. File system inodes pack metadata into bit fields. Durga Antivirus Pro uses bit fields for compact feature flags in scan configurations.
What You Will Learn
- Defining bit fields with specific widths
- Packing flags into minimal space
- Bit field portability and limitations
- Comparing bit fields with manual bit manipulation
Learning Path
flowchart LR A[Unions] --> B[Bit Fields
You are here] B --> C[Typedef] C --> D[Void Pointers] D --> E[File I/O] style B fill:#f90,color:#fff
Defining Bit Fields
A bit field is a struct member with a specified width in bits, separated by a colon:
#include <stdio.h>
struct BitField {
unsigned int a : 3; // 3 bits (0-7)
unsigned int b : 4; // 4 bits (0-15)
unsigned int c : 5; // 5 bits (0-31)
// Total: 12 bits, but typically stored in 4 bytes
};
int main() {
struct BitField bf;
bf.a = 7; // Max value for 3 bits: 7
bf.b = 15; // Max value for 4 bits: 15
bf.c = 31; // Max value for 5 bits: 31
printf("a = %u\n", bf.a);
printf("b = %u\n", bf.b);
printf("c = %u\n", bf.c);
printf("Size of struct: %zu bytes\n", sizeof(bf));
// Likely 4 bytes on most platforms
// Overflow is truncated
bf.a = 10; // 10 = 1010 in binary, truncated to 3 bits = 2 (010)
printf("a after overflow: %u\n", bf.a); // 2
return 0;
}
Expected output:
a = 7
b = 15
c = 31
Size of struct: 4 bytes
a after overflow: 2
Bit Field Syntax
struct Flags {
unsigned int flag1 : 1; // Single bit
unsigned int flag2 : 2; // Two bits
int signed_field : 4; // Signed 4-bit field (-8 to 7)
unsigned int : 0; // Force alignment to next unit
unsigned int padding : 5; // Unnamed padding bits
};
Flag Packing with Bit Fields
Bit fields excel at representing boolean flags compactly:
#include <stdio.h>
// 8 flags packed into 1 byte
struct FilePermissions {
unsigned int owner_read : 1;
unsigned int owner_write : 1;
unsigned int owner_exec : 1;
unsigned int group_read : 1;
unsigned int group_write : 1;
unsigned int group_exec : 1;
unsigned int other_read : 1;
unsigned int other_write : 1;
// Total: 8 bits = 1 byte
};
int main() {
struct FilePermissions perms = {0};
// Set permissions
perms.owner_read = 1;
perms.owner_write = 1;
perms.owner_exec = 1;
perms.group_read = 1;
perms.other_read = 1;
printf("Owner: %s%s%s\n",
perms.owner_read ? "r" : "-",
perms.owner_write ? "w" : "-",
perms.owner_exec ? "x" : "-");
printf("Group: %s%s%s\n",
perms.group_read ? "r" : "-",
perms.group_write ? "w" : "-",
perms.group_exec ? "x" : "-");
printf("Other: %s%s\n",
perms.other_read ? "r" : "-",
perms.other_write ? "w" : "-");
printf("Size of permissions: %zu byte(s)\n", sizeof(perms));
// Check individual flags
if (perms.owner_write) {
printf("Owner can write.\n");
}
return 0;
}
Expected output:
Owner: rwx
Group: r--
Other: r-
Size of permissions: 1 byte(s)
Hardware Register Mapping
Bit fields are commonly used to map hardware registers:
#include <stdio.h>
// Hypothetical UART control register (8 bits)
struct UARTControl {
unsigned int enable : 1; // Bit 0
unsigned int tx_enable : 1; // Bit 1
unsigned int rx_enable : 1; // Bit 2
unsigned int parity : 2; // Bits 3-4: 0=none, 1=odd, 2=even
unsigned int stop_bits : 1; // Bit 5: 0=1 stop bit, 1=2 stop bits
unsigned int data_bits : 2; // Bits 6-7: 0=5, 1=6, 2=7, 3=8
};
// Simulated register address
#define UART_BASE ((volatile struct UARTControl*)0x40001000)
int main() {
// In real code, you would access the hardware register
volatile struct UARTControl uart = {0};
// Configure UART
uart.enable = 1;
uart.tx_enable = 1;
uart.rx_enable = 1;
uart.parity = 0; // No parity
uart.stop_bits = 0; // 1 stop bit
uart.data_bits = 3; // 8 data bits
printf("UART configured: 8N1\n");
printf("Register value: 0x%02X\n", *(unsigned char*)&uart);
return 0;
}
Bit Fields vs Manual Bit Manipulation
Bit fields provide cleaner syntax for bit-level operations:
#include <stdio.h>
#include <stdint.h>
// Method 1: Bit fields
struct StatusField {
unsigned int error : 1;
unsigned int warning : 1;
unsigned int mode : 2;
unsigned int data_ready : 1;
unsigned int : 3; // 3 unused padding bits
};
// Method 2: Manual bit operations
#define STATUS_ERROR (1 << 0)
#define STATUS_WARNING (1 << 1)
#define STATUS_MODE_SHIFT 2
#define STATUS_MODE_MASK (3 << STATUS_MODE_SHIFT)
#define STATUS_DATA_READY (1 << 4)
int main() {
// Bit field approach
struct StatusField sf = {0};
sf.error = 0;
sf.warning = 1;
sf.mode = 2;
sf.data_ready = 1;
// Manual approach
uint8_t status = 0;
status &= ~STATUS_ERROR; // Clear error bit
status |= STATUS_WARNING; // Set warning bit
status = (status & ~STATUS_MODE_MASK) | // Clear mode bits
(2 << STATUS_MODE_SHIFT); // Set mode to 2
status |= STATUS_DATA_READY; // Set data ready
printf("Bit field result: 0x%02X\n", *(uint8_t*)&sf);
printf("Manual result: 0x%02X\n", status);
// Check flags
if (sf.warning) printf("Warning flag set (bit field)\n");
if (status & STATUS_WARNING) printf("Warning flag set (manual)\n");
return 0;
}
When to Use Each Approach
| Approach | Pros | Cons |
|---|---|---|
| Bit fields | Clean syntax, self-documenting | Implementation-defined layout, slower |
| Manual bits | Portable, full control, faster | Error-prone, harder to read |
For portable code, prefer manual bit operations. For hardware-specific code, bit fields are convenient.
Signed Bit Fields
Bit fields can be signed or unsigned:
#include <stdio.h>
struct SignedBitField {
signed int temperature : 8; // -128 to 127
unsigned int humidity : 7; // 0 to 127
signed int offset : 4; // -8 to 7
};
int main() {
struct SignedBitField sensor;
sensor.temperature = -20; // OK for 8-bit signed
sensor.humidity = 65; // OK for 7-bit unsigned
sensor.offset = -5; // OK for 4-bit signed
printf("Temperature: %d\n", sensor.temperature);
printf("Humidity: %u\n", sensor.humidity);
printf("Offset: %d\n", sensor.offset);
// Overflow examples
sensor.offset = 10; // 10 = 1010 in 4-bit signed = -6
printf("Offset overflow: %d\n", sensor.offset); // -6
return 0;
}
Portability Considerations
Bit fields have several implementation-defined aspects:
- Whether fields are allocated left-to-right or right-to-left within a storage unit
- Whether the base type (e.g.,
int) is signed or unsigned - The maximum bit width allowed
- Alignment of bit fields across storage unit boundaries
#include <stdio.h>
// PACKED attribute (compiler-specific)
struct __attribute__((packed)) PackedBitField {
unsigned int a : 3;
unsigned int b : 3;
unsigned int c : 2;
// Total: 8 bits, packed into 1 byte
};
int main() {
struct PackedBitField pb = {5, 6, 2};
unsigned char *bytes = (unsigned char*)&pb;
printf("Size: %zu bytes\n", sizeof(pb));
printf("Raw byte: 0x%02X\n", bytes[0]);
return 0;
}
Why this matters
If you write network protocols or file formats that cross platforms, you cannot rely on bit field layout being consistent. Use manual bit shifting for portable Serialization.
Common Mistakes
1. Assuming Bit Field Layout
Bit fields may be packed right-to-left or left-to-right depending on the platform. Never assume the order for cross-platform data.
2. Taking Address of a Bit Field
struct { unsigned int flag : 1; } s;
unsigned int *p = &s.flag; // ERROR: cannot take address of bit field
Bit fields do not have individual addresses because they share bytes with other fields.
3. Overflowing Bit Fields
struct { unsigned int x : 2; } s;
s.x = 5; // 5 = 101, truncated to 2 bits = 01 = 1
The value is silently truncated to fit the bit width.
4. Using Bit Fields for Thread Synchronization
Bit field writes are not atomic. Two threads writing adjacent bit fields can interfere with each other.
5. Relying on sizeof for Bit Field Layout
struct { unsigned int a : 1; unsigned int b : 15; } s;
// sizeof may be 2 or 4 depending on the compiler
The size of the storage unit is implementation-defined.
Practice Questions
What is a bit field in C? A struct member with an explicitly specified width in bits, allowing compact storage of small values.
What is the syntax for a 3-bit unsigned integer bit field?
unsigned int field : 3;Can you take the address of a bit field? No. Bit fields share bytes with other fields and do not have individual memory addresses.
What is a signed 4-bit field's range? -8 to 7 (one bit for sign, three bits for magnitude in two's complement).
Challenge: Define a bit field structure for an IPv4 header (version 4 bits, IHL 4 bits, DSCP 6 bits, ECN 2 bits, total length 16 bits).
Mini Project: Compact Date Storage
#include <stdio.h>
// Store a date in 16 bits (instead of 3 ints = 12 bytes)
struct CompactDate {
unsigned int year : 7; // 0-127 (relative to 2000)
unsigned int month : 4; // 1-12
unsigned int day : 5; // 1-31
// Total: 16 bits = 2 bytes
};
int main() {
struct CompactDate cd;
cd.year = 26; // 2026
cd.month = 6;
cd.day = 28;
printf("Date: %d/%d/%d\n", cd.month, cd.day, 2000 + cd.year);
printf("Size: %zu bytes (instead of 12 for 3 ints)\n", sizeof(cd));
// Read raw storage
unsigned short *raw = (unsigned short*)&cd;
printf("Raw bits: 0x%04X\n", *raw);
// Decode manually
unsigned short r = *raw;
printf("Decoded: year=%d, month=%d, day=%d\n",
r >> 9, (r >> 5) & 0xF, r & 0x1F);
return 0;
}
FAQ
What is Next
Now that you understand bit fields, proceed to Typedef to learn how to create type aliases for cleaner code.