K&R C Exercise 8-3: Write _flushbuf, fflush and fclose

Exercise 8-3. Design and write _flushbuf, fflush, and fclose.

Exercise 8-2 built the read half of a miniature stdio; this one builds the write half. The design mirrors getc/_fillbuf: putc is a macro that drops characters into a buffer, and only when the buffer fills does it call the real function — _flushbuf — which writes the whole buffer with one system call and stores the character that didn’t fit.

The subtle part is that _flushbuf serves three masters: the very first putc (no buffer exists yet — allocate one, write nothing), a full buffer (write it, then store the new character), and a flush request from fflush/fclose (write pending bytes, store nothing — we signal this by passing EOF as the character). fclose then completes the life cycle: flush, free the buffer, clear the flags so the slot can be reused, and close the descriptor.

Solution

/* K&R Exercise 8-3: design and write _flushbuf, fflush, and fclose.
 * Compile: gcc -ansi -Wall -Wextra ex8_3.c -o ex8_3 */
#define _POSIX_C_SOURCE 200112L
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>          /* printf/fopen for the demo check only */

#define PERMS       0666
#define MY_OPEN_MAX 20
#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 {
        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_putc(x, p) (--(p)->cnt >= 0 \
    ? (unsigned char)(*(p)->ptr++ = (char)(x)) : _flushbuf((x), p))

/* _flushbuf: write out the full (or final) buffer, then store c */
static int _flushbuf(int c, MYFILE *fp)
{
    int bufsize;
    ssize_t nc;

    if (!fp->flag.is_write || fp->flag.is_err)
        return MY_EOF;
    bufsize = fp->flag.is_unbuf ? 1 : MYBUFSIZ;
    if (fp->base == NULL) {             /* first write: no buffer yet */
        if ((fp->base = malloc((size_t)bufsize)) == NULL) {
            fp->flag.is_err = 1;
            return MY_EOF;
        }
    } else {                            /* buffer full (or being flushed) */
        nc = fp->ptr - fp->base;
        if (nc > 0 && write(fp->fd, fp->base, (size_t)nc) != nc) {
            fp->flag.is_err = 1;
            return MY_EOF;
        }
    }
    fp->ptr = fp->base;
    if (c == MY_EOF) {                  /* flush-only call: store nothing */
        fp->cnt = bufsize;
        return 0;
    }
    fp->cnt = bufsize - 1;              /* one slot used by c below */
    *fp->ptr++ = (char)c;
    return (unsigned char)c;
}

/* my_fflush: force pending buffered output to the file */
static int my_fflush(MYFILE *fp)
{
    if (fp == NULL || !fp->flag.is_write)
        return MY_EOF;
    if (fp->base == NULL)               /* nothing was ever written */
        return 0;
    return _flushbuf(MY_EOF, fp) == MY_EOF ? MY_EOF : 0;
}

/* my_fclose: flush, release the buffer, close the descriptor */
static int my_fclose(MYFILE *fp)
{
    int rc;

    if (fp == NULL)
        return MY_EOF;
    rc = 0;
    if (fp->flag.is_write && my_fflush(fp) == MY_EOF)
        rc = MY_EOF;
    free(fp->base);
    fp->base = fp->ptr = NULL;
    fp->cnt = 0;
    fp->flag.is_read = fp->flag.is_write = 0;
    fp->flag.is_unbuf = fp->flag.is_eof = fp->flag.is_err = 0;
    if (close(fp->fd) == -1)
        rc = MY_EOF;
    return rc;
}

/* my_fopen: as in Exercise 8-2 (write and append paths matter here) */
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;
    if (fp >= _iob + MY_OPEN_MAX)
        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)
        return NULL;

    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    fp->flag.is_unbuf = fp->flag.is_eof = fp->flag.is_err = 0;
    if (mode == 'r') {
        fp->flag.is_read = 1;
        fp->flag.is_write = 0;
    } else {
        fp->flag.is_read = 0;
        fp->flag.is_write = 1;
    }
    return fp;
}

static void my_fputs(const char *s, MYFILE *fp)
{
    while (*s)
        my_putc(*s++, fp);
}

int main(void)
{
    MYFILE *fp;
    FILE *check;
    int c, i;

    if ((fp = my_fopen("ex8_3_out.txt", 'w')) == NULL) {
        printf("my_fopen failed\n");
        return 1;
    }
    for (i = 1; i <= 3; i++)
        my_fputs("written through my_putc and _flushbuf\n", fp);
    my_fputs("partial line before fflush", fp);
    my_fflush(fp);                       /* pending bytes hit the disk now */
    my_fputs(" ...and after it\n", fp);
    if (my_fclose(fp) == MY_EOF) {       /* final flush happens here */
        printf("my_fclose failed\n");
        return 1;
    }

    /* prove the round-trip with ordinary stdio */
    if ((check = fopen("ex8_3_out.txt", "r")) == NULL)
        return 1;
    while ((c = getc(check)) != EOF)
        putchar(c);
    fclose(check);
    return 0;
}

Compile and Run

gcc -ansi -Wall -Wextra ex8_3.c -o ex8_3
./ex8_3

Sample Output

written through my_putc and _flushbuf
written through my_putc and _flushbuf
written through my_putc and _flushbuf
partial line before fflush ...and after it

Real captured run — the file written through our layer reads back byte-for-byte through ordinary stdio, including the bytes forced out mid-stream by my_fflush.

What This Exercise Teaches

  • Deferred buffer allocation — the buffer appears on the first putc, not at fopen
  • The sentinel pattern: passing EOF to _flushbuf to mean “flush only, store nothing”
  • Why fclose must flush before it frees — and why exiting without it loses buffered data
  • Resource life cycle: malloc/free of the buffer paired with open/close of the descriptor

Set Up Your C Environment

This chapter uses UNIX system calls — a Linux machine, macOS terminal, or WSL2 on Windows all work:

← Exercise 8-2  | 
All K&R Solutions  | 
Exercise 8-4 →

Book:

The C Programming Language, 2nd Ed — Kernighan & Ritchie

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>