In-Depth Analysis of Bit-Fields in C Language: From Principles to Practice for Embedded Memory Optimization

In the advanced applications of the C language, especially in the field of embedded development, “memory” is often the most precious resource. When we need to handle hardware registers, binary protocols, or massive data storage, ordinary variables occupying 1 byte or 4 bytes can easily lead to resource wastage. Bit-fields (Bit-Field) act like a “space magician”, allowing precise control over the use of binary bits and maximizing memory utilization. Today, we will thoroughly understand bit-fields from principles, usage to practical applications.

1. Why Do We Need Bit-Fields? Let’s Look at a Real Scenario

Suppose you are developing a smart watch that needs to store “date” information: day (1-31), month (1-12), and day of the week (1-7). If you define it using ordinary variables:

// Normal structure: total size 4+4+4=12 bytes (int is 4 bytes by default)

struct NormalDate {

int day; // Actually only needs to store0-31, using4 bytes is too wasteful

int month; // Actually only needs to store0-12,4 bytes are severely redundant

int week; // Actually only needs to store0-7,4 bytes are completely unnecessary

};

The memory of the smart watch is already limited, 12 bytes to store 3 small values is clearly unreasonable. However, after restructuring with bit-fields:

// Bit-field structure: total size 2 bytes (precisely allocated bits, no redundancy)

struct BitDate {

unsigned char day : 5; // 5 bits: maximum representation31 (2^5-1=31), just meets the requirement for day

unsigned char month : 4; // 4 bits: maximum representation15 (2^4-1=15), covers the range for month

unsigned char week : 3; // 3 bits: maximum representation7 (2^3-1=7), perfectly fits the day of the week

};

Only 2 bytes achieve the same functionality, reducing memory usage by 83%— this is the core value of bit-fields:using the least bits to store necessary data.

2. The Essence of Bit-Fields: “Labeling” Binary Bits

The essence of bit-fields is to specify the number of binary bits occupied for each member in a structure, allowing the compiler to allocate memory according to our rules instead of the default “byte” alignment. The syntax rules are very simple:

struct Bit-field structure name {

Base type member name : number of bits occupied; // core format

Base type member name : number of bits occupied;

// …

};

Key Details Explained:

1.Base type: determines the “minimum storage unit” for the bit-field, commonly used types are unsigned char (1 byte = 8 bits), unsigned short (2 bytes = 16 bits), to avoid wasting space with int (default 4 bytes);

2.Number of bits occupied: must be less than or equal to the total number of bits of the base type (for example, unsigned char members can occupy a maximum of 8 bits);

3.Member value range: if occupying n bits, the maximum value for unsigned members is 2^n – 1 (for example, 5 bits maximum is 31), signed members need to reserve 1 bit for the sign, maximum value is 2^(n-1) – 1.

3. Storage Rules for Bit-Fields: Common Pitfalls for Beginners

Many people learning about bit-fields may wonder, “How are the bits of members arranged? How is the total occupied space calculated?” In fact, remembering 3 core rules can easily solve this:

Rule 1: By default, do not cross bytes; if remaining bits are insufficient, “start anew”

Bit-field members will not be split across two bytes by default. If the remaining bits in the current byte are insufficient to store the next member, the compiler will automatically use the next base type size byte to store that member.

For example, with struct BitDate (base type unsigned char, 1 byte = 8 bits):

First memberday:5: occupies byte 1 bits 0-4, remaining 3 bits;

Second membermonth:4: byte 1 has remaining 3 bits insufficient, using byte 2 bits 0-3, remaining 4 bits;

Third memberweek:3: byte 2 has remaining 4 bits sufficient, occupying 4-6 bits;

Total occupied:2 unsigned char → 2 bytes.

Rule 2: Can force cross bytes (use with caution)

If you want a member to be split across two bytes, you can add :0 before the member to force the end of the current byte’s usage:

struct CrossByte {

unsigned char a : 3; // Byte1: bits 0-2

unsigned char : 0; // Force end of byte1, remaining5 bits discarded

unsigned char b : 4; // Byte2: bits 0-3 (even if byte1 has remaining bits)

};

// Total occupied:2 bytes (if not adding:0,a and b could fit in1 byte)

Note: Forcing cross bytes will waste memory, use only when there are special requirements for hardware registers.

Rule 3: Compiler alignment differences (need testing)

Different compilers may have different “bit arrangement orders” for bit-fields (whether to start storing from the high or low bit of the byte). For example:

GCC by default starts storing from the low bit of the byte (day:5 occupies bits 0-4 of byte 1);

Some compilers start storing from the high bit (day:5 occupies bits 3-7 of byte 1).

If developing hardware-related code, you need to use sizeof and binary printing to verify, to avoid compatibility issues.

4. Practical Applications of Bit-Fields: 3 Common Scenarios

Bit-fields are not just “decorative”, they can solve many pain points in actual development. Here are 3 of the most common scenarios:

Scenario 1: Operating Hardware Registers (Core Usage in Embedded)

Each bit of a hardware register has a specific function (such as controlling the LED on/off, reading sensor status), using bit-fields can directly map register bits without manually performing bitwise operations:

// Assuming a certain GPIO register address is0x40020000, with a total of8 bits

struct GPIOReg {

unsigned char led1 : 1; // Bit0: controlLED1 (1= on, 0= off)

unsigned char led2 : 1; // Bit1: controlLED2

unsigned char btn1 : 1; // Bit2: read button1 status (1= pressed, 0= not pressed)

unsigned char btn2 : 1; // Bit3: read button2 status

unsigned char : 4; // Bits4-7: reserved bits, no operation needed

} *gpio = (struct GPIOReg *)0x40020000;

// ControlLED1 on

gpio->led1 = 1;

// Read button1 status

if (gpio->btn1 == 1) {

// Button1 is pressed

}

Compared to traditional bitwise operations (*(unsigned char*)0x40020000 |= (1<<0)), bit-field code is more intuitive and less error-prone.

Scenario 2: Parsing Binary Protocols (e.g., Network, Serial Data)

Binary protocols (such as Modbus, CAN bus) often divide data packets by bits, using bit-fields can directly parse them without manually splitting bytes:

// A certain serial protocol data packet:1 byte, formatted as follows

// Bits0-3: temperature value (0-15℃)

// Bits4-6: humidity value (0-70%, step size10%)

// Bits7: alarm status (1= alarm, 0= normal)

struct SerialData {

unsigned char temp : 4; // Temperature

unsigned char humi : 3; // Humidity (actual value=humi*10)

unsigned char alarm : 1; // Alarm status

};

// After receiving1 byte of data, parse it

unsigned char recv_data = 0x85; // Binary:10000101

struct SerialData *data = (struct SerialData *)&recv_data;

printf(“Temperature: %d℃\n”, data->temp); // Output:5℃ (0101=5)

printf(“Humidity: %d%%\n”, data->humi * 10); // Output:0% (000=0)

printf(“Alarm status: %s\n”, data->alarm ? “Alarm” : “Normal”); // Output: Alarm (1)

Scenario 3: Massive Data Storage (e.g., Logs, Sensor Data)

If you need to store millions of sensor data points (e.g., one every 10ms , containing 4 values of 0-15), using bit-fields can significantly reduce storage size:

// Normal storage:4 unsigned char, 4 bytes/point → 1 million points require4MB

struct NormalSensor {

unsigned char val1;

unsigned char val2;

unsigned char val3;

unsigned char val4;

};

// Bit-field storage:4 4-bit members, total 16 bits = 2 bytes/point → 1 million points only require2MB

struct BitSensor {

unsigned char val1 : 4;

unsigned char val2 : 4;

unsigned char val3 : 4;

unsigned char val4 : 4;

};

The storage size is directly halved, which is friendly for embedded devices’ Flash/SD cards.

5. The “Pitfalls” of Bit-Fields: These Taboos to Remember

Although bit-fields are useful, they also have many limitations, and beginners can easily fall into pitfalls:

1.Cannot take address: Bit-field members are not aligned by “bytes”, so you cannot use & to take the address, nor can you use pointer operations:

struct BitDate date;

// &date.day; // Error: cannot take address of bit-field member

2.Arrays not supported: You cannot define an array of bit-fields (e.g., unsigned char arr[5]:3), if you need to store in bulk, you must use an array of structures;

3.Compiler compatibility: Different compilers may have different storage orders and alignment methods for bit-fields, cross-platform development requires compatibility testing;

4.Do not overuse: For ordinary PC applications (such as desktop software), memory is sufficient, using bit-fields may increase code complexity, it is recommended to use ordinary variables first.

6. Thought Questions: Test Your Understanding in Practice

Based on today’s content, let’s do a small exercise: use bit-fields to store “time” information — hours (0-23), minutes (0-59), seconds (0-59), how many bits are needed at a minimum? How to define the structure?

Analysis Thought:

1.Hours 0-23: needs5 bits (2^5=32 ≥23);

2.Minutes 0-59: needs6 bits (2^6=64 ≥59);

3.Seconds 0-59: also needs6 bits;

4.Total bits:5+6+6=17 bits → need 2 unsigned short (2 bytes = 16 bits, not enough; 3 bytes = 24 bits, sufficient) or 1 unsigned int (4 bytes = 32 bits).

Reference Definition:

// Total occupied4 bytes (unsigned int), or3 bytes (requires special handling, compatibility is poor)

struct BitTime {

unsigned int hour : 5; // 5 bits:0-23

unsigned int minute : 6;// 6 bits:0-59

unsigned int second : 6;// 6 bits:0-59

};

Do you have a better definition method? Feel free to share your thoughts in the comments!

Summary

Bit-fields are an efficient tool designed in the C language for “resource-constrained scenarios”. The core value isprecise bit control and memory saving. They are indispensable in embedded development and binary protocol parsing, but attention must be paid to compiler compatibility and usage taboos. Remember:using bit-fields in suitable scenarios can maximize their value.

If your project is currently troubled by “memory shortage”, consider trying bit-fields; if you have specific usage questions, feel free to leave comments for discussion!

(Note: Personal opinion, for reference only)

Leave a Comment