Skip to content

C Bit Fields — Packed Data, Bit-Level Struct Members, and Flags

DodaTech Updated 2026-06-28 9 min read

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

  1. What is a bit field in C? A struct member with an explicitly specified width in bits, allowing compact storage of small values.

  2. What is the syntax for a 3-bit unsigned integer bit field? unsigned int field : 3;

  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.

  4. What is a signed 4-bit field's range? -8 to 7 (one bit for sign, three bits for magnitude in two's complement).

  5. 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

Are bit fields portable across compilers?

Not entirely. The order (left-to-right or right-to-left), alignment, and whether int is signed or unsigned are implementation-defined.

Can I use bit fields in an array?

No. You cannot create an array of bit fields. You can create arrays of structs that contain bit fields.

What is the maximum bit width for a bit field?

The maximum is the width of the base type. For unsigned int, typically 32 bits. Some compilers allow larger with extensions.

How do I reset a bit field to zero?

Simple assignment: my_bitfield = 0; resets all bits to zero regardless of their widths.

Are bit fields initialized by default?

No. Bit fields in automatic storage are uninitialized (contain garbage). Global and static bit fields are zero-initialized.

What is Next

Now that you understand bit fields, proceed to Typedef to learn how to create type aliases for cleaner code.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C