CLA (C language) study materials
CLA (C language) study resources

feof Loop, Double-Evaluating Macro, and a Leaked FILE

CLA (C language) · Put your knowledge to work in context.

All case studies

feof Loop, Double-Evaluating Macro, and a Leaked FILE

A log summarizer processes files up to 2 GB with this loop:

char buf[256];
size_t count = 0;
while (!feof(in)) {
    fgets(buf, sizeof buf, in);
    count++;
}

For a file containing exactly three newline-terminated lines it reports 4 lines. On a file where an I/O error is injected after 10 MB, the loop never terminates. The same source defines:

#define MAX(a,b) ((a) > (b) ? (a) : (b))
int m = MAX(i++, j);

With i == 5 and j == 3, the test expects m == 6 and i == 6, but after the call i == 7. The function also returns -1 on a malformed line after opening the file but before fclose; after 1021 such files the process fails with EMFILE (limit 1024). The team needs a loop that terminates on EOF and error, a safe MAX, and no descriptor leak.

1. Explain why the loop reports 4 lines for a 3-line file and why it never terminates on a read error. Rewrite the loop so it handles EOF, error, and long lines.

Read the worked answer

feof does not predict EOF; it becomes true only after a read has already failed because of EOF. For three newline-terminated lines: iterations 1–3 read each line; iteration 4 calls fgets, which immediately hits EOF and returns NULL without changing the buffer; the code ignores the return value and increments count anyway, so count becomes 4; the next feof is true and the loop exits. On an I/O error, fgets returns NULL, but feof stays false and ferror becomes true, so the loop repeats forever with count growing. Rewrite:

while (fgets(buf, sizeof buf, in) != NULL) {
    size_t len = strlen(buf);
    if (len > 0 && buf[len-1] == '\n') {
        count++;
    } else if (!feof(in)) {
        int c;
        while ((c = getc(in)) != '\n' && c != EOF) { }
        count++;
    }
}
if (ferror(in)) {
    /* report the I/O error */
}

The loop condition uses the read result, not feof. The long-line handling prevents a 256-byte buffer from treating one 2 GB line as multiple lines; it consumes the remainder and counts the logical line once. After the loop, check ferror to distinguish normal EOF from an error.

2. The macro `MAX(i++, j)` with `i == 5` and `j == 3` leaves `i == 7` and yields 6, while the test expects `i == 6` and 6. Explain the expansion and give a standard C fix.

Read the worked answer

The macro expands to ((i++) > (j) ? (i++) : (j)). With i == 5 and j == 3, the condition evaluates i++: it yields 5, then i becomes 6; 5 > 3 is true, so the true branch evaluates the second i++: it yields 6, then i becomes 7; the result is 6. The argument expression is substituted twice, so a side effect in the argument can execute twice. The conditional operator does sequence the condition before the chosen branch, so this particular case is defined, but the macro is still wrong because the number of evaluations depends on the comparison. Replace it with an inline function:

static inline int max_int(int a, int b) {
    return a > b ? a : b;
}

Then max_int(i++, j) evaluates i++ once. If a macro is unavoidable, use a GNU statement expression or a function-like macro that evaluates arguments once into locals, but a standard C inline function is clearer and type-checked. Parenthesizing the macro body does not fix double evaluation.

3. The function returns -1 on a malformed line without closing the file. Explain the `EMFILE` symptom and show a cleanup structure that closes the file on every path.

Read the worked answer

Each successful fopen consumes a file descriptor. The process limit is 1024 descriptors; with three already used by stdin, stdout, and stderr, only about 1021 remain. If every malformed file leaks one descriptor, the 1022nd open fails with EMFILE (too many open files). The leak also keeps the file offset and buffers alive, and fclose may not flush buffered output. Use a single cleanup path:

int process(const char *path) {
    FILE *in = fopen(path, "r");
    if (in == NULL) return -1;
    int rc = 0;
    char buf[256];
    while (fgets(buf, sizeof buf, in) != NULL) {
        if (malformed(buf)) { rc = -1; goto cleanup; }
        /* ... */
    }
    if (ferror(in)) rc = -1;
cleanup:
    if (fclose(in) != 0 && rc == 0) rc = -1;
    return rc;
}

Every exit after a successful fopen goes through cleanup, so the descriptor is released. Check fclose for write errors if the stream was opened for writing; for reading, a fclose failure is still worth reporting. Avoid return statements that bypass fclose, or duplicate the close on each path if the function is too small for goto.