Foundations Updated 2026-09 View as Markdown

File I/O

Reading and writing files in C, and the error checks that generated code omits every single time.

c
#include <stdio.h>

FILE *f = fopen("data.txt", "r");
if (f == NULL) {                       /* ALWAYS check */
    perror("fopen data.txt");
    return -1;
}
/* ... */
fclose(f);

fopen returns NULL on failure and sets errno. perror prints your message followed by the system's description, which is almost always what you want in a small tool.

Modes#

ModeMeaning
"r"read; fails if the file does not exist
"w"write; truncates an existing file to zero length
"a"append; writes always go to the end
"r+"read and write; file must exist
"w+"read and write; truncates
"a+"read and append

Add b for binary ("rb", "wb"). On POSIX it changes nothing; on Windows it stops newline translation corrupting binary data. Use it whenever the content is not text.

Reading lines#

c
char line[256];
while (fgets(line, sizeof line, f) != NULL) {
    line[strcspn(line, "\n")] = '\0';      /* strip the newline */
    printf("read: %s\n", line);
}
if (ferror(f)) {                            /* fgets returns NULL for EOF AND error */
    perror("read");
    return -1;
}

Two details that matter:

  • fgets returns NULL for both end-of-file and error. Without the ferror check you cannot tell "finished the file" from "gave up halfway", and generated code omits this check essentially always.
  • A line longer than the buffer is split. fgets reads at most size - 1 bytes; the rest arrives on the next call. If the distinction matters, check whether the buffer contains a \n.

getline (POSIX) allocates for you and handles any line length:

c
char *line = NULL;
size_t cap = 0;
ssize_t len;

while ((len = getline(&line, &cap, f)) != -1) {
    if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
    puts(line);
}
free(line);                                 /* free once, at the end */

Note getline reuses and grows the same buffer, so you free it once after the loop, not inside it.

Reading a whole file#

c
static char *read_all(const char *path, size_t *out_len) {
    FILE *f = fopen(path, "rb");
    if (!f) return NULL;

    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
    long size = ftell(f);
    if (size < 0) { fclose(f); return NULL; }
    rewind(f);

    if ((unsigned long)size > MAX_FILE_BYTES) { fclose(f); return NULL; }  /* bound it */

    char *buf = malloc((size_t)size + 1);
    if (!buf) { fclose(f); return NULL; }

    size_t got = fread(buf, 1, (size_t)size, f);
    if (got != (size_t)size) { free(buf); fclose(f); return NULL; }

    buf[size] = '\0';
    *out_len = got;
    fclose(f);
    return buf;
}

The bound check is the security-relevant line: without it, the file's size decides your allocation, and a huge or special file exhausts memory. See the security review.

Binary I/O#

c
struct record { uint32_t id; char name[32]; double score; };

struct record r = { .id = 1, .name = "ada", .score = 99.5 };

if (fwrite(&r, sizeof r, 1, f) != 1) { perror("write"); return -1; }

struct record back;
if (fread(&back, sizeof back, 1, f) != 1) {
    if (feof(f)) fprintf(stderr, "unexpected end of file\n");
    else perror("read");
    return -1;
}

fread and fwrite return the number of items transferred, not bytes. Compare against the count you asked for.

Writing a struct directly is fine for a file only your program reads on one machine. It is not a portable format: struct padding, integer endianness and type sizes all vary. For anything that crosses machines, serialise field by field with fixed-width types.

fclose can fail#

c
if (fclose(f) != 0) {
    perror("fclose");           /* buffered data may not have reached disk */
    return -1;
}

Output is buffered, so a fwrite can succeed while the actual disk write fails later — and fclose is where you find out. For a file you wrote, the close error is the one that tells you the write failed, and it is the one generated code always discards.

For data that must survive a crash, flush and sync before closing:

c
fflush(f);
fsync(fileno(f));
fclose(f);

Standard streams#

c
fprintf(stderr, "warning: %s\n", msg);    /* diagnostics go to stderr */
printf("result: %d\n", value);            /* actual output to stdout */

Keeping them separate is what lets a caller do ./tool > out.txt and still see errors. stderr is unbuffered, stdout is line-buffered to a terminal and block-buffered to a pipe — which is why interleaved output can look out of order when redirected.

Exercise#

c
#include <stdio.h>

int main(void) {
    /* Write a program that:
       1. reads "input.txt" line by line with getline
       2. writes lines containing "ERROR" to "errors.txt"
       3. checks EVERY call: fopen, getline via ferror, fclose on both files
       4. writes via a temp file + rename so a failure leaves the old file intact
       5. returns 0 on success, non-zero on any failure                        */
    printf("start\n");
    return 0;
}

Common questions#

fgets or getline?#

getline when it is available — it handles any line length and allocates for you. fgets when you need strict C portability or want a fixed-size buffer with no allocation, and then you must handle over-long lines yourself.

Why check the return value of fclose?#

Because writes are buffered. fwrite can report success while the data is still in memory, and the real failure — a full disk, an I/O error — surfaces when the buffer is flushed at close. Discarding that return means silently losing data.

Is writing a struct with fwrite safe?#

Only for a private format read back by the same program on the same platform. Padding, endianness and type sizes differ between compilers and architectures, so a struct dumped on one machine may not read correctly on another. Serialise explicitly for anything portable.

Get the C agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for C. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.