# File I/O

> Source: https://learn-c.net/file-io/
> Part of Learn C, free to read.

```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

| Mode | Meaning |
|---|---|
| `"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.

:::danger `"w"` destroys the file before you write anything
`fopen(path, "w")` truncates immediately. If your program then fails, the original content is gone. For anything you cannot afford to lose, write to a temporary file and rename:

```c
char tmp[PATH_MAX];
snprintf(tmp, sizeof tmp, "%s.tmp", path);

FILE *f = fopen(tmp, "w");
if (!f) return -1;
if (write_everything(f) != 0) { fclose(f); remove(tmp); return -1; }
if (fclose(f) != 0) { remove(tmp); return -1; }   /* check the close! */
if (rename(tmp, path) != 0) { remove(tmp); return -1; }
```
`rename` within a filesystem is atomic, so a reader sees either the old file or the new one — never a half-written one.
:::

## 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.

:::warn Never use `gets`
It has no way to know the buffer size and was removed from the C standard in C11. If you see it in generated code, it is a buffer overflow waiting for input longer than your array.
:::

## 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](/review/security/).

## 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.
