쓰다보니 쓸 데 없이 길어졌네요..
바쁘신 분은 세줄요약만 보셔도 됩니다....
------------------------------
과제 때문인지 업무 때문인지는 모르겠지만,
C언어의 scanf 함수 사용에 대한 질문 글이 꾸준히 올라오네요. ^^;
사실, scanf 를 업무용 프로그램에서 쓰는 일은 '거의' 라고 쓰고 '네버'라고 읽음 없습니다.
왜냐 하면, 여러 질문에 나와 있는 '그 문제점들' 때문이죠.. ㅋㅋ.
그리고, scanf 함수의 실제 구현과 동작은 각 시스템(Unix/Linux/Windows...)별로 다르고,
컴파일러별로 다릅니다. C 표준에 정의된 사양만 그것에 부합하도록 같을 것이고, undefined behavior는 제조사마다 다를 겁니다.
아무튼, 저도 scanf 질문글이 올라올 때마다,
"뭐, 또, 엔터키 입력된 것 버퍼에서 안지워서 그렇겠지..."
요정도 생각하고 들어 옵니다. 그리고 실제로 예지력 상승을 확인하고 나가죠...
그러다 문득, 정말로 scanf 가 어떤 짓을 하도록 표준에 정의되어 있는 걸까...
왜 그따위로 만들어서 여러 착한 사람들을 못살게 굴까 궁금해지기 시작했습니다.
그래서, 일단, 소스코드를 좀 봐야겠다... 싶어서 찾아 봤죠.
물론 시스템별로 구현이 다르니, scanf 소스코드도 여러 버전이 있습니다.
좀 읽어 보려다가... 금세... 아몰랑.. 시전..
뭐 소스를 꼭 이해해야 하는 건 아니잖아, 어차피 시스템마다 구현도 다른 걸... 이라는 자기합리화... -_-;;
뭐, 아무튼, scanf 를 쓰면서 오는 혼란 중 가장 큰 것은,
scanf 가 input buffer 를 '비울 때도 있고 안비울 때도 있기' 때문이라고 봅니다.
게다가, scanf 의 표준에는 scanf 가 동작하다가 예외를 만났을 때의 상황을 일일이 정의하지 않고 있기 때문에,
Windows/Linux/Unix/Mac 등등에서 서로 다른 시스템과 컴파일러를 사용하면서
서로 묻고 답하는 와중에, 또다시 혼란이 생기고... 하여... 문제가 더 복잡해 지지 않았나... 마.. 그런 생각이 듭니다.
흔히 scanf 관련 질문 올라올 때는 아래와 같은 패턴이죠:
scanf("%d", &var1);
printf("%d\n", var1);
scanf("%d", &var2);
printf("%d\n", var2);
저기서, 첫번째 scanf에 입력을 했는데 왜 두 번째 scanf가 실행이 안되고 쭉 지나가 버리는가...하는 것이 가장 많지요?
거기에 대한 답은 여러분도 익히 아시다시피 첫번째 scanf 문에서 입력을 할 때,
마지막에 enter 를 넣은 것이 input buffer에 남아 있어서 그렇다. 그러니 아래와 같이 고쳐라..는 겁니다.
scanf("%d", &var1);
printf("%d\n", var1);
getchar();
scanf("%d", &var2);
printf("%d\n", var2);
요렇게 하면 해결이 된다! 맞습니다. 하지만, 예외처리가 완전히 된 것은 아닙니다.
만약, 첫 번째 scanf 함수에서 사용자가 숫자가 아닌 문자를 '여러 개' 입력하면,
또는 숫자와 문자를 합쳐서 11aaaa 와 같이 입력하면 또 다른 예외상황이 발생하게 됩니다.
그래서, 입력을 받고 나면 무조건 버퍼를 비워라!
라는 뜻으로 아래와 같이 하는 방법도 제시합니다.
scanf("%d", &var1);
printf("%d\n", var1);
scanf("%s", str);
scanf("%d", &var2);
printf("%d\n", var2);
요렇게 하면, 입력 버퍼가 비워지는 것은 맞습니다. 대부분의 경우 동작도 잘 될겁니다.
다만, 저기서 사용자가 아주 긴.. 문자열을 입력하게 되면, 즉, str 이라는 버퍼의 크기를
넘는 문자열을 입력하게 되면, 또 다시 예외상황이 발생하게 됩니다.
그래서 이번에는, scanf로 비우지 말고, 아래와 같이 비워라... 하는 방법도 있습니다.
scanf("%d", &var1);
printf("%d\n", var1);
fflush(stdin);
scanf("%d", &var2);
printf("%d\n", var2);
요렇게요. 근데, 문제가 있습니다.
fflush 라는 함수는 output buffer 에 있는 것을 모두 해당 stream으로 보내버리라는 것이지 (변기에서 물 내리는 것 처럼요)
그리고, scanf("%d", &var) 등으로 읽을 때,
어떤 시스템에서는 숫자 입력하고 난 후 'enter'를 누른 것을 input buffer에서 없애버리고,
어떤 시스템에서는 남겨두고.. 하니까 이게... 더 혼란이 오는 것 같습니다.
그리고, 이건 순전히 제 추측인데요.
Windows 와 Linux 에서 개행(New Line)을 처리하는 문자가 다릅니다. 아시죠?
Windows : 0D0A (Carriage-Return + Line-Feed)
Linux/Unix/Mac : 0A
요렇게 다른데요, Windows(또는 DOS)에서의 scanf는 0D는 읽어서 없애지만,
0A는 남겨두는 것 같습니다.
그러니, linux등에서 잘 되던 소스가 window에서 실행하면 이상하게 안되는 현상이 발생하기도 할 것이고요...
그런데, 실제로 VisualC/C++ 등에서 구현된 scanf 의 소스코드를 보지 않았기 때문에,
0D만 읽어서 없애고, 뒤따라오는 0A는 내버려 두는 일을 실제로 하는 지는 확인되지 않았습니다.
scanf에 대해서 또하나 아셔야 할 것은,
input stream에서 format에 지정된 타입의 값을 읽지 못하면 input stream buffer를 비우지 않는다는 것인데요,
사실 이렇게 말하는 것 보다는 아래와 같이 말하는 것이 더 정확할 겁니다.
"Input Stream에서 format에 지정된 type의 값을 읽게 되면 해당 값을 읽을 때까지의 버퍼는 비운다" 가 맞을 겁니다.
무슨 말인고 하면,
scanf("%d", &var); 로 사용자 입력을 읽을 때, 사용자가 입력을:
1) 1234
2) abcd
3) ab12
4) 12ab
의 네 가지 경우가 있다고 가정하면,
----var에 들어가는 값------버퍼에 남아있는 문자---
1) ==> 1234 ________________(없음)
2) ==> (없음)________________abcd
3) ==> (없음)________________ab12
4) ==> 12 ___________________ab
와 같이 된다는 겁니다.
위에서 %d 로(즉, 숫자) 읽으라고 했으니, scanf는 input buffer에서 숫자를 찾습니다.
1)의 경우에는 숫자가 찾아졌으므로 읽어서 변수에 할당하죠, 그리고 읽어낸 값을 inuput buffer에서 지웁니다.
2)의 경우에는 숫자가 없으므로 변수는 변동되지 않지만, abcd 라는 문자는 '여전히' input buffer에 남아 있습니다.
3)의 경우에도 2)와 다를 바 없습니다.
4)의 경우에는 먼저 들어 온 숫자 12를 읽고 그 다음 문자(ab)를 만나게 되어서 12만 값을 넣고, 버퍼에서 12를 지우고, ab는 남겨 놓습니다.
자, 이제 거의 다 왔네요 ^^;
scanf 의 함수 이름이, SCAN 이잖아요... getInput 이나 readInput 이 아니고...
그 말은, 뭔가를 scan....한다는 뜻이겠죠? 즉, input buffer를 scan 한다는 겁니다.
scanf는 input buffer의 내용을 scan하면서, 주어진 format에 지정된 data-type에 matching되는 값을 찾아서
주어진 변수에 할당을 하는 일을 하는 것이지, 단순히 키보드를 입력 받아서 그것을 실시간으로 변환하는 것이 아니기 때문에,
이름을 get...이나 read..가 아닌 scan으로 지은 것이 아닌가.. 합니다.
그럼 어떻게 scanf를 써야 하는가...?
우선, 입력을 받기 위해서 scanf를 쓸 때는,
실제로 scanf가 입력을 제대로 받았는지 확인하는 것이 가장 기본일 것 같습니다.
scanf는 int형의 return 값이 있는데, format에 지정된 대로 값을 읽은 결과, 성공적으로 읽어서 변수에 저장한 갯수를 리턴합니다.
ret = scanf("%d%f%c", &v1, &v2, &v3);
위의 경우, scanf를 통해서 성공적으로 읽어 들인 값이 1개인지, 2개 또는 3개인지에 따라,
ret의 값이 정해지게 됩니다. 물론, 하나도 못읽어들였다면 ret의 값은 0이 되겠죠.
따라서, scanf의 리턴값과 v1/v2/v3 등의 변수의 값을 조사해서,
적절한 예외 처리와 조치를 취하는 것이 좋을 것 같습니다.
그리고, 여러 번의 scanf 함수 호출로, 사용자로부터 입력을 받아야 할 경우에는,
input buffer에 남아 있는 white space나 개행문자, 또는 필요없이 남아 있는 문자들을 잘 제거해 주는 것이 필요하고요,
특히나, Windows/Dos 에서는 엔터키를 칠 때 0D0A 라는 두개의 캐릭터가 input stream buffer에 들어가기 때문에,
0A가 남아서 다음번 scanf 입력에서 원치않는 결과를 초래하는 것을 잘 방지해야 한다고 봅니다.
그래서,
10개의 정수를 입력받는 프로그램을 scanf를 써서 구현할 때,
가장 이식성이 좋게 만든다면,
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() { int i; int c; i = 0; do { printf("Input Number[%d]: ",i); switch(scanf("%d",&c)) { case 1: printf("Input: %d\n",c); i++; break; default: printf("No Integer Found\n"); printf("Check Your Input: >>>"); while((c = getchar()) != '\n' && c != '\r' && c != EOF) printf("%c",c); printf("<<<\n"); break; } } while(i<10); return 0; } | cs |
위와 같이, scanf의 리턴값을 조사해서 값을 제대로 읽어들이지 못했을 경우에는 input buffer를 비우도록 하는 것이 좋을 것 같습니다.
결론 (세줄 요약)
1. scanf를 여러 번 호출 할때, input buffer를 잘 지우자! (특히 Windows에서 0A 값이 남아서 문제가 자주 된다!)
2. scanf는 입력 버퍼를 'SCAN' 하고, 원하는 형식의 값이 있으면 버퍼를 비우지만, 아니면 버퍼를 비우지 않고 그대로 놔둔다!
3. scanf도 리턴 값이 있다. 리턴값을 잘 활용하자!
입니다... 그리고 참고로, fflush(stdin) 함수는 DOS에선 잘 동작하지만 유닉스 계열에선 동작하지 않는다는 것도요~
마지막!
scanf 함수는 실제 업무에서는 '전혀' 쓰이지 않습니다... 고로 지금까지 전 뻘짓한겁니다...ㅠㅠ