I/O#

C has two I/O surfaces: the stdio layer in <stdio.h> (fopen, fread, fwrite, printf) and the POSIX syscall layer in <unistd.h> (open, read, write). The stdio layer is buffered and portable; the syscall layer is unbuffered and Unix-specific.

For networking I/O, see Networking. For CLI argument plumbing, see CLI.

Buffered (stdio)#

Files#

#include <stdio.h>

FILE *f = fopen(path, "rb");          /* "r" "w" "a" + "b" for binary */
if (!f) { perror("fopen"); return -1; }

char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof buf, f)) > 0) {
    process(buf, n);
}
if (ferror(f)) perror("fread");

fclose(f);

FILE *out = fopen("out.txt", "w");
fputs("hello\n", out);
fprintf(out, "port=%d\n", 8080);
fclose(out);

Standard streams#

stdin, stdout, stderr are pre-opened FILE *.

fputs("hello, world\n", stdout);
fprintf(stderr, "warning: %s\n", msg);

char line[1024];
while (fgets(line, sizeof line, stdin)) {
    /* line includes the trailing \n unless EOF cut it short */
}

Use fgets (or getline on POSIX), never gets (removed in C11, with cause).

Formatted I/O#

printf family. Common verbs.

Verb

Type

Example

%d / %i

int

printf("%d\n", 42);

%u

unsigned

printf("%u\n", 42u);

%ld / %lld

long / long long

%zd / %zu

ssize_t / size_t

the right verb for sizes

%x / %X

hex

printf("0x%08x\n", 255);

%f / %g

double

%s

char *

%c

int (char)

%p

pointer

%%

literal %

For stdint.h types, <inttypes.h> defines macros.

#include <inttypes.h>
uint64_t v = 1;
printf("%" PRIu64 "\n", v);

snprintf writes to a buffer with a size cap; the operator uses it everywhere instead of sprintf (which can overflow).

char msg[64];
int n = snprintf(msg, sizeof msg, "%s:%d", host, port);
if (n < 0 || (size_t)n >= sizeof msg) { /* truncated */ }

Unbuffered (POSIX)#

The syscall layer talks to the kernel directly. No buffering; each read / write is a syscall.

#include <fcntl.h>
#include <unistd.h>

int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open"); return -1; }

char buf[4096];
ssize_t n;
while ((n = read(fd, buf, sizeof buf)) > 0) {
    process(buf, n);
}
if (n < 0) perror("read");
close(fd);

For writes, write returns the number of bytes actually written; the operator loops if it is less than requested.

ssize_t written = 0;
while ((size_t)written < n) {
    ssize_t w = write(fd, buf + written, n - written);
    if (w < 0) { if (errno == EINTR) continue; return -1; }
    written += w;
}

Memory-mapped I/O#

mmap maps a file into the process’s address space. Useful for large read-only inputs (search through gigabyte files without copying through buffers).

#include <sys/mman.h>

int fd = open(path, O_RDONLY);
struct stat st; fstat(fd, &st);
void *base = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (base == MAP_FAILED) { perror("mmap"); return -1; }
/* base[0..st.st_size] is the file contents */
munmap(base, st.st_size);
close(fd);

Reading whole files#

The “read a file into memory” idiom.

int slurp(const char *path, char **out, size_t *out_n) {
    FILE *f = fopen(path, "rb");
    if (!f) return -1;
    fseek(f, 0, SEEK_END);
    long n = ftell(f);
    fseek(f, 0, SEEK_SET);

    char *buf = malloc((size_t)n + 1);
    if (!buf) { fclose(f); return -1; }
    size_t got = fread(buf, 1, (size_t)n, f);
    fclose(f);
    if (got != (size_t)n) { free(buf); return -1; }
    buf[n] = '\0';

    *out = buf;
    *out_n = (size_t)n;
    return 0;
}

Atomic writes#

Write to a temp file, fsync, then rename over the target.

int write_atomic(const char *path, const void *data, size_t n) {
    char tmp[PATH_MAX];
    snprintf(tmp, sizeof tmp, "%s.tmp", path);

    int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) return -1;

    const char *p = data;
    while (n > 0) {
        ssize_t w = write(fd, p, n);
        if (w < 0) { close(fd); unlink(tmp); return -1; }
        p += w; n -= w;
    }
    fsync(fd);
    close(fd);

    if (rename(tmp, path) < 0) { unlink(tmp); return -1; }
    return 0;
}

References#