K&R2 8.5 Example -- An Implementation of Fopen and Getc

2026-10-20

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//run with:     ./cc-c < zhouyi.txt         # stdin is a file: section 8 reads it with my_getchar
//              echo 乾為天 | ./cc-c        # stdin is a pipe: my_getchar doesn't care which
//              ./cc-c                      # stdin is the terminal: section 8 is skipped

//K&R2 8.5 Example -- An Implementation of Fopen and Getc. The book's
//code, run, with every claim in the section shown by a printf:
//  1. a FILE is a struct: buffer, position, count, flags, fd; all open
//     files live in one table, _iob[OPEN_MAX]; stdin is &_iob[0]
//  2. fopen finds a free slot, gets an fd from open/creat, and sets
//     cnt 0 and base NULL -- no buffer yet
//  3. the first getc finds cnt empty and calls _fillbuf, which mallocs
//     the buffer and fills it with ONE read
//  4. after that, getc is a macro that never calls anything until the
//     buffer is empty again: one read per BUFSIZ characters
//  5. _fillbuf turns read's 0 into the EOF flag, and stops reading
//     once that flag is set
//  6. read's -1 becomes the ERR flag
//  7. the (unsigned char) cast keeps every byte 0..255, so EOF can only
//     mean end of file
//  8. stdin, stdout, stderr need no fopen: the table starts with fd 0,
//     1, 2 already in it
//  9. _UNBUF means a buffer of 1: one read per character
// 10. a macro evaluates its argument more than once (the book's
//     warning about getc and putc)
// 11. modes: "w" is creat, "a" is open + lseek to the end, anything
//     else is refused
// 12. the table is fixed at OPEN_MAX: the 21st fopen fails
// 13. the real glibc FILE, for comparison
//
//The book's names (FILE, _iob, getc, _fillbuf, _READ ...) belong to
//<stdio.h>, which is included here for printf -- so every name gets a
//my_ / M_ prefix. Otherwise it is the book's code line for line;
//anything added for the printfs is marked "not in the book".

#define MY_OPEN_MAX 20      //max #files open at once
#define MY_BUFSIZ   1024
#define PERMS       0666    //RW for owner, group, others
#define DEMO        "fopen-demo.txt"    //made in section 11, unlinked at the end

typedef struct my_iobuf {
    int   cnt;      //characters left
    char *ptr;      //next character position
    char *base;     //location of buffer
    int   flag;     //mode of file access
    int   fd;       //file descriptor
} MYFILE;

//IDIOM: flags as powers of two -- each one its own bit, so one int
//holds all of them: set with |=, test with &
enum my_flags {
    M_READ  = 01,   //file open for reading
    M_WRITE = 02,   //file open for writing
    M_UNBUF = 04,   //file is unbuffered
    M_EOF   = 010,  //EOF has occurred on this file
    M_ERR   = 020   //error occurred on this file
};

//IDIOM: partial array initializer -- the 3 listed slots get these
//values, the other 17 are all zeros: flag 0 means "free"
MYFILE my_iob[MY_OPEN_MAX] = {      //stdin, stdout, stderr
    { 0, (char *) 0, (char *) 0, M_READ, 0 },
    { 0, (char *) 0, (char *) 0, M_WRITE, 1 },
    { 0, (char *) 0, (char *) 0, M_WRITE | M_UNBUF, 2 }
};

#define my_stdin  (&my_iob[0])
#define my_stdout (&my_iob[1])
#define my_stderr (&my_iob[2])

int my_fillbuf(MYFILE *);

#define my_feof(p)   (((p)->flag & M_EOF) != 0)
#define my_ferror(p) (((p)->flag & M_ERR) != 0)
#define my_fileno(p) ((p)->fd)

//IDIOM: --cnt >= 0 ? fast : slow -- decrement first: cnt 1 becomes 0,
//still >= 0, so the last byte is taken; cnt 0 becomes -1: refill
#define my_getc(p) (--(p)->cnt >= 0 \
                   ? (unsigned char) *(p)->ptr++ : my_fillbuf(p))
#define my_getchar() my_getc(my_stdin)

//not in the book: the same macro without the cast, for section 7
#define nocast_getc(p) (--(p)->cnt >= 0 \
                       ? *(p)->ptr++ : my_fillbuf(p))

int nreads = 0;     //not in the book: how many times read() was called
int trace = 0;      //not in the book: 1 = my_fillbuf prints each read

//my_fopen:  open file, return file ptr
MYFILE *my_fopen(char *name, char *mode)
{
    int fd;
    MYFILE *fp;

    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    //IDIOM: pointer walk over an array -- fp < my_iob + MY_OPEN_MAX is
    //"not past the end", the pointer form of i < MY_OPEN_MAX
    for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
        //IDIOM: (flag & (A | B)) == 0 -- "neither bit is set"
        if ((fp->flag & (M_READ | M_WRITE)) == 0)
            break;      //found free slot
    if (fp >= my_iob + MY_OPEN_MAX)     //no free slots
        return NULL;

    if (*mode == 'w')
        fd = creat(name, PERMS);
    else if (*mode == 'a') {
        //IDIOM: assign-in-test with -1 -- try open; only if that
        //fails, creat
        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 = (*mode == 'r') ? M_READ : M_WRITE;
    return fp;
}

//my_fillbuf:  allocate and fill input buffer
int my_fillbuf(MYFILE *fp)
{
    int bufsize;

    //IDIOM: (flag & (A|B|C)) != A -- mask three bits, then demand
    //exactly one pattern: READ set, EOF and ERR both clear
    if ((fp->flag & (M_READ | M_EOF | M_ERR)) != M_READ)
        return EOF;
    bufsize = (fp->flag & M_UNBUF) ? 1 : MY_BUFSIZ;
    if (fp->base == NULL)       //no buffer yet
        if ((fp->base = (char *) malloc(bufsize)) == NULL)
            return EOF;         //can't get buffer
    fp->ptr = fp->base;
    nreads++;                   //not in the book
    fp->cnt = read(fp->fd, fp->ptr, bufsize);
    if (trace)                  //not in the book
        printf("   [my_fillbuf: read() returned %d]\n", fp->cnt);
    //IDIOM: one decrement sorts three cases -- n bytes: n-1 left after
    //this one; 0 (end of file) becomes -1; -1 (error) becomes -2
    if (--fp->cnt < 0) {
        if (fp->cnt == -1)
            fp->flag |= M_EOF;
        else
            fp->flag |= M_ERR;
        fp->cnt = 0;
        return EOF;
    }
    return (unsigned char) *fp->ptr++;
}

//not in the book: point a MYFILE at bytes already in memory, so a test
//can feed it exact bytes; M_EOF set, so when they run out my_fillbuf
//returns EOF without reading anything
void fake(MYFILE *fp, char *s, int n)
{
    fp->base = fp->ptr = s;
    fp->cnt = n;
    fp->flag = M_READ | M_EOF;
    fp->fd = -1;
}

int main(void)
{
    MYFILE *fp, *dp, *up, *ap;
    int c, i, n, r0;
    char line[200];
    struct stat st;

    //--- 1. the table --------------------------------------------------
    printf("1. sizeof(MYFILE) = %zu bytes; my_iob[] holds %d of them = %zu bytes\n",
           sizeof(MYFILE), MY_OPEN_MAX, sizeof(my_iob));
    for (i = 0; i < 4; i++)
        printf("   my_iob[%d]: fd %d, flag %02o%s\n", i, my_iob[i].fd, my_iob[i].flag,
               i == 0 ? "  READ          <- stdin" :
               i == 1 ? "  WRITE         <- stdout" :
               i == 2 ? "  WRITE|UNBUF   <- stderr" : "  (0 = free, like the 16 after it)");
    printf("   my_stdin == &my_iob[0]: %s -- a FILE * is a pointer into this table\n\n",
           my_stdin == &my_iob[0] ? "yes" : "no");

    //--- 2. fopen ------------------------------------------------------
    fp = my_fopen("zhouyi.txt", "r");
    if (fp == NULL) {
        fprintf(stderr, "can't open zhouyi.txt\n");
        return 1;
    }
    //IDIOM: fp - my_iob -- subtracting pointers into one array gives
    //the index; %td is the printf format for that difference
    printf("2. my_fopen(\"zhouyi.txt\", \"r\") -> &my_iob[%td]: the first free slot\n",
           fp - my_iob);
    printf("   fd %d (0, 1, 2 are taken), flag %02o, cnt %d, base %s -- nothing read yet\n\n",
           fp->fd, fp->flag, fp->cnt, fp->base == NULL ? "NULL" : "set");

    //--- 3. the first getc fills the buffer --------------------------------
    c = my_getc(fp);
    printf("3. first my_getc -> 0x%02x; read() calls so far: %d\n", c, nreads);
    printf("   now base is malloc'd, cnt %d, ptr - base = %td\n",
           fp->cnt, fp->ptr - fp->base);
    printf("   the kernel's position: %ld -- it moved %d, we used 1: two positions\n",
           (long) lseek(fp->fd, 0L, SEEK_CUR), MY_BUFSIZ);
    my_getc(fp);
    my_getc(fp);
    printf("   2 more my_getc: cnt %d, read() calls still %d; the 3 bytes: %.3s\n\n",
           fp->cnt, nreads, fp->base);

    //--- 4. the rest of the file: one read per 1024 ------------------------
    n = 3;
    //IDIOM: while ((c = getc(fp)) != EOF) -- c is an int, so it can
    //hold every byte 0..255 AND the -1 that isn't a byte
    while ((c = my_getc(fp)) != EOF) {
        n++;
        if (n == 263000)        //watch the last refills
            trace = 1;
    }
    trace = 0;
    printf("4. %d characters, %d read() calls (%d full + 1 short + 1 that got 0)\n",
           n, nreads, n / MY_BUFSIZ);
    printf("   a getc FUNCTION would be %d calls; the macro called my_fillbuf %d times\n\n",
           n, nreads);

    //--- 5. EOF ---------------------------------------------------------
    r0 = nreads;
    printf("5. my_feof(fp) %d, cnt %d\n", my_feof(fp), fp->cnt);
    c = my_getc(fp);
    printf("   my_getc again -> %d, read() calls %d: the flag test stopped it, no read\n",
           c, nreads - r0);
    c = my_getc(fp);
    printf("   and again -> %d, but now cnt is %d: the early return in my_fillbuf\n"
           "   doesn't reset cnt, so every call after EOF pushes it one lower\n\n",
           c, fp->cnt);

    //--- 6. ERR ---------------------------------------------------------
    //Linux lets you open() a directory for reading, but read() on it fails
    dp = my_fopen(".", "r");
    c = my_getc(dp);
    printf("6. my_fopen(\".\", \"r\") works, my_getc -> %d; my_ferror %d, my_feof %d\n"
           "   read() returned -1 (a directory), so ERR, not EOF\n\n",
           c, my_ferror(dp), my_feof(dp));

    //--- 7. the (unsigned char) cast --------------------------------------
    MYFILE f1, f2;
    char qian[] = "\xe4\xb9\xbe";       //乾 in UTF-8
    char ff[] = "\xff";
    fake(&f1, qian, 3);
    fake(&f2, qian, 3);
    //one call per statement: the order printf's arguments are evaluated
    //in is not fixed by C, so three my_getc in one printf could run in
    //any order
    int u[3], s[3];
    for (i = 0; i < 3; i++) {
        u[i] = my_getc(&f1);
        s[i] = nocast_getc(&f2);
    }
    printf("7. 乾 is e4 b9 be. my_getc: %d %d %d   nocast_getc: %d %d %d\n",
           u[0], u[1], u[2], s[0], s[1], s[2]);
    fake(&f1, ff, 1);
    fake(&f2, ff, 1);
    c = my_getc(&f1);
    printf("   byte ff: my_getc -> %d", c);
    c = nocast_getc(&f2);
    printf(", nocast_getc -> %d %s\n", c, c == EOF ? "== EOF: a loop would stop in the middle of the data" : "");
    printf("   (char is signed here, so every byte >= 0x80 -- all of zhouyi.txt's\n"
           "   Chinese -- comes back negative without the cast)\n\n");

    //--- 8. stdin needs no fopen ---------------------------------------
    if (isatty(0))
        printf("8. stdin is the terminal: skipped (run with < zhouyi.txt)\n\n");
    else {
        r0 = nreads;
        //IDIOM: i < sizeof(line) - 1 && ... -- && stops before the
        //call: never read a character there's no room to keep
        for (i = 0; i < (int) sizeof(line) - 1 && (c = my_getchar()) != EOF && c != '\n'; i++)
            line[i] = c;
        //zhouyi.txt has Windows line endings, \r\n: the loop stopped at
        //the \n but kept the \r. Printed, \r sends the cursor back to
        //the start of the line and the closing quote lands on the "8"
        //IDIOM: i > 0 && line[i-1] -- && guards the index: never look
        //at line[-1]
        n = (i > 0 && line[i - 1] == '\r');
        if (n)
            i--;
        line[i] = '\0';
        printf("8. my_getchar(), no my_fopen: \"%s\"\n", line);
        printf("   fd %d was opened by the shell; read() calls: %d, cnt left %d\n",
               my_fileno(my_stdin), nreads - r0, my_stdin->cnt);
        if (n)
            printf("   the line ended \\r\\n (Windows): getc hands back every byte, \\r\n"
                   "   too -- Unix stdio never translates, which is why a \"b\" in the\n"
                   "   mode does nothing here. The \\r was dropped before printing\n");
        printf("\n");
    }

    //--- 9. unbuffered: one read per character -----------------------------
    up = my_fopen("zhouyi.txt", "r");
    up->flag |= M_UNBUF;            //not in the book: make it like stderr
    r0 = nreads;
    for (i = 0; i < 100; i++)
        my_getc(up);
    printf("9. with M_UNBUF set: 100 my_getc -> %d read() calls (buffered: 1)\n\n",
           nreads - r0);

    //--- 10. a macro evaluates its argument more than once -------------------
    MYFILE *fps[2];
    char a[] = "AAAA", b[] = "bbbb";
    fake(&f1, a, 4);
    fake(&f2, b, 4);
    fps[0] = &f1;
    fps[1] = &f2;
    i = 0;
    c = my_getc(fps[i++]);
    printf("10. c = my_getc(fps[i++]): c '%c', i %d -- p appears twice in the macro:\n"
           "    --fps[0]->cnt (now %d), then *fps[1]->ptr++: the wrong file's byte\n"
           "    (fps[1]->cnt is still %d, so file 1 is now out of step too)\n\n",
           c, i, f1.cnt, f2.cnt);

    //--- 11. modes ----------------------------------------------------
    n = open(DEMO, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    write(n, "0123456789", 10);
    close(n);
    ap = my_fopen(DEMO, "a");
    printf("11. %s has 10 bytes. my_fopen \"a\": position %ld -- lseek put it at the end\n",
           DEMO, (long) lseek(ap->fd, 0L, SEEK_CUR));
    my_fopen(DEMO, "w");
    stat(DEMO, &st);
    printf("    my_fopen \"w\": creat truncated it, size now %ld\n", (long) st.st_size);
    printf("    my_fopen \"x\": %s\n", my_fopen(DEMO, "x") == NULL ? "NULL" : "opened?!");
    printf("    \"r+\" -> flag %02o: only the first char is looked at, so it's just \"r\"\n\n",
           my_fopen(DEMO, "r+")->flag);
    unlink(DEMO);

    //--- 12. the table fills up --------------------------------------------
    n = 0;
    while (my_fopen("zhouyi.txt", "r") != NULL)
        n++;
    printf("12. %d more my_fopen worked, then NULL: all %d slots taken.\n"
           "    No my_fclose to free one -- that's exercise 8-3. (The kernel's own\n"
           "    limit here is far bigger: ulimit -n)\n\n", n, MY_OPEN_MAX);

    //--- 13. the real FILE ----------------------------------------------
    FILE *rf = fopen("zhouyi.txt", "r");
    getc(rf);
    printf("13. glibc: sizeof(FILE) %zu (ours %zu), BUFSIZ %d (the book's 1024);\n"
           "    after one getc the kernel position is %ld -- glibc's real buffer size\n",
           sizeof(FILE), sizeof(MYFILE), BUFSIZ, (long) lseek(fileno(rf), 0L, SEEK_CUR));
    fclose(rf);
    return 0;
}

win@DESKTOP-MEIH88T:~$ cd webdev-projects
win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c < zhouyi.txt
1. sizeof(MYFILE) = 32 bytes; my_iob[] holds 20 of them = 640 bytes
   my_iob[0]: fd 0, flag 01  READ          <- stdin
   my_iob[1]: fd 1, flag 02  WRITE         <- stdout
   my_iob[2]: fd 2, flag 06  WRITE|UNBUF   <- stderr
   my_iob[3]: fd 0, flag 00  (0 = free, like the 16 after it)
   my_stdin == &my_iob[0]: yes -- a FILE * is a pointer into this table

2. my_fopen("zhouyi.txt", "r") -> &my_iob[3]: the first free slot
   fd 3 (0, 1, 2 are taken), flag 01, cnt 0, base NULL -- nothing read yet

3. first my_getc -> 0xe5; read() calls so far: 1
   now base is malloc'd, cnt 1023, ptr - base = 1
   the kernel's position: 1024 -- it moved 1024, we used 1: two positions
   2 more my_getc: cnt 1021, read() calls still 1; the 3 bytes: 厥

   [my_fillbuf: read() returned 1024]
   [my_fillbuf: read() returned 197]
   [my_fillbuf: read() returned 0]
4. 264389 characters, 260 read() calls (258 full + 1 short + 1 that got 0)
   a getc FUNCTION would be 264389 calls; the macro called my_fillbuf 260 times

5. my_feof(fp) 1, cnt 0
   my_getc again -> -1, read() calls 0: the flag test stopped it, no read
   and again -> -1, but now cnt is -2: the early return in my_fillbuf
   doesn't reset cnt, so every call after EOF pushes it one lower

6. my_fopen(".", "r") works, my_getc -> -1; my_ferror 1, my_feof 0
   read() returned -1 (a directory), so ERR, not EOF

7. 乾 is e4 b9 be. my_getc: 228 185 190   nocast_getc: -28 -71 -66
   byte ff: my_getc -> 255, nocast_getc -> -1 == EOF: a loop would stop in the middle of the data
   (char is signed here, so every byte >= 0x80 -- all of zhouyi.txt's
   Chinese -- comes back negative without the cast)

". my_getchar(), no my_fopen: "厥孚(integrity)    貞吉凶(ultimate caution)    大有(owner)
   fd 0 was opened by the shell; read() calls: 1, cnt left 954

9. with M_UNBUF set: 100 my_getc -> 100 read() calls (buffered: 1)

10. c = my_getc(fps[i++]): c 'b', i 2 -- p appears twice in the macro:
    --fps[0]->cnt (now 3), then *fps[1]->ptr++: the wrong file's byte
    (fps[1]->cnt is still 4, so file 1 is now out of step too)

11. fopen-demo.txt has 10 bytes. my_fopen "a": position 10 -- lseek put it at the end
    my_fopen "w": creat truncated it, size now 0
    my_fopen "x": NULL
    "r+" -> flag 01: only the first char is looked at, so it's just "r"

12. 11 more my_fopen worked, then NULL: all 20 slots taken.
    No my_fclose to free one -- that's exercise 8-3. (The kernel's own
    limit here is far bigger: ulimit -n)

13. glibc: sizeof(FILE) 216 (ours 32), BUFSIZ 8192 (the book's 1024);
    after one getc the kernel position is 4096 -- glibc's real buffer size
win@DESKTOP-MEIH88T:~/webdev-projects$ echo 乾為天 | ./cc-c
1. sizeof(MYFILE) = 32 bytes; my_iob[] holds 20 of them = 640 bytes
   my_iob[0]: fd 0, flag 01  READ          <- stdin
   my_iob[1]: fd 1, flag 02  WRITE         <- stdout
   my_iob[2]: fd 2, flag 06  WRITE|UNBUF   <- stderr
   my_iob[3]: fd 0, flag 00  (0 = free, like the 16 after it)
   my_stdin == &my_iob[0]: yes -- a FILE * is a pointer into this table

2. my_fopen("zhouyi.txt", "r") -> &my_iob[3]: the first free slot
   fd 3 (0, 1, 2 are taken), flag 01, cnt 0, base NULL -- nothing read yet

3. first my_getc -> 0xe5; read() calls so far: 1
   now base is malloc'd, cnt 1023, ptr - base = 1
   the kernel's position: 1024 -- it moved 1024, we used 1: two positions
   2 more my_getc: cnt 1021, read() calls still 1; the 3 bytes: 厥

   [my_fillbuf: read() returned 1024]
   [my_fillbuf: read() returned 197]
   [my_fillbuf: read() returned 0]
4. 264389 characters, 260 read() calls (258 full + 1 short + 1 that got 0)
   a getc FUNCTION would be 264389 calls; the macro called my_fillbuf 260 times

5. my_feof(fp) 1, cnt 0
   my_getc again -> -1, read() calls 0: the flag test stopped it, no read
   and again -> -1, but now cnt is -2: the early return in my_fillbuf
   doesn't reset cnt, so every call after EOF pushes it one lower

6. my_fopen(".", "r") works, my_getc -> -1; my_ferror 1, my_feof 0
   read() returned -1 (a directory), so ERR, not EOF

7. 乾 is e4 b9 be. my_getc: 228 185 190   nocast_getc: -28 -71 -66
   byte ff: my_getc -> 255, nocast_getc -> -1 == EOF: a loop would stop in the middle of the data
   (char is signed here, so every byte >= 0x80 -- all of zhouyi.txt's
   Chinese -- comes back negative without the cast)

8. my_getchar(), no my_fopen: "乾為天"
   fd 0 was opened by the shell; read() calls: 1, cnt left 0

9. with M_UNBUF set: 100 my_getc -> 100 read() calls (buffered: 1)

10. c = my_getc(fps[i++]): c 'b', i 2 -- p appears twice in the macro:
    --fps[0]->cnt (now 3), then *fps[1]->ptr++: the wrong file's byte
    (fps[1]->cnt is still 4, so file 1 is now out of step too)

11. fopen-demo.txt has 10 bytes. my_fopen "a": position 10 -- lseek put it at the end
    my_fopen "w": creat truncated it, size now 0
    my_fopen "x": NULL
    "r+" -> flag 01: only the first char is looked at, so it's just "r"

12. 11 more my_fopen worked, then NULL: all 20 slots taken.
    No my_fclose to free one -- that's exercise 8-3. (The kernel's own
    limit here is far bigger: ulimit -n)

13. glibc: sizeof(FILE) 216 (ours 32), BUFSIZ 8192 (the book's 1024);
    after one getc the kernel position is 4096 -- glibc's real buffer size
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c
1. sizeof(MYFILE) = 32 bytes; my_iob[] holds 20 of them = 640 bytes
   my_iob[0]: fd 0, flag 01  READ          <- stdin
   my_iob[1]: fd 1, flag 02  WRITE         <- stdout
   my_iob[2]: fd 2, flag 06  WRITE|UNBUF   <- stderr
   my_iob[3]: fd 0, flag 00  (0 = free, like the 16 after it)
   my_stdin == &my_iob[0]: yes -- a FILE * is a pointer into this table

2. my_fopen("zhouyi.txt", "r") -> &my_iob[3]: the first free slot
   fd 3 (0, 1, 2 are taken), flag 01, cnt 0, base NULL -- nothing read yet

3. first my_getc -> 0xe5; read() calls so far: 1
   now base is malloc'd, cnt 1023, ptr - base = 1
   the kernel's position: 1024 -- it moved 1024, we used 1: two positions
   2 more my_getc: cnt 1021, read() calls still 1; the 3 bytes: 厥

   [my_fillbuf: read() returned 1024]
   [my_fillbuf: read() returned 197]
   [my_fillbuf: read() returned 0]
4. 264389 characters, 260 read() calls (258 full + 1 short + 1 that got 0)
   a getc FUNCTION would be 264389 calls; the macro called my_fillbuf 260 times

5. my_feof(fp) 1, cnt 0
   my_getc again -> -1, read() calls 0: the flag test stopped it, no read
   and again -> -1, but now cnt is -2: the early return in my_fillbuf
   doesn't reset cnt, so every call after EOF pushes it one lower

6. my_fopen(".", "r") works, my_getc -> -1; my_ferror 1, my_feof 0
   read() returned -1 (a directory), so ERR, not EOF

7. 乾 is e4 b9 be. my_getc: 228 185 190   nocast_getc: -28 -71 -66
   byte ff: my_getc -> 255, nocast_getc -> -1 == EOF: a loop would stop in the middle of the data
   (char is signed here, so every byte >= 0x80 -- all of zhouyi.txt's
   Chinese -- comes back negative without the cast)

8. stdin is the terminal: skipped (run with < zhouyi.txt)

9. with M_UNBUF set: 100 my_getc -> 100 read() calls (buffered: 1)

10. c = my_getc(fps[i++]): c 'b', i 2 -- p appears twice in the macro:
    --fps[0]->cnt (now 3), then *fps[1]->ptr++: the wrong file's byte
    (fps[1]->cnt is still 4, so file 1 is now out of step too)

11. fopen-demo.txt has 10 bytes. my_fopen "a": position 10 -- lseek put it at the end
    my_fopen "w": creat truncated it, size now 0
    my_fopen "x": NULL
    "r+" -> flag 01: only the first char is looked at, so it's just "r"

12. 11 more my_fopen worked, then NULL: all 20 slots taken.
    No my_fclose to free one -- that's exercise 8-3. (The kernel's own
    limit here is far bigger: ulimit -n)

13. glibc: sizeof(FILE) 216 (ours 32), BUFSIZ 8192 (the book's 1024);
    after one getc the kernel position is 4096 -- glibc's real buffer size
win@DESKTOP-MEIH88T:~/webdev-projects$ wc -c zhouyi.txt
264389 zhouyi.txt
win@DESKTOP-MEIH88T:~/webdev-projects$ tr -d '\r' < zhouyi.txt | wc -c
259932