Exercise 8-2. Rewrite
fopenand_fillbufwith fields instead of explicit bit operations. Compare code size and execution speed.
Chapter 8 sketches a miniature version of the standard I/O library: a FILE structure holding a buffer, a count, a position pointer, and a set of status flags. In the book the flags live in one int and are tested with bit masks like fp->flag & _READ. The exercise asks for the same machinery expressed with bit-fields — named one-bit members the compiler packs for you. The logic of fopen and _fillbuf is untouched; what changes is that every masked test becomes a readable member access: fp->flag.is_read instead of fp->flag & _READ, and clearing a flag no longer needs a carefully inverted mask.
The one real trap: with masks, “this slot is free” is written (fp->flag & (_READ | _WRITE)) == 0. With fields it must become !fp->flag.is_read && !fp->flag.is_write — there is no way to test two bit-fields in one operation, which is exactly the code-size trade-off the exercise wants you to notice.
Solution
/* K&R Exercise 8-2: fopen and _fillbuf with bit-fields instead of
* explicit bit operations.
* Compile: gcc -ansi -Wall -Wextra ex8_2.c -o ex8_2 */
#define _POSIX_C_SOURCE 200112L
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> /* printf for the demo only */
#define PERMS 0666
#define MY_OPEN_MAX 20 /* max files open at once */
#define MYBUFSIZ 1024
#define MY_EOF (-1)
typedef struct {
int cnt; /* characters left in buffer */
char *ptr; /* next character position */
char *base; /* location of buffer */
struct { /* the exercise: fields, not bit masks */
unsigned is_read : 1;
unsigned is_write : 1;
unsigned is_unbuf : 1;
unsigned is_eof : 1;
unsigned is_err : 1;
} flag;
int fd; /* file descriptor */
} MYFILE;
static MYFILE _iob[MY_OPEN_MAX];
#define my_getc(p) (--(p)->cnt >= 0 \
? (unsigned char) *(p)->ptr++ : _fillbuf(p))
/* _fillbuf: allocate and fill input buffer */
static int _fillbuf(MYFILE *fp)
{
int bufsize;
ssize_t n;
if (!fp->flag.is_read || fp->flag.is_eof || fp->flag.is_err)
return MY_EOF;
bufsize = fp->flag.is_unbuf ? 1 : MYBUFSIZ;
if (fp->base == NULL) /* no buffer yet */
if ((fp->base = malloc((size_t)bufsize)) == NULL)
return MY_EOF; /* can't get buffer */
fp->ptr = fp->base;
n = read(fp->fd, fp->ptr, (size_t)bufsize);
fp->cnt = (int)n;
if (--fp->cnt < 0) {
if (fp->cnt == -1)
fp->flag.is_eof = 1; /* read returned 0 */
else
fp->flag.is_err = 1; /* read returned < 0 */
fp->cnt = 0;
return MY_EOF;
}
return (unsigned char) *fp->ptr++;
}
/* my_fopen: open file, return file ptr */
static MYFILE *my_fopen(const char *name, char mode)
{
int fd;
MYFILE *fp;
if (mode != 'r' && mode != 'w' && mode != 'a')
return NULL;
for (fp = _iob; fp < _iob + MY_OPEN_MAX; fp++)
if (!fp->flag.is_read && !fp->flag.is_write)
break; /* found a free slot */
if (fp >= _iob + MY_OPEN_MAX) /* no free slots */
return NULL;
if (mode == 'w')
fd = creat(name, PERMS);
else if (mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1) /* couldn't access name */
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
fp->flag.is_unbuf = 0;
fp->flag.is_eof = 0;
fp->flag.is_err = 0;
if (mode == 'r')
fp->flag.is_read = 1;
else
fp->flag.is_write = 1;
return fp;
}
int main(void)
{
MYFILE *fp;
int c, count;
/* create a small test file with ordinary stdio */
FILE *out = fopen("ex8_2_test.txt", "w");
if (out == NULL)
return 1;
fputs("bit-fields replace bit masks\n", out);
fclose(out);
/* read it back through OUR layer */
if ((fp = my_fopen("ex8_2_test.txt", 'r')) == NULL) {
printf("my_fopen failed\n");
return 1;
}
count = 0;
while ((c = my_getc(fp)) != MY_EOF) {
putchar(c);
count++;
}
printf("read %d characters through my_getc/_fillbuf\n", count);
printf("sizeof(MYFILE) with bit-fields: %lu bytes\n",
(unsigned long) sizeof(MYFILE));
return 0;
}
Compile and Run
gcc -ansi -Wall -Wextra ex8_2.c -o ex8_2
./ex8_2
Sample Output
bit-fields replace bit masks read 29 characters through my_getc/_fillbuf sizeof(MYFILE) with bit-fields: 32 bytes
Real captured run of the exact code above (gcc on Linux x86-64).
Code Size and Speed — the Comparison the Exercise Asks For
- Struct size: identical. The five one-bit fields pack into a single padded word — 32 bytes for
MYFILEeither way on x86-64, because anint flagword occupies the same aligned slot. - Speed: no measurable difference. The compiler implements each bit-field access with the same AND/OR masking instructions the book’s version writes by hand. Bit-fields buy readability, not performance.
- Code size: fields can be slightly larger where masks combined tests — the free-slot check needs two field tests where
flag & (_READ | _WRITE)was one instruction’s worth of masking.
What This Exercise Teaches
- Bit-field struct members — declaring, setting, and testing 1-bit flags
- How the standard library’s
FILEstructure actually works: buffer, count, position, flags, descriptor - The
getcbuffering trick: a macro that only calls a function when the buffer empties - Why
read()returning 0 means EOF but negative means error — and keeping the two flags distinct
Set Up Your C Environment
This chapter uses UNIX system calls — a Linux machine, macOS terminal, or WSL2 on Windows all work:
- Install GCC on Ubuntu/Linux
- Install GCC on Windows 11 (use the WSL2 path for this chapter)
- Install GCC on macOS
← Exercise 8-1 |
All K&R Solutions |
Exercise 8-3 →
Book:
The C Programming Language, 2nd Ed — Kernighan & Ritchie