#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.