Skip to main content

Standard I/O: printf, scanf, fgets, and Why gets Was Removed

What This Concept Is

<stdio.h> exposes the C Standard Library's stream I/O interface. The three implicit streams are stdin, stdout, and stderr. Each is a FILE * with a library-managed buffer.

The main operations:

  • Formatted output: printf(fmt, ...), fprintf(stream, fmt, ...), snprintf(buf, size, fmt, ...).
  • Formatted input: scanf(fmt, ...), fscanf(stream, fmt, ...), sscanf(str, fmt, ...).
  • Line input: fgets(buf, size, stream). Reads up to size - 1 characters or a newline, whichever comes first. Always null-terminates.
  • Character input/output: getchar, putchar, fgetc, fputc.
  • File access: fopen, fread, fwrite, fclose, plus ferror/feof for status.

gets(buf) used to read a line into buf without any size limit. It was deprecated in C99 and removed in C11. If you see it in old code, replace it with fgets(buf, sizeof buf, stdin) immediately.

Why It Matters Here

Almost every first C program you write does I/O, and I/O is where most early crashes appear:

  • printf("%d", x) when x is a long long -> undefined behavior
  • scanf("%d", n) instead of scanf("%d", &n) -> undefined behavior
  • fgets not checking its return for NULL at EOF -> reading uninitialized buffer
  • printf(user_input) -> classic format-string vulnerability

All of these are avoidable with a few rules you learn once.

Concrete Example

Reading a line and parsing two numbers safely:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
char line[256];
if (!fgets(line, sizeof line, stdin)) {
fprintf(stderr, "no input\n");
return 1;
}
/* strip trailing newline if present */
size_t n = strlen(line);
if (n > 0 && line[n - 1] == '\n') line[n - 1] = '\0';

int a, b;
if (sscanf(line, "%d %d", &a, &b) != 2) {
fprintf(stderr, "expected two integers, got: %s\n", line);
return 1;
}
printf("sum = %d\n", a + b);
return 0;
}

Why this pattern:

  • fgets bounds the read and null-terminates.
  • sscanf parses from an already-bounded buffer, so failure is recoverable.
  • printf gets the correct %d for an int.

Common Confusion / Misconception

"scanf is fine for interactive input." It leaves whitespace and newlines in the buffer in ways that confuse the next call. Prefer fgets + sscanf for anything non-trivial.

"printf is type-safe because of the format string." It is type-safe only if the format matches the arguments. The compiler warns via -Wformat when it can see the format string literally, but not when the format string is computed at runtime.

"gets is still there if I include the right header." No. It is gone in C11. Even in C89 code, treat it as removed.

How To Use It

Rules:

  1. printf("%d", int_value);, printf("%ld", long_value);, printf("%zu", size_t_value);, printf("%s", "string");. Always match specifier to argument type.
  2. scanf("%d", &n); - pass an address for every %d, %s, etc.
  3. Use fgets(buf, sizeof buf, stdin) for line input. Always check the return value.
  4. Use snprintf(buf, sizeof buf, ...) instead of sprintf.
  5. Never pass user input directly as a format string: printf("%s", user_input);, not printf(user_input);.
  6. Check fclose, fread, fwrite return values. Partial reads/writes happen.

Check Yourself

  1. Why was gets removed from C11?
  2. What happens if the format specifier in printf does not match the argument type?
  3. How do you detect an EOF with fgets?

Mini Drill or Application

Write a small program that:

  1. Reads lines from stdin until EOF using fgets.
  2. For each line, trims the trailing newline and tokenizes it with strtok on spaces.
  3. Prints the token count to stdout and errors to stderr.
  4. Returns 0 on normal EOF, non-zero on any I/O error (check ferror).

Compile with gcc -Wall -Wextra -std=c11 -o tokcount tokcount.c.

Read This Only If Stuck