C Bit Fields
A bit field is a struct member whose size you specify in bits instead of bytes, so several small values can share a single storage unit — usually one unsigned int — instead of each getting its own byte or word. Bit fields matter most when you model hardware registers, network packet headers, file-format flags, or any place where you want to pack many small on/off or narrow-range values as tightly as possible. Used well they make flag-heavy code compact and self-documenting; used carelessly they introduce some of the most portability-breaking bugs in C, because the standard leaves several of their layout details up to the compiler.
Overview: How Bit Fields Work
Ordinarily every member of a struct occupies at least one whole byte, and often more once padding for alignment is added. A bit field changes this: it tells the compiler "this member only needs N bits, so pack it as tightly as possible next to its neighbors." You declare one with a colon and a constant width after the member name:
struct Flags {
unsigned int isReady : 1;
unsigned int mode : 2;
};
Here isReady needs only 1 bit (0 or 1) and mode needs only 2 bits (values 0-3). Internally, the compiler groups consecutive bit fields into what the C standard calls an allocation unit, usually a block the size of the declared type — 4 bytes for unsigned int on almost every modern platform. As long as the running total of requested bits still fits inside that unit, a new field packs into the same unit as the previous ones. Once a field would overflow the unit, the compiler starts a new unit for it. Whether the compiler is allowed to split a single field across two units, and which end of the unit fields fill from first, are both left implementation-defined by the C standard — more on why that matters in "Common Mistakes" below.
The standard is deliberately narrow about which types you may use for a bit field: int, signed int, unsigned int, and _Bool are the portable choices. Most real compilers (GCC, Clang, MSVC) also accept other integer types such as char or long as an extension, but relying on that is not portable ISO C. A bit field still behaves like an integer in expressions: reading one promotes it to int or unsigned int through the usual arithmetic conversions, the same way reading a char does.
One restriction follows directly from how bit fields are packed: since individual bits are not individually addressable in memory, you cannot take the address of a bit field. There is no such thing as a pointer to a bit field or an array of bit fields, and sizeof cannot be applied directly to a bit-field member — only to the enclosing struct.
Syntax
struct tag_name {
type member_name : width; /* width in bits, a constant expression */
type : width; /* unnamed: padding bits, not accessible */
type : 0; /* forces the next field into a new unit */
};
| Part | Meaning |
|---|---|
type |
The base integer type: int, unsigned int, signed int, or _Bool for guaranteed portability. |
member_name |
The field’s identifier; omit it to declare unused padding bits. |
width |
A constant expression giving the number of bits; must not exceed the number of bits in type. |
: 0 |
A special zero-width unnamed field that forces the next bit field to start a fresh allocation unit. |
Examples
Example 1: Packing status flags
#include <stdio.h>
struct Flags {
unsigned int isReady : 1;
unsigned int isError : 1;
unsigned int hasData : 1;
unsigned int mode : 2;
};
int main(void) {
struct Flags f = {0};
f.isReady = 1;
f.hasData = 1;
f.mode = 3;
printf("isReady=%u\n", f.isReady);
printf("isError=%u\n", f.isError);
printf("hasData=%u\n", f.hasData);
printf("mode=%u\n", f.mode);
printf("size of struct Flags = %zu bytes\n", sizeof(f));
return 0;
}
Output:
isReady=1
isError=0
hasData=1
mode=3
size of struct Flags = 4 bytes
Four small values — three 1-bit flags and one 2-bit mode — use only 5 bits total, so the compiler packs all of them into a single 4-byte unsigned int allocation unit instead of giving each its own storage. Without bit fields, four separate unsigned int members would cost 16 bytes; here the whole struct costs 4.
Example 2: Measuring the memory savings
#include <stdio.h>
struct NoBitFields {
unsigned int a;
unsigned int b;
unsigned int c;
unsigned int d;
};
struct WithBitFields {
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 1;
unsigned int d : 1;
};
int main(void) {
printf("sizeof(struct NoBitFields) = %zu\n", sizeof(struct NoBitFields));
printf("sizeof(struct WithBitFields) = %zu\n", sizeof(struct WithBitFields));
return 0;
}
Output:
sizeof(struct NoBitFields) = 16
sizeof(struct WithBitFields) = 4
This is the core payoff of bit fields: four unsigned int members normally cost 16 bytes (4 bytes each), but when each only needs 1 bit, the compiler packs all four into one 4-byte unit — a 4x reduction. The savings scale further as you add more narrow fields, up to the width of the allocation unit.
Example 3: A realistic packed date
#include <stdio.h>
struct Date {
unsigned int day : 5; /* 1-31 fits in 5 bits */
unsigned int month : 4; /* 1-12 fits in 4 bits */
unsigned int year : 12; /* offset from year 2000, fits in 12 bits */
};
int main(void) {
struct Date today = {18, 7, 2026 - 2000};
printf("Day: %u\n", today.day);
printf("Month: %u\n", today.month);
printf("Year: %u\n", today.year + 2000);
printf("sizeof(struct Date) = %zu bytes\n", sizeof(today));
return 0;
}
Output:
Day: 18
Month: 7
Year: 2026
sizeof(struct Date) = 4 bytes
A day (1-31), month (1-12), and year offset (0-4095, stored relative to 2000) fit into 5 + 4 + 12 = 21 bits, well within one 32-bit unit, so the whole date costs only 4 bytes instead of the 12 bytes three separate unsigned int members would need. Storing the year as an offset from 2000 keeps it inside 12 bits instead of needing a full 32-bit year value.
Under the Hood: Reading and Writing a Bit Field
Hardware cannot address individual bits directly — only whole bytes or words. So every bit-field access the compiler generates is really a small sequence of ordinary integer operations on the enclosing allocation unit:
- Reading a field shifts the whole storage unit right so the field’s bits sit at position 0, then masks off everything above the field’s width:
value = (unit >> shift) & mask; - Writing a field is a read-modify-write: clear the field’s bits in the unit with an inverted mask, then OR in the new value shifted into place:
unit = (unit & ~(mask << shift)) | ((new_value & mask) << shift);
In Example 1, setting f.mode = 3 compiles to something equivalent to clearing bits 3-4 of the underlying unsigned int and OR-ing in 3 << 3. You never see this shifting and masking in your source code — the compiler generates it for you — but understanding that it happens explains both the performance cost (a few extra instructions per access, versus none for a plain int) and the truncation behavior described next.
Common Mistakes
Mistake 1: Assigning a value that doesn’t fit
A bit field silently keeps only its lowest width bits; there is no overflow error.
#include <stdio.h>
struct Mode {
unsigned int value : 2; /* only 2 bits: can hold 0-3 */
};
int main(void) {
struct Mode m;
m.value = 5; /* 5 = 101 in binary, but only the low 2 bits fit */
printf("value = %u\n", m.value);
return 0;
}
Output:
value = 1
5 in binary is 101; a 2-bit field can only keep the lowest two bits, 01, so 5 silently becomes 1. The fix is to make the field wide enough for every value you intend to store in it:
#include <stdio.h>
struct Mode {
unsigned int value : 3; /* widened: can hold 0-7 */
};
int main(void) {
struct Mode m;
m.value = 5;
printf("value = %u\n", m.value);
return 0;
}
Output:
value = 5
With 3 bits the field can represent 0-7, so 5 round-trips correctly. When the valid range isn’t known at compile time, validate the value before assigning it instead of trusting the field to catch the mistake.
Mistake 2: Taking the address of a bit field
Because bit fields don’t occupy a whole addressable byte, the & operator is not allowed on them — this is a compile-time constraint violation, not a runtime bug:
#include <stdio.h>
struct Flags {
unsigned int isReady : 1;
unsigned int mode : 2;
};
int main(void) {
struct Flags f = {0};
unsigned int *p = &f.isReady; /* ERROR: address of bit-field requested */
*p = 1;
printf("%u\n", f.isReady);
return 0;
}
Output: does not compile — error: cannot take address of bit-field 'isReady'.
Any code that needs a pointer to the value — for example, passing it to a function expecting unsigned int * — must instead copy the value out into a plain local variable, operate on that, and copy it back:
#include <stdio.h>
struct Flags {
unsigned int isReady : 1;
unsigned int mode : 2;
};
void setFromPointer(unsigned int *out) {
*out = 1;
}
int main(void) {
struct Flags f = {0};
unsigned int tmp = f.isReady;
setFromPointer(&tmp);
f.isReady = tmp;
printf("isReady = %u\n", f.isReady);
return 0;
}
Output:
isReady = 1
A related, subtler mistake is assuming bit-field layout is portable: the bit order within a unit, whether a field can straddle two units, and even whether a plain int bit field is treated as signed are all implementation-defined. Never memcpy a bit-field struct to a file or over a network and expect another compiler or platform to read it back the same way — use explicit shift-and-mask code for any format that must be portable.
Best Practices
- Use
unsigned intfor bit-field types unless you have a specific reason to want signed behavior — a plainintbit field’s signedness is implementation-defined. - Never rely on bit fields for on-disk file formats or network protocols; the bit order and padding are compiler- and platform-specific. Use explicit shifts and masks on a fixed-width integer instead.
- Keep the total width of related fields within one allocation unit (usually 32 bits) if you’re relying on them being packed together for size savings.
- Validate values before assigning them to a narrow field, since out-of-range assignments are silently truncated rather than reported.
- Don’t take the address of a bit field or pass one to a function expecting a pointer; copy it to a local variable first.
- Reserve bit fields for genuinely bit-oriented data (flags, small enumerations, hardware register images); for ordinary booleans or small counters in non-memory-constrained code, plain members are simpler and just as fast to access.
- Document each field’s valid range in a comment, since the width alone (e.g.
: 5) doesn’t tell a reader whether 0-31 or some smaller range is actually meaningful.
Practice Exercises
- Define a
struct RGBwith 8-bit bit fieldsred,green, andblue, each holding 0-255. Printsizeofof the struct and compare it to the same three fields declared as plainunsigned charmembers. - Write a
struct Permissionswith three 1-bit fieldscanRead,canWrite, andcanExecute. Write a functionvoid printPermissions(struct Permissions p)that prints"rwx"-style output (e.g."r-x"when write is off). - Given a hypothetical hardware status register where bits 0-1 are an error code (0-3), bit 2 is a "busy" flag, and bits 3-7 are reserved, define a bit-field struct for it, set the error code to 2 and busy to 1, then print both values back out.
Summary
- A bit field is a struct member declared with
type name : width;, storing a value in only width bits instead of a whole byte or word. - Consecutive bit fields are packed into shared allocation units (usually the size of
unsigned int) by the compiler, which is what produces the memory savings. - Only
int,signed int,unsigned int, and_Boolare portable bit-field types per the C standard; other types are compiler extensions. - You cannot take the address of a bit field, form a pointer to one, or apply
sizeofto one directly. - Assigning a value that doesn’t fit truncates silently — there is no overflow warning at runtime.
- Bit order, unit-splitting, and plain-
intsignedness are all implementation-defined, so bit fields are unsuitable for portable binary file or network formats. - Use bit fields for flags, small enumerations, and hardware register modeling; use explicit shift-and-mask code when portability across compilers matters.
