6.9 bit-fielfs

2026-10-14

#include <stdio.h>
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//run with:     ./cc-c

//K&R2 section 6.9: bit-fields -- the same three symbol-table flags
//(keyword? extern? static?) packed into one word, done twice:
//
//  1. MASKS: an unsigned int plus named single-bit constants, set,
//     cleared and tested with | & ~ (the section 2.9 operators)
//  2. BIT-FIELDS: a struct whose members are declared ": 1" wide, so the
//     compiler does the shifting and masking for you
//
//After each step both versions print their raw bits, so you can see they
//end up as the SAME pattern in memory. To see a bit-field's raw bits the
//struct is put in a union with a plain unsigned int (section 6.8):
//store through one member, read through the other. That is the "store as
//one type, extract as another" case -- implementation-dependent, but gcc
//documents it as working, and it is the honest way to peek.
//
//Then the rough edges of bit-fields:
//  3. a value too big for its field is silently cut to the low bits
//  4. a 1-bit SIGNED field holds only 0 and -1, never 1
//  5. unnamed fields are padding; width 0 jumps to the next word
//  6. a field has no address (shown in a comment -- it won't compile)

//IDIOM: one bit per flag -- octal powers of two, each with exactly one 1 bit
enum { KEYWORD = 01, EXTERNAL = 02, STATIC = 04 };

struct flagbits {                   //the book's struct, given a tag
    unsigned int is_keyword : 1;
    unsigned int is_extern  : 1;
    unsigned int is_static  : 1;
};

union peek {                        //two views of the same 4 bytes
    struct flagbits f;
    unsigned int raw;
};

struct small {
    unsigned int u : 3;             //0..7
    int s : 1;                      //plain int: signed on gcc, so 0 or -1
};

struct padded {
    unsigned int a : 3;
    unsigned int   : 2;             //unnamed: 2 bits of padding
    unsigned int b : 3;
};

struct split {
    unsigned int a : 3;
    unsigned int   : 0;             //width 0: start the next field in a new word
    unsigned int b : 3;
};

void printbits(unsigned int, int);

int main(void)
{
    unsigned int flags = 0;
    union peek p = { { 0, 0, 0 } };     //first member is the struct: all fields 0
    struct small sm;
    int n;

    //1. masks
    printf("--- masks ---\n");
    //IDIOM: x |= MASK -- turn the mask's bits ON, leave the rest alone
    flags |= EXTERNAL | STATIC;
    printf("flags |= EXTERNAL | STATIC     ");
    printbits(flags, 3);
    //IDIOM: x &= ~MASK -- turn the mask's bits OFF (AND with its complement)
    flags &= ~(EXTERNAL | STATIC);
    printf("flags &= ~(EXTERNAL | STATIC)  ");
    printbits(flags, 3);
    //IDIOM: (x & MASK) == 0 -- the inner parens are required: == binds
    //tighter than &, so without them it would be x & (MASK == 0)
    if ((flags & (EXTERNAL | STATIC)) == 0)
        printf("both off\n");
    flags |= KEYWORD;
    printf("flags |= KEYWORD               ");
    printbits(flags, 3);

    //2. bit-fields: the same three steps
    printf("\n--- bit-fields (sizeof(struct flagbits) = %zu) ---\n",
            sizeof(struct flagbits));
    //IDIOM: a = b = 1 -- chained assignment, right to left
    p.f.is_extern = p.f.is_static = 1;
    printf("is_extern = is_static = 1      ");
    printbits(p.raw, 3);
    p.f.is_extern = p.f.is_static = 0;
    printf("is_extern = is_static = 0      ");
    printbits(p.raw, 3);
    if (p.f.is_extern == 0 && p.f.is_static == 0)
        printf("both off\n");
    p.f.is_keyword = 1;
    printf("is_keyword = 1                 ");
    printbits(p.raw, 3);
    printf("(gcc on x86 puts the FIRST field in bit 0, so this matches the masks;\n"
           " another compiler may start from the top bit instead)\n");

    //3. too big for the field: only the low bits are kept
    printf("\n--- truncation ---\n");
    n = 9;                  //a variable, so gcc can't see the overflow and won't warn
    sm.u = n;
    printf("store 9 = 1001 in 3 bits -> %u = ", sm.u);
    printbits(sm.u, 3);
    printf("the top 1 fell off, no warning\n");

    //4. a 1-bit signed field
    printf("\n--- 1-bit signed field ---\n");
    n = 1;
    sm.s = n;
    printf("int s : 1;  s = 1  ->  s reads back %d\n", sm.s);
    //gcc -Wextra knows this too: writing  if (sm.s == 1)  here gets
    //"warning: comparison is always false due to limited range of data type"
    printf("its one bit is the sign bit, so it holds 0 or -1 -- use unsigned\n");

    //5. unnamed and zero-width fields
    printf("\n--- padding ---\n");
    printf("a:3, unnamed :2, b:3   sizeof = %zu  (8 bits used, one word)\n",
            sizeof(struct padded));
    printf("a:3, unnamed :0, b:3   sizeof = %zu  (:0 pushed b into a 2nd word)\n",
            sizeof(struct split));

    //6. no address: uncomment the next line and gcc says
    //   error: cannot take address of bit-field 'is_static'
    //unsigned int *ip = &p.f.is_static;
    return 0;
}

//printbits:  print the low n bits of x, highest first, then a newline
void printbits(unsigned int x, int n)
{
    //IDIOM: (x >> i) & 1 -- shift bit i down to position 0, then mask off the rest
    while (--n >= 0)
        putchar((x >> n) & 1 ? '1' : '0');
    putchar('\n');
}

win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c
--- masks ---
flags |= EXTERNAL | STATIC     110
flags &= ~(EXTERNAL | STATIC)  000
both off
flags |= KEYWORD               001

--- bit-fields (sizeof(struct flagbits) = 4) ---
is_extern = is_static = 1      110
is_extern = is_static = 0      000
both off
is_keyword = 1                 001
(gcc on x86 puts the FIRST field in bit 0, so this matches the masks;
 another compiler may start from the top bit instead)

--- truncation ---
store 9 = 1001 in 3 bits -> 1 = 001
the top 1 fell off, no warning

--- 1-bit signed field ---
int s : 1;  s = 1  ->  s reads back -1
its one bit is the sign bit, so it holds 0 or -1 -- use unsigned

--- padding ---
a:3, unnamed :2, b:3   sizeof = 4  (8 bits used, one word)
a:3, unnamed :0, b:3   sizeof = 8  (:0 pushed b into a 2nd word)

This walkthrough goes through cc-c.c:1-139 in order. I've put the bit layouts in tables, and where an idea was already explained in the earlier 6.9 overview I only point back to it.

The enum (line 27)

enum { KEYWORD = 01, EXTERNAL = 02, STATIC = 04 };
Last block's enum counted 0, 1, 2 automatically. Here each value is given explicitly, because the values have to be powers of two:

name    octal   binary  bit set
KEYWORD 01  001 bit 0
EXTERNAL    02  010 bit 1
STATIC  04  100 bit 2
The leading 0 makes them octal, a K&R habit for bit constants. Each octal digit is exactly 3 bits, so 0755-style masks are easy to read. One catch: 08 is a compile error, because 8 isn't an octal digit. The next flag after 04 would be 010, which is 8.

The structs and union (lines 29–55)

struct flagbits {
    unsigned int is_keyword : 1;
    unsigned int is_extern  : 1;
    unsigned int is_static  : 1;
};
Here's how gcc lays it out in its 4-byte storage unit:

bit 31 … 3  2   1   0
field   unused (29 bits)    is_static   is_extern   is_keyword
Each flag sits at the same position as its enum mask, which is why the two output columns match.


union peek {
    struct flagbits f;
    unsigned int raw;
};
Both members are 4 bytes, so raw covers all of f, every bit, including the 29 unused ones.


struct small {
    unsigned int u : 3;
    int s : 1;
};
These are two unrelated test fields sharing one word: u in bits 0–2 and s in bit 3.


struct padded { unsigned int a : 3;  unsigned int : 2;  unsigned int b : 3; };
struct split  { unsigned int a : 3;  unsigned int : 0;  unsigned int b : 3; };
struct  bits 0–2    bits 3–4    bits 5–7    size
padded  a   (padding)   b   4
split   a   —   —   word 1
b   —   —   word 2 → 8
An unnamed field is just a type followed by : and a width. The : 0 ends the current word, so b starts in a new one.

main: declarations (lines 61–64)

unsigned int flags = 0;
union peek p = { { 0, 0, 0 } };
struct small sm;
int n;
flags starts at 0, so the bits set later are the only ones on.
{ { 0, 0, 0 } } uses nested braces. The outer pair is for the union, which per the 6.8 rule initializes its first member, f. The inner pair is for that struct, one 0 per field.
One subtle point: the initializer covers the three named fields. The standard doesn't promise that the 29 unused bits of a local variable are zeroed (gcc does zero them in practice). That's one reason the code prints only the low 3 bits of p.raw, the ones it's certain about.
sm and n have no initial values. They're assigned before they're read.
Step 1: masks (lines 67–82)

flags |= EXTERNAL | STATIC;
EXTERNAL | STATIC uses only constants, so the compiler computes it at compile time as 06. The line then compiles to flags = flags | 6.


flags &= ~(EXTERNAL | STATIC);
Step by step:

expression  value (low 8 bits shown)
EXTERNAL | STATIC   0000 0110
~(...)  1111 1001 (all 32 bits flipped: 0xFFFFFFF9)
flags before    0000 0110
flags & ~(...)  0000 0000

if ((flags & (EXTERNAL | STATIC)) == 0)
flags & 6 keeps only bits 1 and 2. If the result is 0, both are off, no matter what the other bits contain.


flags |= KEYWORD;
This turns on bit 0 → 001.

Step 2: the same steps with bit-fields (lines 85–100)

p.f.is_extern = p.f.is_static = 1;
p.f.is_extern reads left to right: union p, its member f (the struct), then that struct's field. The chained assignment works on fields too, with one detail: the value of an assignment is the value actually stored. So with a 1-bit field, a = p.f.is_static = 9 would give a the value 1, not 9.


printbits(p.raw, 3);
This reads the same 4 bytes through the other member of the union (the trick from 6.8), which shows the bits that the field assignments changed.


printf("(gcc on x86 puts the FIRST field ...;\n"
       " another compiler may ...)\n");
This shows a new idiom: adjacent string literals are joined at compile time. "abc" "def" is the same as "abcdef", so a long printf string can be split across lines with no + or extra function call.

Step 3: truncation (lines 103–108)

n = 9;
sm.u = n;
For an unsigned field, the rule is defined by the standard: the value is reduced modulo 2^width, here 2³ = 8.

binary  decimal
n   1001    9
kept (low 3 bits)   001 9 mod 8 = 1
Step 4: the signed field (lines 111–117)

n = 1;
sm.s = n;
printf("... s reads back %d\n", sm.s);
For a signed field, storing a value that doesn't fit is implementation-defined, not specified by the standard. gcc stores the bit pattern 1, and in a 1-bit signed field that one bit is the sign bit, so it reads back as −1. This is the difference from step 3: the unsigned result is guaranteed, the signed result depends on the compiler.

%d matches sm.s because of the integer promotions. When a bit-field narrower than int is used in an expression, it's promoted to int. Even sm.u, an unsigned 3-bit field, promotes to plain int, because every value from 0 to 7 fits. That's the same mechanism that turns a char into an int when you pass it to printf.

Steps 5 and 6 (lines 120–128)
sizeof(struct padded) and sizeof(struct split) give the 4 and 8 from the layout table. The commented-out line 128 is the & test. Remove the //, rebuild, and gcc stops with cannot take address of bit-field. Put the // back afterwards so the block compiles.

printbits (lines 133–139)

while (--n >= 0)
    putchar((x >> n) & 1 ? '1' : '0');
putchar('\n');
With n = 3, the pre-decrement gives the loop the values 2, 1, 0, so it goes from the highest bit to the lowest. For x = 6:

n   x >> n  & 1 prints
2   001 1   1
1   011 1   1
0   110 0   0
The output is 110. Precedence makes this work without extra parentheses: & binds tighter than ?:, so it's read as ((x >> n) & 1) ? '1' : '0'. putchar writes one character, which is simpler than printf("%c", ...) for a single char.