Why scanf Needs Ctrl-Z Pressed Three Times for EOF on Windows (And How to Fix It)
The Frustrating "Three Ctrl-Z" Mystery in Windows Console Apps
If you have ever developed a C console application on Windows and tried to signal an End-of-File (EOF) using Ctrl+Z followed by Enter, you may have noticed a bizarre inconsistency: while fgets() detects EOF on the very first try, scanf() often sits silently until you press Ctrl+Z and Enter three times in a row.
This behavior is not a flaw in your program logic, nor is it a compiler-specific bug with MinGW or MSVC. Instead, it stems from how the Windows Universal C Runtime (UCRT) and the Windows Console subsystem handle text-mode EOF lookaheads and character ungetting.
Why Does scanf Require Three Consecutive Ctrl-Z Presses?
To understand the root cause, we need to examine how Windows console line input interacts with scanf's internal token parser.
1. Console Line Buffering vs. EOF Characters
By default, the Windows console runs in line-buffered mode (ENABLE_LINE_INPUT). Characters are not dispatched to the C runtime's input buffer until you hit Enter. When you press Ctrl+Z followed by Enter, the console delivers byte 26 (ASCII substitution character 0x1A) followed by carriage return and newline characters (
).
2. How scanf("%s") Reads Tokens
When parsing a format specifier like %s or skipping leading whitespace, scanf must perform three distinct phases:
- Skip leading whitespace: It reads characters one by one using an internal
fgetc-like routine until it finds a non-whitespace character or hits EOF. - Read the token: It reads non-whitespace characters into your buffer until it encounters whitespace or EOF.
- Lookahead and unget: When it hits a delimiter (such as a space or newline), it must call
ungetc()to push that terminating character back onto the stream so subsequent reads can see it.
3. The UCRT Console Lookahead Glitch
The discrepancy between fgets() and scanf() comes down to character pushback:
fgets()simply reads until it encounters a newline () or EOF. Once the underlying read returns EOF,fgets()terminates and returnsNULLimmediately.scanf(), however, inspects characters ahead of time. In text mode on Windows, the CRT encounters0x1A, sets the internal stream EOF flag, and attempts lookahead logic. Because0x1Awas followed by line termination in the console buffer, the CRT's stream reading logic tries to reconcile the pushed-back character state with the Windows console handle (viaReadConsoleWorReadFile).- Because the EOF condition is not "sticky" across internal pushback and refill attempts in the CRT's console stream reader,
scanfrepeatedly queries the console buffer. The first Ctrl+Z satisfies the character fetch, the second handles stream synchronization after the failed lookahead/pushback, and the third finally forces the stream state machine to confirm an unrecoverable EOF.
On Unix-like systems (and WSL), Ctrl+D instructs the tty driver to flush an empty buffer, immediately causing read() to return 0 bytes, which the standard C library seamlessly translates to EOF in one step.
The Solution: The "Read-Line, Then Parse" Pattern
Relying on scanf() directly on interactive console streams is notoriously fraught with edge cases—not just for EOF detection, but also for leaving leftover newline characters in the buffer. The universally recommended solution in C is to decouple line retrieval from parsing using fgets() and sscanf().
Refactored Example
Here is how to update a word-reading prompt so that it immediately responds to a single Ctrl+Z + Enter:
#include <stdio.h> // standard I/O functions
#include <string.h> // strcspn, strlen
#define BUFFER_SIZE 1024
#define MAX_WORD_LEN 100
#define STR_(x) #x
#define STR(x) STR_(x)
int main(void) {
char line[BUFFER_SIZE];
char word[MAX_WORD_LEN + 1];
printf("Enter a word: ");
fflush(stdout);
// fgets immediately returns NULL on the first Ctrl+Z + Enter
if (fgets(line, sizeof(line), stdin) == NULL) {
puts("\nEOF encountered.");
return 0;
}
// Parse the token safely from the line buffer in memory
if (sscanf(line, "%" STR(MAX_WORD_LEN) "s", word) == 1) {
printf("Read word: %s\n", word);
} else {
puts("No valid word found.");
}
return 0;
}
Why This Fix Works
- Predictable EOF:
fgets()talks directly to the stream without token lookahead or whitespace skipping. A single Ctrl+Z triggers EOF cleanly. - No Stray Input: Whole-line reading cleans out carriage returns and newlines from
stdin, preventing subsequent input calls from consuming leftover characters. - Safe Parsing:
sscanf()works on an in-memory buffer where lookaheads and string boundaries are completely free of operating system console quirks.
Alternative: Checking feof() Manually
If you must read individual characters or tokens without buffering full lines, use getchar() or fgetc() in an explicit loop, and inspect feof(stdin) directly rather than chaining format strings inside scanf():
#include <stdio.h>
int main(void) {
int ch;
printf("Enter input: ");
fflush(stdout);
ch = getchar();
if (ch == EOF) {
puts("\nEOF detected immediately.");
} else {
printf("Character code read: %d\n", ch);
}
return 0;
}
Summary
The requirement to press Ctrl+Z three times is an inherent artifact of the Windows C Runtime's console stream lookahead and ungetc() mechanics inside scanf. Rather than fighting CRT internals, adopt the standard C best practice: read full lines into memory with fgets(), and parse them with sscanf().