들어가면서
질문 게시판에 올라오는 C/C++ 코드 중 종종 fflush(stdin)
또는 rewind(stdin)
을 사용한 코드들이 올라옵니다.
결론부터 말하자면, 이런 문장을 쓰는 프로그램은 호환성이 없으며, 구현체가 달라지면 전혀 다른 행동을 할 수 있습니다.
위 두 문장은 Windows 운영체계에서 Microsoft Visual C++ (이후 MSVC)로 컴파일한 프로그램을 실행하고, stdin 이 키보드인 경우에만 원하는 동작을 하고, BOJ 의 채점 환경인 Linux 운영체계에서 GCC 또는 clang 로 컴파일한 프로그램을 실행하고, stdin 이 파일인 경우에는 전혀 다른 동작을 합니다. 이 두 함수의 원래 의미를 설명하는 것으로 글을 시작하겠습니다.
fflush
fflush(FILE *stream)
은 출력 스트림의 내부 버퍼에 남아있는 데이터를 강제로 출력하는 함수입니다.
입출력은 매우 무거운 연산이기 때문에, 성능 향상을 위해 중간에 버퍼를 둡니다. 출력 명령이 내려진 경우, 한글자 한글자마다 출력을 하는 대신, 버퍼에 출력할 데이터를 차곡차곡 쌓아놓습니다. 버퍼에 데이터가 어느 정도 이상 쌓이게 되면, 쌓여있는 데이터를 한꺼번에 출력합니다.
BOJ 의 채점 프로그램은 일반적으로 프로그램이 모두 끝난 후 최종 출력결과를 모범답안과 비교하기 때문에
일부러 출력 스트림을 비울 필요가 없습니다.
단, 문제에 인터렉티브
태그가 달려있는 경우, 여러분이 작성한 프로그램과 채점 프로그램이 동시에 실행하면서 데이터를 주고받으면서 채점이 이루어집니다.
여러분의 프로그램의 출력 버퍼에 데이터가 있으면 채점 프로그램은 그 데이터를 읽을 수 없기 때문에, 매번 fflush(stdout)
를 호출해서, 버퍼에 남아있는 내용을 채점 프로그램이 읽을 수 있도록 해야 합니다.
반면, 입력 스트림에 fflush
함수를 호출한 결과는 정의되어 있지 않습니다.
이를 전문 용어로 Undefined Behavior 라고 부르며, int a[10]; a[100] = 42;
같은 말도 안되는 문장을 실행하는 것과 크게 다르지 않습니다.
특이하게 MSVC는 입력 스트림에 fflush
함수를 호출 할 때의 동작을 문서에 명시하고 있습니다. (다른 C 컴파일러에서는 여전히 Undefined Behavior 입니다.)
예전 버전의 MSVC 에서는 입력 스트림에 fflush
함수를 호출하면 입력 버퍼에 남아있는 데이터를 버립니다.
하지만 이는 옛날 이야기이며, 최신 버전의 MSVC 에서는 이러한 기능을 제거하였고, 아무 효과가 없는 함수 호출이 되었습니다.
- MSVC 2013 버전의
fflush
: 입력 스트림의fflush
는 입력 버퍼의 내용을 지운다. - MSVC 2015 버전의
fflush
: 입력 스트림의fflush
는 아무 효과가 없다.
rewind
rewind(FILE *stream)
은 파일의 다음번에 읽거나 쓸 위치를 파일의 가장 처음으로 옮기는 함수입니다.
예를 들어, 입력 파일의 내용이 "1 2 3 4"
일 때, 다음 프로그램은 다음과 같이 동작합니다.
#include <stdio.h>
int main() {
FILE *fp = fopen("input.txt", "r");
int a, b, c, d;
fscanf(fp, "%d %d %d", &a, &b, &c); /* a = 1, b = 2, c = 3; 다음에 읽을 데이터 = 4 */
rewind(fp); /* 다음에 읽을 데이터 = 1 */
fscanf(fp, "%d", &d); /* d = 1 */
printf("%d %d %d %d\n", a, b, c, d);
return 0;
}
/*
파일 input.txt 의 내용:
1 2 3 4
출력 결과:
1 2 3 1
*/
BOJ 에서 채점할 때 stdin 은 redirection 기능으로 키보드가 아닌 파일과 연결되어 있습니다.
이때 rewind
함수를 호출하면 이미 읽었던 데이터를 다시 처음부터 읽기 시작합니다.
파일이 아닌 스트림에 rewind
를 호출할 경우 구현체에 따라 동작이 달라집니다.
MSVC 의 rewind 함수 설명 문서를 보면, 키보드 버퍼를 비우려면 키보드에 rewind
를 호출하라고 설명되어 있습니다. 반면 Linux 환경에서는 키보드와 같이 seekable 기능이 없는 스트림에 rewind
를 호출하면 아무도 안 보는 오류 코드를 설정합니다.
왜 이런 함수를 쓸까
대부분의 경우 scanf
함수의 %c
가 원하지 않는 공백문자를 입력받게 될 때, fflush(stdin)
또는 rewind(stdin)
의 유혹에 빠지게 됩니다.
#include <stdio.h>
int main() {
int n;
char c;
scanf("%d", &n); /* Line 7 */
for (int i = 0; i < n; i++) {
scanf("%c", &c); /* Line 9 */
printf("[%c]\n", c);
}
}
/*
입력 내용:
3
a
b
c
출력 결과:
[
]
[a]
[
]
*/
scanf 의 대부분의 형식 지시자 ("%d"
, "%s"
, "%f"
등등)는 데이터를 읽기 전에 공백문자가 있으면 공백문자를 읽은 후 버리고, 실제 데이터가 시작하는 부분부터 처리합니다. 반면, 데이터의 끝에 붙는 공백문제는 따로 처리하지 않고 입력 스트림에 남겨둡니다.
반면, "%c"
지시자는 공백문자도 다른 일반적인 글자와 똑같이 취급해서 저장합니다.
즉, 위 프로그램의 경우 7번째 줄의 scanf
는 3
만 처리해서 n에 저장하고, 입력 스트림에 개행문자를 남겨둡니다.
그러면 i = 0 루프의 9번째 줄에서 입력 스트림에 남겨둔 개행문자를 읽어 c에 저장합니다.
scanf
직후에 fgets
(아직도 를 호출하는 경우에도 이와 비슷한 증상이 나타날 수 있습니다.gets
함수 쓰시는 분은 없겠죠?)
#include <stdio.h>
int main() {
int data1;
char data2[256];
scanf("%d", &data1);
fgets(data2, 256, stdin);
printf("data1 = %d\ndata2 = %s", data1, data2);
}
/*
입력 내용:
42
The Universe
출력 결과:
data1 = 42
data2 =
*/
추천하는 해결법
공백 문자를 입력받는 건 싫고, 따로 처리하는 것은 너무 번거롭고, fflush
/ rewind
는 쓰지 말라고 하고, 그럼 어쩌라고?
필자가 가장 추천하는 방법은 scanf
형식 문자열에 공백을 포함하는 것입니다.
#include <stdio.h>
int main() {
int n;
char c;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf(" %c", &c); /* Line 9; %c 앞에 공백이 한 칸 들어간 것에 주목 */
printf("[%c]\n", c);
}
}
/*
입력 내용:
3
a
b
c
출력 결과:
[a]
[b]
[c]
*/
scanf
의 형식 문자열 " "
은 '공백문자가 아닌 글자가 나올 때 까지 읽고, 읽은 공백문자는 그냥 버려라' 라는 의미를 가지고 있습니다.
즉, 이 프로그램에서 a
를 읽기 전에 있을 수 있는 개행문자는 형식 문자열 " "
가 담당합니다.
scanf
직후에 fgets
를 사용하는 경우, fgets
로 받을 데이터가 공백으로 시작하지 않는다면 비슷한 방법을 사용할 수 있습니다.
#include <stdio.h>
int main() {
int data1;
char data2[256];
scanf("%d ", &data1);
fgets(data2, 256, stdin);
printf("data1 = %d\ndata2 = %s", data1, data2);
}
/*
입력 내용:
42
The Universe
출력 결과:
data1 = 42
data2 = The Universe
*/
단, fgets
로 받을 데이터가 공백으로 시작할 수도 있다면, 개행문자만 따로 입력받아 버릴 수 밖에 없습니다.
댓글 댓글 쓰기