C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
64: Unions and Bit Fields
I've seen this exact bug pop up in countless embedded projects. A developer is trying to interface with a piece of hardware—say, a network chip or a sensor—that sends a 16-bit status register. To make the code readable, they use a union to "overlay" a bit field structure on top of a raw integer. It looks elegant on paper, but then the program starts reporting that the system is "Healthy" when the hardware is actually screaming "Critical Error."
#include <stdio.h>
#include <stdint.h>
typedef union {
uint16_t raw;
struct {
uint16_t error : 1;
uint16_t warning : 1;
uint16_t ready : 1;
uint16_t reserved : 13;
} flags;
} StatusRegister;
int main() {
StatusRegister reg;
// Hardware sends 0x8000 (The most significant bit is set)
// In the manual, the MSB is the 'error' bit.
reg.raw = 0x8000;
if (reg.flags.error) {
printf("Error detected!\n");
} else {
printf("System OK\n");
}
return 0;
}
When you run this on a standard x86 machine, it prints "System OK". Wait, what? We set the raw value to 0x8000, and the manual says the error bit is the most significant bit. Why is the error flag coming back as 0?
The Bit-Order Trap
Here is the thing about bit fields: the C standard gives the compiler a huge amount of leeway in how it packs them. On most common compilers (like GCC or Clang on little-endian systems), bit fields are packed from the least significant bit (LSB) to the most significant bit (MSB).
In the code above, you told the compiler that error is the first 1-bit field. The compiler put it at the very bottom (bit 0). But the hardware manual told you the error bit is at the top (bit 15). You've essentially mapped your "Error" variable to the "Reserved" section of the hardware register and your "Reserved" section to the actual error bit. I'll be honest: relying on bit fields for hardware mapping is a gamble unless you've read your compiler's ABI manual cover-to-cover.
Correcting the Layout for Little-Endian
If you absolutely want to use a union and bit fields for readability, you have to define them in the order they appear in memory, not the order they appear in the documentation's diagram. To fix the bug, we need to move the error bit to the end of the struct so it aligns with the MSB of the 16-bit word.
typedef union {
uint16_t raw;
struct {
uint16_t reserved : 13; // Bottom 13 bits
uint16_t ready : 1;
uint16_t warning : 1;
uint16_t error : 1; // Top bit (MSB)
} flags;
} StatusRegister;
Now, when reg.raw = 0x8000 is set, the 15th bit is toggled, which now maps directly to flags.error. It works, but you're still tied to a specific endianness and compiler behavior. In professional production code, I usually steer my team toward explicit bitmasking (reg & ERROR_MASK), but unions are incredibly powerful for other tasks where you need to view the same memory in two different ways without casting pointers everywhere.
When Unions Actually Shine
Unions aren't just for bit-hacking. They are most useful when you have a data structure that can be one of several different things, but never more than one at a time. Think of a "Variant" type. If you're building a protocol where a packet could be a Command, a DataPayload, or a KeepAlive, using a union allows you to allocate only enough memory for the largest of the three, rather than summing their sizes.
Just remember: the union doesn't know which member is currently "active." You'll almost always pair a union with an enum inside a larger struct to keep track of what's actually stored there. This is called a "tagged union."
The Danger of Type Punning
You'll often see people use unions to "cheat" the type system—for example, putting a float in a union and reading it back as an int to look at the raw IEEE 754 bits. This is called type punning. While most modern compilers support this, be careful. Strictly speaking, reading from a union member other than the one last written to can trigger undefined behavior in some strict interpretations of the C standard, though in practice, it's the standard way to handle raw bit access in systems programming.
📋 Practical Task
Building a Compact Network Packet Header
You are writing a driver for a legacy network protocol. The protocol uses a 32-bit header. You need to create a PacketHeader union that allows you to set the entire header as a uint32_t, but also allows you to access the following fields individually:
- Version: 4 bits (Least Significant Bits)
- Priority: 3 bits
- PayloadType: 5 bits
- SequenceNumber: 20 bits (Most Significant Bits)
Your task:
- Define a
unioncalledPacketHeadercontaining auint32_t rawand astructwith the bit fields listed above. - In your
mainfunction, create an instance ofPacketHeader. - Set the
rawvalue to0x12345678. - Print the values of the
Version,Priority,PayloadType, andSequenceNumberto the console. - Change the
Priorityto 7 and theVersionto 2 using the bit field members, then print the newrawhex value to verify the changes.
There are no comments for now.