Intermediate

I/O Operations in C

Learn console and file input/output in C - printf formatting, reading stdin, and working with FILE pointers using Docker-ready examples

Input and output are where a program meets the outside world. In C, all I/O lives in the standard library rather than the language itself - the <stdio.h> header (standard input/output) provides the functions you use to print to the screen, read what a user types, and read or write files on disk.

As a procedural systems language, C exposes I/O at a fairly low level. There is no automatic string handling or garbage collection cleaning up after you: you manage buffers yourself, you check whether a file actually opened, and you close what you open. Streams are represented by FILE pointers, and three of them - stdin, stdout, and stderr - are opened for you automatically when the program starts.

This tutorial covers formatted console output with printf, reading keyboard input safely, and reading and writing files with FILE pointers. Each example is self-contained and runs unchanged inside the official gcc:14 Docker image.

Formatted Output with printf

You already met printf in Hello World. Its real power is format specifiers - placeholders like %d and %f that control exactly how each value is rendered. This is the printf-style formatted output that dozens of later languages inherited from C.

Create a file named io_output.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(void) {
    int         count = 42;
    double      price = 19.95;
    char        grade = 'A';
    const char *name  = "codearchaeology";

    printf("Integer:        %d\n", count);
    printf("Hexadecimal:    %#x\n", count);
    printf("Float (2 dp):   %.2f\n", price);
    printf("Character:      %c\n", grade);
    printf("String:         %s\n", name);
    printf("Padded number: |%5d|\n", count);
    printf("Left-aligned:  |%-5d|\n", count);
    printf("Percent sign:   100%%\n");

    return 0;
}

Each specifier maps to one argument, in order:

  • %d - a signed decimal integer.
  • %#x - an integer in hexadecimal; the # flag adds the 0x prefix.
  • %.2f - a floating-point value with exactly two digits after the decimal point.
  • %c - a single character.
  • %s - a null-terminated string.
  • %5d / %-5d - a field width of 5, right-aligned by default, left-aligned with -.
  • %% - a literal percent sign (you cannot write a bare %).

Reading Input from stdin

To read what a user types, you read from stdin. The safest general-purpose approach is fgets, which reads a whole line into a fixed-size buffer and will never overflow it. fgets keeps the trailing newline, so we strip it. For numbers, scanf with a format specifier parses the text.

Create a file named io_input.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int main(void) {
    char name[64];
    int  age;

    printf("Enter your name: ");
    if (fgets(name, sizeof(name), stdin) == NULL) {
        fprintf(stderr, "Error reading name\n");
        return 1;
    }

    /* fgets keeps the newline - replace it with a string terminator */
    for (int i = 0; name[i] != '\0'; i++) {
        if (name[i] == '\n') {
            name[i] = '\0';
            break;
        }
    }

    printf("Enter your age: ");
    if (scanf("%d", &age) != 1) {
        fprintf(stderr, "Error reading age\n");
        return 1;
    }

    printf("Hello, %s! Next year you will be %d.\n", name, age + 1);
    return 0;
}

A few C-specific details worth noting:

  • sizeof(name) passes the buffer’s capacity so fgets cannot write past the array - this is how you avoid buffer overflows.
  • &age passes the address of age; scanf needs a pointer so it can store the parsed value back into your variable.
  • Return values are your error handling. fgets returns NULL on failure and scanf returns the number of items successfully read. Checking them is the idiomatic way to detect bad input in C.
  • fprintf(stderr, ...) writes errors to the standard error stream instead of stdout.

Reading and Writing Files

Files are handled through FILE pointers. You fopen a file with a mode string ("w" for write, "r" for read, "a" for append), work with it, and fclose it. fopen returns NULL if it fails - checking that is not optional in correct C code. The example below writes three lines to a file, then reopens it and reads them back.

Create a file named io_files.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

int main(void) {
    /* Open a file for writing ("w" truncates any existing file) */
    FILE *out = fopen("notes.txt", "w");
    if (out == NULL) {
        perror("fopen (write)");
        return 1;
    }
    fprintf(out, "Line 1: C handles files with FILE pointers.\n");
    fprintf(out, "Line 2: Always check the return of fopen.\n");
    fprintf(out, "Line 3: Close files with fclose.\n");
    fclose(out);

    /* Reopen the same file for reading */
    FILE *in = fopen("notes.txt", "r");
    if (in == NULL) {
        perror("fopen (read)");
        return 1;
    }

    char line[128];
    int  number = 1;
    while (fgets(line, sizeof(line), in) != NULL) {
        printf("%2d | %s", number, line);
        number++;
    }
    fclose(in);

    return 0;
}

Key points:

  • fprintf works exactly like printf but writes to a stream you name - here the open file rather than the console.
  • perror prints your message followed by a human-readable description of the system error (for example, Permission denied).
  • fgets in a loop is the standard idiom for reading a file line by line; it returns NULL at end-of-file, ending the loop.
  • Each line read already contains its newline, so the printf format string has no \n of its own.

Running with Docker

The gcc:14 image compiles and runs all three programs. For the input example we pipe text into the program’s stdin so it runs without interaction.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official GCC image
docker pull gcc:14

# Formatted output
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "gcc -o io_output io_output.c && ./io_output"

# Reading input (piped in via stdin)
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "gcc -o io_input io_input.c && printf 'Ada\n42\n' | ./io_input"

# Writing and reading a file
docker run --rm -v $(pwd):/app -w /app gcc:14 \
    sh -c "gcc -o io_files io_files.c && ./io_files"

Expected Output

Running io_output:

Integer:        42
Hexadecimal:    0x2a
Float (2 dp):   19.95
Character:      A
String:         codearchaeology
Padded number: |   42|
Left-aligned:  |42   |
Percent sign:   100%

Running io_input with the piped input Ada and 42:

Enter your name: Enter your age: Hello, Ada! Next year you will be 43.

Running io_files (which also creates notes.txt in your directory):

 1 | Line 1: C handles files with FILE pointers.
 2 | Line 2: Always check the return of fopen.
 3 | Line 3: Close files with fclose.

Key Concepts

  • All I/O comes from <stdio.h> - the language has no built-in I/O statements; you call library functions instead.
  • Three streams open automatically: stdin (input), stdout (normal output), and stderr (error output). Sending errors to stderr keeps them separate from a program’s real output.
  • Format specifiers control rendering - %d, %f, %s, %c, %x, plus width, precision, and alignment flags like %.2f and %-5d.
  • Prefer fgets over gets - fgets takes a buffer size and cannot overflow; the old gets function is unsafe and was removed from the standard.
  • scanf needs addresses - pass &variable so the function can write the parsed value back to you.
  • Check every return value - fopen returns NULL, scanf returns a count, and fgets returns NULL at end-of-file or error. In C, these return values are your error handling.
  • Open, use, close - every fopen should be paired with an fclose; C will not clean up open file handles for you.

Running Today

All examples can be run using Docker:

docker pull gcc:14
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining