Bit Fields in C

The variable size of a standard type is always in bytes – not in fraction of bytes. For example, the size of a char type variable is 1, short type variable is 1, in type variable is 4 and so on. We can not have a variable of size half byte (4 bits) or 1 bit.

But we can keep some information in less that a byte or 8 bits. For example, the date of a month, whose value can be in the range of 1 to 31, can be kept in 5 bits. A boolean flag, whose value can be either true or false, can be kept in 1 bit.

But a boolean type variable takes 1 byte (or bits) space in memory. Any number, even if we declare it as ‘unsigned char’, takes at least 1 byte.

In some cases where we have resource limitations, saving a few bytes can make huge difference. We need to take significant effort to reduce memory consumption of a program running on low footprint embedded devices. One way to reduce a program’s memory consumption is to pack the variables as compact as possible.

In C programming, we can define bit field inside a struct or union to minimize memory requirement wherever possible. We can define a variable of size of as many bits are really required. Not that we have to allocate at least 1 byte where even 1 bit is sufficient.

Bit Fields – the Solution

Let’s consider this struct.

struct S1 {
    unsigned char flag1;  /*Range: 0-1*/
    unsigned char flag2;  /*Range: 0-1*/
    unsigned char flag3;  /*Range: 0-1*/

    unsigned char date;   /*Range: 1-31*/
    unsigned char num;    /*Range: 0-256*/
};

Here we have 5 member variables. First 3 variables, flag1, flag2 and flag3, are booleans whose value could be either 1 (true) or 0 (false). Next one is date – value could be in the range of 1 and 31. The last variable is a number ranging from 0 to 256.

We declared all variables as ‘unsigned char‘ – the data type of minimum length of 1 byte. So the size of this structure will be 5. Five variables, each with length 1.

But the whole 1 byte is not required for all the variables. The first three variable will require 1 bit each to hold o or 1. For the date variable, 5 bits are sufficient. So, the first 4 variable could be accommodated within 8 bits or 1 byte. The num variable will need one whole byte as its value can range from 0 to 256.

So the whole structure could be accommodated within 2 bytes. But it will take 5 bytes in memory in its current form.

Good news is that we can force it to take required 2 bytes. Here is the modified structure.

struct S2 {
    unsigned char flag1:1;
    unsigned char flag2:1;
    unsigned char flag3:1;

    unsigned char date:5;
    unsigned char num;
};

We can write a program to check that the size of this structure is really 2 bytes.

#include <stdio.h>

struct S2 {
    unsigned char flag1:1;
    unsigned char flag2:1;
    unsigned char flag3:1;

    unsigned char date:5;
    unsigned char num;
};

int main() {
    printf("%lu\n", sizeof(struct S2));
    return 0;
}
$ cc test.c
$ ./a.out 
2

We specified the size in bits for the variables where the whole byte is not required. The ‘:1‘ after ‘flag1‘ signifies that the size of ‘flag1‘ is 1 bit. Similarly, we specified the size of date as 5 bits.

So, we learnt the trick to save some bytes in our programs.

Things to Remember

So far all good. But we need to remember few things. Otherwise, we’ll not get the expected results.

Variable Ordering is Important

Let consider this structure.

struct S2 {
    unsigned char flag1:1;
    unsigned char flag2:1;
    unsigned char num;
    unsigned char flag3:1;

    unsigned char date:5;
};

Earlier, the variable, num, was the last variable. We put that between flag1 and flag2. You can expect that the size of this structure will be the same as before, i.e. 2. But in reality, it is of 3 bytes.

What happened here?

We can explain it this way. The first variable requires 1 bit. So, for that 1 byte is allocated as the data type is ‘unsigned char‘. Now we have 7 spare bits available. The next variable also requires 1 bit. As the first byte has 7 unused bits, the second variable is fitted there. The first byte now has 6 spare bits. But the third variable needs 8 bits. The first byte, that has 6 unused bits left, can not accommodate that. So, a new byte is allocated. The 6 unused bits of the first byte will remain unused. The last two variables require 6 bits. That’s why another byte is allocated. So, total 3 bytes are required here.

We need to mindful about the bit size as well as the ordering of variables to get best effect.

Unnamed Bit Field

C supports an unnamed struct member of size 0 to force alignment on the next boundary.

struct S2 {
    unsigned char v1:3;
    unsigned char v2:5;
};

We can rightly expect that the size of this structure is 1 byte.

struct S2 {
    unsigned char v1:3;
    unsigned char : 0;
    unsigned char v2:5;
};

But size of the above structure will be 2. 1 byte will be allocated for the first variable v1 of size 3 bit. In that byte, there will be still 5 bits left in that byte to accommodate the last variable v2. But as we have an unnamed variable with size 0 between v1 and v2, the unused bits after v1 will not be used. This unnamed variable will force to align the next variable in next boundary. As this structure is 1 byte aligned, the v2 will start at the next byte. So, whole 2 bytes will be used for this structure.

Reference of Bit-Field

We can not get reference or memory location of a bit-field variable. Reference is basically the starting byte location of a variable in memory. But a bit-field can start to locate at any position of a byte – not necessarily from the starting of the byte. That’s why compiler prevents to get the location of a bit-field variable.

#include <stdio.h>

struct S2 {
    unsigned char v1:4;
    unsigned char v2;
};

int main() {
    struct S2 s;
    
    printf("Address of v1: %p\n", &s.v1);

    return 0;
}
$ cc -o test test.c
test.c: In function ‘main’:
test.c:11:35: error: cannot take address of bit-field ‘v1’
   11 |     printf("Address of v1: %p\n", &s.v1);
      |                                   ^

But we can get the location of a non-bit-field variable of the same structure.

#include <stdio.h>

struct S2 {
    unsigned char v1:4;
    unsigned char v2;
};

int main() {
    struct S2 s;
    
    printf("Address of v2: %p\n", &s.v2);

    return 0;
}
$ cc -o test test.c
$ ./test 
Address of v2: 0x7fff7268cfb7

We need to keep in mind that if we ever need the reference of a variable, we can not make that variable a bit-field.

Author: Srikanta

I write here to help the readers learn and understand computer programing, algorithms, networking, OS concepts etc. in a simple way. I have 20 years of working experience in computer networking and industrial automation.


If you also want to contribute, click here.

Leave a Reply

Your email address will not be published. Required fields are marked *

0
0
0
0
1
0