Skip to content
VLSI Mentor

C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types

Bit-Fields and Their Limitations

C lets a structure member be declared with a width in bits instead of a type's natural size, so several small fields share one integer — a flag, a three-bit counter, a two-bit mode, packed together rather than each taking a whole byte or more. It looks exactly like the tool for describing something whose layout someone else specified, and that appearance is why this chapter is the last in the module rather than a footnote in the first. The standard fixes the names and the widths and then leaves six separate questions about placement open, and the six are precisely the ones a register description depends on. This chapter sets out all six, measures each against a real compiler, shows what bit-fields genuinely are good for, and explains the alternative that Part II of this curriculum uses instead.

Advanced21 min readCPart IFundamentalsTypesMemory

Part I · Module 10 · Chapter 10.10 · Bit-Fields and Their Limitations

1. The Declaration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct flags {
        unsigned int ready    : 1;
        unsigned int mode     : 2;
        unsigned int count    : 5;
        unsigned int reserved : 24;
    };

A bit-field is a structure member declared with a colon and a width in bits. The four fields above total 32 bits, and the structure is four bytes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(struct flags) = 4  _Alignof 4

Using them is ordinary member access — f.ready = 1;, if (f.mode == 2), f.count++. They read and write like small integers, and almost everything from Chapter 10.2 applies: designated initialisers, nesting, . and ->.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct flags f = { .ready = 1, .mode = 2, .count = 9, .reserved = 0 };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ready=1 mode=2 count=9

Three restrictions apply immediately.

The width may not exceed the type's width.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: width of bit-field 'a' (33 bits) exceeds the width of its type (32 bits)

You cannot take a bit-field's address. There is no address — the field may not start on a byte boundary.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: address of bit-field requested

So a bit-field cannot be passed to a function that takes a pointer, cannot be the target of Chapter 7.5's out-parameter, and cannot have sizeof applied to it.

Values wrap silently at the field's width.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    f.count = 31;
    f.count = f.count + 1;        /* 32 does not fit in 5 bits */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after count = 31 then +1 : count = 0

No warning at run time and none at compile time, because the arithmetic is done in int and the result is truncated on the way back in. A 5-bit field is a modulo-32 counter whether or not you meant it to be.

2. What the Standard Fixes

Short list. Four things.

  • The names and widths are what you wrote.
  • Consecutive bit-fields are packed into the same addressable unit if there is room. They do not each get a unit of their own.
  • A zero-width unnamed field forces the next field into a new unit.
  • An unnamed field of non-zero width reserves bits that no name can reach, useful as explicit padding.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(struct zerogap { unsigned a:3; unsigned :0; unsigned b:3; }) = 8
sizeof(struct unnamed { unsigned a:3; unsigned :5; unsigned b:3; }) = 4

The first is eight bytes because :0 sent b to a fresh unit; the second is four because the unnamed five bits are just reserved space inside the same one.

That is the whole of what you can rely on. Now the other list.

3. What the Standard Leaves Open

Six separate questions, from C17 §6.7.2.1 and §6.7.2. Each is measured below against one compiler, and in each case a different compiler may legitimately answer differently.

#QuestionStatus
1Within a unit, are fields allocated high-order to low-order or low-order to high-order?implementation-defined
2If a field does not fit in the space left, does it straddle units or move to the next?implementation-defined
3What is the alignment of the addressable storage unit?unspecified
4Which unit size does the implementation choose?the implementation's choice
5Is a plain int bit-field signed or unsigned?implementation-defined
6Are types other than _Bool, signed int, unsigned int permitted?implementation-defined

Six open questions against four fixed facts. Note item 3 especially: unspecified is weaker than implementation-defined — the implementation is not even required to document it.

4. Watching Item 1 Happen

Take the struct flags from §1 with ready = 1, mode = 2, count = 9, and read the object's bytes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
object bytes: 4d 00 00 00

0x4d is 0100 1101. Reading from the low-order end: bit 0 is 1 — that is ready. Bits 1 and 2 are 10 in binary, which is 2 — that is mode. Bits 3 to 7 are 01001, which is 9 — that is count.

This implementation allocated low-order bits first. That is one of the two permitted answers to question 1.

The first eight bits of one bit-field structure, drawn twice. The top row is the layout this implementation produced, allocating low-order bits first: ready occupies one bit at the low end, mode occupies the next two, and count occupies the next five. The bottom row is an equally conforming layout that allocates high-order bits first: count occupies the top five bits, then mode two, then ready one at the low end. The same declaration produced the first; the standard permits the second. Only the field names and widths are fixed by the declaration, not their positions.ready1 bitmode2 bitscount5 bitscount5 bitsmode2 bitsready1 bit12
Figure 1 — the first eight bits of the same declaration, drawn twice. The upper row is what this compiler produced, allocating from the low-order end. The lower row is the mirror image, allocating from the high-order end, and it is equally conforming — a compiler that produces it is not broken and is not doing anything unusual. The declaration fixes the names and the widths; it does not fix which bits they occupy. If the bits have to land in particular places because something outside your program says so, this picture is the reason a bit-field declaration cannot express that requirement.

Item 2 is observable too:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(struct straddle { unsigned a:30; unsigned b:5; }) = 8

Thirty-five bits. Two bits of b would fit in the first unit and three would not, and this implementation moved the whole field to the next unit rather than splitting it — eight bytes for thirty-five bits. Another compiler may straddle and produce five. Both conform.

5. Item 5: The One-Bit Field That Cannot Hold 1

Of the six, this is the one most likely to bite you today rather than on some future port.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct plain_vs_unsigned {
        int          s : 1;        /* plain int */
        unsigned int u : 1;
    };

    p.s = 1;
    p.u = 1;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
plain `int x : 1` set to 1 reads back as -1
`unsigned int x : 1` set to 1 reads back as 1

Assign 1, read back -1.

The reason is item 5: for a bit-field, plain int is not a synonym for signed int — whether it is signed is implementation-defined, and here it is signed. A signed 1-bit field has one bit, which is its sign bit, so its only representable values are 0 and -1. The value 1 does not fit.

The compiler says exactly this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: implicit truncation from 'int' to a one-bit wide bit-field
         changes value from 1 to -1
         [-Wsingle-bit-bitfield-constant-conversion]

Never declare a bit-field as plain int. Write unsigned int for a field holding a count or a flag, signed int if you genuinely want a signed field, or _Bool for a single flag — which holds 0 and 1 by definition and sidesteps the question entirely.

Item 6 is adjacent and quieter. char c : 3; is outside the three types the standard requires, so it is implementation-defined whether it is permitted at all — and this compiler accepted it with no diagnostic even at -Wpedantic. Portable code should stay with unsigned int, signed int and _Bool.

6. Why Part II Never Uses Bit-Fields for a Register

Now the question c.ts asks this chapter to answer.

A hardware register is a specification written by somebody else. It says: bit 0 is the enable, bits 1 to 2 are the mode, bits 3 to 7 are the count. Your program does not get to choose; it must match, exactly, or it controls the wrong hardware.

Line that requirement up against §3's table:

What the register specification fixesWhat a bit-field declaration guarantees
which bit index each field occupiesnothing — item 1
that a field spanning a boundary stays contiguousnothing — item 2
the width of the containing unitnothing — item 4
the alignment of that unitnothing — item 3, and unspecified at that
the signedness of each fieldnothing for plain int — item 5

Every single property the specification is about is one the standard leaves open. A bit-field declaration expresses the part that was never in doubt — the names and the widths — and is silent on the part you actually needed.

It is worth being clear about what this argument is and is not.

It is not "bit-fields are broken". They do what they are specified to do. The specification simply does not include "these bits, in this order, in this unit".

It is not "your compiler gets it wrong". Your compiler very likely lays them out exactly as you hoped. The problem is that the source code does not say so, so the arrangement survives only as long as nobody changes compiler, target, or optimisation settings — and when it breaks, it breaks silently, by controlling the wrong bits.

And it is not a portability nicety. A layout that is right today and wrong after a toolchain upgrade is not a portability concern; it is a correctness concern with a delay on it.

Use bit-fields for state that is private to your program. Do not use them to describe a layout somebody else defined. That is the rule this curriculum follows, and it is why Part II describes every register with an integer plus explicit shifts and masks — a form in which the bit positions are written down in the source, checked by nothing but visible to everyone, and identical on every target. Chapters 15.2 and 15.4 are where that technique is taught.

7. What Bit-Fields Are Genuinely Good For

The rule above leaves a real and useful space.

Dense internal state. A program holding millions of records, each needing a handful of small values, can save a great deal. Four unsigned int members cost 16 bytes; four bit-fields of 1, 2, 5 and 24 bits cost 4. Across ten million records that is 120 megabytes.

Readable flag sets that never leave the program. f.ready, f.mode, f.count read better than (w >> 3) & 0x1f, and when nothing outside the program sees the bits, readability is the only thing at stake.

Fields that are compiled exactly once. State inside one program, built by one toolchain, never written to a file, never sent anywhere, never read by anything else. The six open questions are all about agreement — with another implementation, another build, another device. With no second party, there is nothing to disagree with.

And the honest counterweight, measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof(struct spread { unsigned a:1; unsigned b:1; }) = 4

Two bits of data in four bytes. Bit-fields only save space when enough of them share a unit; a structure with two flags in it saves nothing at all over two _Bool members and gives up addressability for the privilege. Measure sizeof before assuming a saving — Chapter 10.5 §8's habit, applied here.

There is also a cost that does not show in sizeof: reading or writing a bit-field is a shift and a mask that the compiler generates for you, so a field in a hot loop is not free the way a whole int member is. If both density and speed matter, measure both.

8. The Alternative, Named

When bit-fields are the wrong tool, the right one is an ordinary unsigned integer with named shifts and masks — the positions written down explicitly instead of left to the implementation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #define MODE_SHIFT  1u
    #define MODE_MASK   0x3u

    unsigned int mode = (word >> MODE_SHIFT) & MODE_MASK;

Three properties follow, and they are exactly the three §6 said were missing.

The bit positions are in the source, so the code states the layout rather than hoping for it. The result is identical on every implementation, because shifting and masking an unsigned int is fully defined by Module 3. And it is reviewable — a reader can check the constants against the specification line by line, which is impossible with a bit-field declaration because the specification is not in the declaration.

The operators are Module 3's and you have had them since Chapter 3.8. Using them deliberately to describe a hardware field — with the masks, the shifts, the read-modify-write discipline and the mistakes that go with it — is Chapter 15.2's and Chapter 15.4's subject, and it is where Part II picks this up.

9. What You Should Be Able to Say Now

  1. A bit-field is a structure member with a width in bits; consecutive fields share an addressable unit when they fit.
  2. Fixed by the standard: names, widths, packing into one unit when there is room, and that :0 starts a new unit.
  3. Left open — six things: allocation order within a unit, whether a field straddles, the unit's alignment (unspecified), the unit size, a plain int field's signedness, and whether other types are allowed.
  4. Measured: ready/mode/count produced bytes 4d 00 00 00 — this implementation allocated low-order first, and the mirror image is equally conforming.
  5. Measured: a 30-bit and a 5-bit field took 8 bytes — this implementation moved rather than straddled.
  6. Measured: int x : 1 set to 1 reads back as -1, with a warning saying so. Never use plain int — use unsigned int, signed int, or _Bool.
  7. You cannot take a bit-field's address — "address of bit-field requested".
  8. Values wrap silently at the field width — 31 + 1 gave 0.
  9. Every property a register specification fixes is one the standard leaves open, which is why this curriculum never describes a register with bit-fields.
  10. They are right for dense state private to one program, compiled once.
  11. Measured: two 1-bit fields still cost 4 bytes — check sizeof before assuming a saving.
  12. The alternative is an unsigned integer with named shifts and masks, which puts the positions in the source — Chapters 15.2 and 15.4.

10. Exercises

Exercise 1 — predict the sizes. Give sizeof for each on this target, then compile and check. Say which of your answers rely on something the standard leaves open.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct a { unsigned x : 1; unsigned y : 1; };
struct b { unsigned x : 16; unsigned y : 16; };
struct c { unsigned x : 16; unsigned y : 20; };
struct d { unsigned x : 4; unsigned : 0; unsigned y : 4; };
struct e { unsigned x : 4; unsigned : 4; unsigned y : 4; };

Exercise 2 — fix the declarations. Each has a defect from §5 or §1. Name it and correct it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct s1 { int enabled : 1; };
struct s2 { unsigned int level : 33; };
struct s3 { char mode : 2; };
struct s4 { unsigned int count : 3; };   /* must hold values up to 10 */

Exercise 3 — explain the minus one. In three sentences, explain why int x : 1; assigned 1 reads back as -1, and give two different one-word changes that fix it.

Exercise 4 — classify six claims. ISO C guarantee, implementation-defined, unspecified, or false?

  • consecutive bit-fields that fit share one unit
  • the first bit-field occupies the lowest-order bits
  • a zero-width unnamed field starts a new unit
  • you can take the address of a bit-field
  • a field too large for the remaining space moves to the next unit
  • a bit-field's width may exceed its type's width

Exercise 5 — the register argument. A colleague proposes describing a hardware status register with a bit-field struct, noting that it produces the right layout on your compiler today. Give the two-sentence reply, and say what would have to be true about the project for their approach to be defensible.

Exercise 6 — convert it. Rewrite this as an unsigned int with named shift and mask constants, and write the two accessor functions.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct status { unsigned ready : 1; unsigned mode : 2; unsigned count : 5; };

Exercise 7 — measure the saving. A record needs a flag, a 3-bit mode and a 10-bit count, and there will be 5,000,000 of them. Give sizeof for the bit-field version and for a version using _Bool and two unsigned shorts, and the total memory for each. Say which you would ship and what else you would check first.

Exercise 8 — when they are fine. Give two realistic uses of bit-fields that §6's argument does not touch at all, and state the property both of them share.

11. Summary

  • A bit-field gives a member a width in bits; consecutive fields share a unit when they fit, and :0 starts a new one.
  • Fixed: names, widths, packing, the zero-width rule. Open: six things — order, straddling, unit alignment (unspecified), unit size, plain-int signedness, and other types.
  • Measured: bytes 4d 00 00 00 — low-order first here, and the mirror layout is equally legal.
  • Measured: 30 + 5 bits took 8 bytes — moved, not straddled.
  • Measured: int x : 1 set to 1 read back as -1. Use unsigned int, signed int or _Bool, never plain int.
  • No address; silent wrap at the field width — 31 + 1 gave 0.
  • Every property a register specification fixes is left open, so never describe a register with bit-fields.
  • Good for dense state private to one program; measured, two 1-bit fields still cost 4 bytes.
  • The alternative is an unsigned integer with named shifts and masks — Chapters 15.2 and 15.4.

12. Closing Module 10

This module gave C a way to describe the problem rather than only the machine. Before it, every type you had was one the language chose: an integer of some width, a character, an address, an array of identical things. Now you can declare that a reading is an id and a value and a grade, that a message is exactly one of three alternatives, that a level is one of four named states — and the compiler carries those statements with you into every function signature and every array.

The module's thread is that a type is a statement that the compiler can check, and the value of each construct is exactly the checking it enables. A structure makes Chapter 10.1's desynchronised sort unwritable. An enum makes an unhandled case a build warning — until a default turns it off. A union makes alternatives exclusive and then refuses to tell you which one is live, which is why the tag discipline exists. A bit-field states widths and says nothing about positions. In each case the useful question was the same one: what, exactly, does this guarantee? — and the answer was often smaller than the syntax suggested.

Three habits are worth carrying forward. Initialise by named member, so a reordered declaration cannot silently permute your data. Declare members in decreasing order of alignment when a structure is large or numerous, which cost nothing and saved a third. And make the invariant unwritable rather than documented — one constructor per alternative, one owner per allocation, one place that can set a tag — which is the same lesson Chapter 9.8 reached from the other direction.

One limitation has been quietly present throughout. Every type in this module was declared in the same file that used it, and that is not how programs are built. A struct reading is useless if only one .c file can see it, and the moment a second file needs it, questions appear that have no answer yet: where does the declaration go so that both files agree, what happens if it is included twice, and what is the difference between telling the compiler a thing exists and creating it? That last distinction turns out to be the one the whole next module rests on — and it is also the one that finally separates three words this curriculum has deliberately kept apart: scope, lifetime, and a third property you have not met, which decides whether a name in one file and the same name in another refer to the same object or to two different ones.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.