모두의 코드
씹어먹는 C 언어 - <15 - 2. 일로와봐, 문자열(string) - 버퍼에 관한 이해>

작성일 : 2010-01-25 이 글은 77009 번 읽혔습니다.

이번 강좌에서는

  • 버퍼(stdin)에 대한 이해

  • 고질적인 scanf 문제에 대한 해결 및 이해

씹어먹는 C 언어

안녕하세요 여러분. 요즘에 제가 많이 바빠서 글을 자주 못올리고 있지만 여러분은 너그러운 마음으로 이해해 주시라 믿고 있습니다. 그렇다면, 지난번의 강좌를 계속 이어 나가도록 하겠습니다.

/* 이상한 scanf */
#include <stdio.h>
int main() {
  int num;
  char c;

  printf("숫자를 입력하세요 : ");
  scanf("%d", &num);

  printf("문자를 입력하세요 : ");
  scanf("%c", &c);
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

숫자를 입력하세요 : 1
문자를 입력하세요 : 

허거걱! 여러분은 위 소스를 실행했을 때 충격을 금치 못했을 것입니다. 분명히 우리는 다음과 같이 scanf 를 이용해서 정수를 입력 받고 그 다음에 문자를 입력받으라고 명시했습니다.

printf("숫자를 입력하세요 : ");
scanf("%d", &num);

printf("문자를 입력하세요 : ");
scanf("%c", &c);

분명히 우리는 컴퓨터로 하여금 숫자를 입력받은 후, "문자를 입력하세요 : " 를 출력한 다음, c 에 문자를 입력받으라고 명령하였습니다. 그리고, 컴퓨터가 정말 특별히 우리를 싫어하지 않는 한 무조건 우리의 말을 따라아 하는 것이지요. 그런데, 이게 무슨 일입니까? 우리의 컴퓨터는 우리가 친절히 명시해 준 scanf("%c", &c); 명령을 완전히 무시한 것 아닙니까? 이게 도대체 무슨 일이죠?? 사실, 우리가 컴퓨터에게 화풀이를 하기 전에 scanf 함수가 어떻게 작동하는 것인지 먼저 확인해볼 필요가 있습니다.

우리가 컴퓨터에 무언가를 입력하면 컴퓨터는 어떻게 처리를 할까요? 예를 들어서 우리가 컴퓨터에게 abcde 를 입력하였을 때, 컴퓨터가 각 문자를 입력받을 때 마다 처리를 한다면 (즉 우리가 a 를 누르는 순간 a 라는 문자를 변수에 저장하고 등등 작업을 하고 그 다음에 b 가 들어오면 다시 이 문자를 ...) 비효율적일 것입니다.

하지만 이렇게 하면 어떨까요. 우리가 문자를 입력한다면 다른 곳에 잠시 보관해 놓았다가 우리의 입력이 끝난다면 잠깐 보관해 놓았던 곳의 정보를 한꺼번에 처리하는 것입니다. 따라서, 만일 우리가 abcde 를 입력하였다면 abcde 를 잠시 다른 곳에 보관해놓았다가 입력이 끝난다면 이를 한꺼번에 처리하는 것입니다.

사실, 이 두 방법이 어떤 차이가 있을 수 있느냐 라고 물을 수 있지만 아래 비유를 보면 쉽게 이해가 될 것입니다. 우리가 만일 약수터에 가서 물을 떠온다고 해봅시다. 물을 3 L 받아 온다고 했을 때 우리는 물을 두 가지 방법으로 받아올 수 있습니다. 하나는 손에 물을 받아서 약수터까지 수십번 왔다 갔다 하는 것이고, 다른 방법은 양동이를 들고가서 3 L를 채운 후, 다시 양동이를 가지고 내려오는 것입니다.

자. 그럼 어떤 방법이 합리적인것 같습니까? 손으로 조금씩 조금씩 받아서 수십번 왔다갔다 하는 것이 나을 까요? 아니면 양동이를 들고 한 번만 갔다오는 것이 나을까요? 당연히, 후자가 훨씬 좋은 방법이겠지요. 컴퓨터도, 원리는 조금 더 복잡하지만 이러한 방법을 채택하고 있는 것입니다.

그렇다면 컴퓨터의 양동이에 해당하는 부분은 무엇일까요? 바로, 버퍼(buffer) 라고 부르는 것입니다. 또한, 수 많은 버퍼 중에서도 키보드의 입력을 처리하는 버퍼는 바로 입력 버퍼, 혹은 stdin (흔히 입력 스트림) 이라 부르는 것입니다.

다시말해 우리가 키보드로 쳐다는 모든 정보는 일시적으로 stdin 에 저장되었다가 나중에 입력이 종료되면 한꺼번에 처리를 하는 것입니다. 그런데, 컴퓨터가 어떻게 우리가 입력을 종료했는지 알 수 있죠? 바로 엔터를 치면 됩니다. 왜냐하면 이전에도 우리가 계속 보았듯이 엔터를 치기만 하면 입력을 끝내고 프로그램이 계속 실행되었잖아요. 안그래요?

다시말해 컴퓨터는 개행 문자, 즉 \n 을 '입력을 종료하였으니 버퍼에 들어 있는 내용을 가지고 놀아라' 라는 뜻으로 받아 들입니다.

그런데 컴퓨터는 \n 까지 버퍼에 저장하게 됩니다. 즉, 우리가 1 을 쓰고 엔터를 딱 치면 버퍼에 아래와 같은 상태가 됩니다.

자. 그럼 입력을 끝냈다면 컴퓨터는 scanf 함수를 이용해서 stdin 으로 부터 숫자를 얻어옵니다. 왜 숫자냐면, 잘 아겠지만 우리가

scanf("%d", &num);

로 하였기 때문이죠. 즉 오직 숫자 데이터만 stdin 에서 얻어온다는 말입니다. 그렇다면 scanf 함수는 언제까지 stdin 으로 부터 데이터를 얻어올까요? 바로 ' ', '\n', '\t' 를 만날 때 까지 입니다. 여기서 ' ' 는 띄어쓰기 한 칸 (키보드의 Space) 을 의미합니다. 또한 \t 는 탭 문자로 여러분이 키보드에 Tab 키를 누를 때 나오는 문자 입니다.

또한 \n 은 이전에 이야기 했던대로 개행문자 (키보드의 Enter) 입니다. 다시 말해 scanf 함수는 stdin 에서 위 세 개의 문자들을 만난다면 아. 여기서 입력은 끝이구나 하고 입력을 종료해 버립니다.

참고적으로 %d 계열의 것들, 즉 수를 입력받는 형식은 수가 아닌 데이터가 와도 입력을 종료해 버립니다. 즉, a 를 입력했다면 num 에는 아무런 값이 들어가지 않아 치명적인 결과를 야기할 수 있습니다. 뿐만 아니라 수 데이터를 입력받는 형식의 경우 처음 부터 공백문자가 나타나면 수가 나타날 때 까지 입력을 계속 받게 됩니다. (다시 말해, 수를 입력 받는데 엔터를 아무리 쳐도 숫자를 치기 전까지 넘어가지 않는다) 암튼 scanf 함수는 공백 문자(' ', '\n', '\t') 를 만나기 전까지 stdin 에서 데이터를 가져간 후 버퍼에서 삭제해 버립니다. 다시말해, 위 scanf 함수가 num 에 1 을 저장한 후 버퍼의 모습은 아래와 같습니다.

자, 이제. 우리의 말을 아주 잘 듣는 컴퓨터는

scanf("%c", &c);

를 실행하게 됩니다. 그런데 말이죠, %c 는 이유를 불문하고 stdin 에서 딱 한개의 문자만을 가져오게 됩니다. 만일 stdin 에 아무것도 없다면 사용자의 입력을 기다리고 있겠지만 stdin 에 무언가가 있다면 그 것을 냉큼 가져오게 되지요. 그런데 공교롭게도 위에서 \n 을 버퍼에 남겨 놓았기 때문에 scanf 는 냉큼 이를 c 에 저장하게 됩니다. 즉, c 에는 사용자의 입력을 받지도 않고 \n 을 집어 넣은 것이지요.

따라서 만일 우리가 printf("%c 출력", c); 를 해보게 된다면 '출력'이 한 칸 개행(엔터가 쳐져서)되어 나타나게 됩니다.

%s 로 scanf 에서 받을 경우

/* 그렇다면 %s 는 ? */
#include <stdio.h>
int main() {
  char str[30];
  int i;

  scanf("%d", &i);
  scanf("%s", str);

  printf("str : %s", str);

  return 0;
}

성공적으로 컴파일 한다면

실행 결과

1
asdfasfasdf
str : asdfasfasdf

오오.. 이번에는 다행입니다. %c 와는 달리 %s 의 경우 컴퓨터가 사용자로 부터 입력을 잘 받았습니다. 사실, 그 이유는 간단합니다. 일단,

scanf("%d", &i);

를 실행하여 사용자로 부터 수를 입력 받게 된다면 역시 stdin 에는 \n 이 남아있게 됩니다. 그리고

scanf("%s", str);

를 실행하게 되면 역시 수 데이터를 입력 받는 형식 처럼실질 적인 데이터 (공백 문자가 아닌 것들) 이 나오기 전 까지 버퍼에 남아 있던 공백 문자들은 무시하고 실질 적인 문자(공백 문자가 아닌 것들)가 입력이 된다면 그 다음 부터 등장하게 되는 공백 문자에서는 종료하게 됩니다. 즉, 기존에 1 을 입력하였을 때 남아있었던 \n 은 사라지고, 내가 aasdfdasfads 를 입력하고 난 후, 엔터를 쳤을 때 들어가는 \n 을 인식 하게 된다는 것이지요.

결론적으로 요약하자면 %s%d 그리고 다른 모든 수 데이터를 입력 받는 형식은 버퍼에 남아 있는 공백 문자에 신경쓰지 않고 사용할 수 있습니다.

하지만 %c 를 이용할 때 에는 버퍼에 무엇이 남아 있는지 잘 고려해야 합니다. 이는 정말로 번거로운 일이 아닐 수 없습니다. 물론, 이를 대체할 수 있는 멋진 대안이 있지만 이는 조금 있다가 고려하도록 하고 다음 예제를 살펴 보도록 합시다.

/* 마지막 stdin 예제 */
#include <stdio.h>
int main() {
  char str1[10], str2[10];

  printf("문자열을 입력하세요 : ");
  scanf("%s", str1);
  printf("입력한 문자열 : %s \n", str1);

  printf("문자열을 입력하세요 : ");
  scanf("%s", str2);
  printf("입력한 문자열 : %s \n", str2);

  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

문자열을 입력하세요 : hello
입력한 문자열 : hello 
문자열을 입력하세요 : baby
입력한 문자열 : baby 

상당히 평범한 내용입니다. 여태까지의 강좌를 잘 따라오고 있었다면 위 내용쯤이야 쉽게 이해할 수 있을 것입니다.

그렇다면 다음과 같이 입력 해보도록 하겠습니다.

실행 결과

문자열을 입력하세요 : hello baby
입력한 문자열 : hello 
문자열을 입력하세요 : 입력한 문자열 : baby 

헉.. 이번에는 우리의 두번째 scanf 를 완전히 무시하고 지나갔습니다. 하지만 똑똑한 여러분이라면 왜 두번째 scanf 에서 사용자로 부터 입력을 받지 않았고, str1 에는 hello, str2 에는 baby 가 제대로 들어갔는지도 알 수 있을 것입니다. 우리가 "hello baby" 를 입력하였을 때 stdin 의 상태를 살펴봅시다.

그렇다면

scanf("%s", str1);

scanf 함수는 stdin 으로 부터 의미가 있는 문자 (공백 문자(' ', '\n', '\t') 를 제외한 나머지 문자) 가 나올 때 까지 모든 공백 문자들을 무시합니다. 위의 경우 stdin 에서 처음에 공백 문자가 하나도 없으므로 바로 stdin 으로 부터 데이터를 가져오겠군요. 데이터를 가져오다가 공백 문자를 만나게 되면 입력을 중지합니다. 위의 경우 ' ' 이 공백 문자의 역할을 하기 때문에 str1 에는 hello 까지만 입력이 됩니다.

첫번째 scanf 함수를 지나게 되면 stdin 의 모습은 아래와 같습니다.

이제 두번째 scanf 를 지나갈 차례입니다.

scanf("%s", str2);

scanf 함수는 stdin 에 아무 것도 없거나, 공백 문자들 밖에 없다면 사용자가 무언가 의미 있는 문자를 입력해줄 때 까지 기다리겠지만 위 경우는 상황이 다릅니다. 일단, 처음에 공백 문자인 띄어쓰기는 살포시 무시합니다. 왜냐하면 아직 의미 있는 문자를 받지 않았기 때문이죠. 그 다음에 b 를 보고 str2 에 입력을 쭉 받기 시작합니다. 그러다가 마지막에 공백 문자인 \n 을 보고 입력을 중지합니다. 따라서 메모리에는 다음과 같이 \n 만이 덩그러니 남아있게 됩니다.

아무튼. scanf 는 상당히 이해하기 복잡한 것임은 틀림이 없습니다. 가뜩이나 머리 아픈데 %c 를 이용하면 고려해야 될 것이 더욱 많아지니 정말 짜증이 나는 것 같습니다. 하지만 다행스럽게도 이러한 문제를 해결할 수 있는 방법이 있을 뿐더러 실질 적으로 %c 는 많이 쓰이지 않으니 다행인 것 같습니다.

도대체 이 문제를 어떻게 해결하냐

하지만, 아무리 %c 를 사용하지 않는다고 해도 필연적으로 사용할 일이 생기게 됩니다. 그렇다면 그 때 마다 이처럼 버퍼에 \n 이 남아 있는 것을 고려해야 할까요? 정말 번거로운 일이 아닐 수 없습니다. 하지만 걱정 마십시오. 이를 해결할 수 있는 방법이 여러 가지가 있습니다.

/*

버퍼 비우기

주의하실 점은 반드시 MS 계열의 컴파일러로 컴파일 해주세요. 즉, Visual Studio
계열의 컴파일러로 말이죠. 이 말이 무슨 말인지 모르면 그냥 늘 하던대로 하면
됩니다.

gcc 에서는 정상적으로 작동되지 않는 위험한 코드 입니다.

*/
#include <stdio.h>
int main() {
  int num;
  char c;

  printf("숫자를 입력하세요 : ");
  scanf("%d", &num);

  fflush(stdin);

  printf("문자를 입력하세요 : ");
  scanf("%c", &c);
  return 0;
}

성공적으로 컴파일 했다면

아마도 여러분은 컴파일 하면서 포스 넘치는 주석을 보며 무언가 당황 하셨을 수 도 있습니다. 하지만 걱정하진 마세요. 지금 수준의 프로그래밍에서는 크게 걱정할 문제는 아닙니다. 먼저 위 소스가 어떻게 해서 올바르게 작동하는지 부터 살펴보도록 합시다. 사실, 올바르게 라는 말 보다는 'scanf 가 사용자의 입력을 무시하지 않는지' 가 적당할 듯 하네요.

printf("숫자를 입력하세요 : ");
scanf("%d", &num);

위까지 실행했을 때 에는 이전처럼 stdin 에 '\n' 이 남아 있습니다. 그런데 말이죠.

fflush(stdin);

두둥. 새로운 문장이 등장했습니다. 위 문장의 의미는 'stdin 을 비워버려라' 라는 의미이죠. 다시말해 stdin 에 있는 모든 데이터들을 날려버리게 되는 것입니다. 따라서 버퍼가 완전히 비워지게 됩니다. 즉 버퍼에 가시 처럼 남아 있던 \n 이 사라지게 됩니다.

scanf("%c", &c);

따라서 그러한 상태에서 scanf 를 호출하게 되면 %c 는 버퍼에 아무것도 남아 있는 것이 없으므로 사용자의 입력을 차분히 기다리고 있게 됩니다. 즉, 우리가 c 에 원하는 값을 넣을 수 있다는 뜻이 되죠. 하지만 프로그램 코드 상단에 있는 무서운 주석을 보면 알겠지만 사실 위 코드는 추천하고 싶지 않습니다. 왜냐하면 fflush 가 표준으로 '무슨 역할은 한다' 라고 정해진 것이 아니기 때문입니다.

다시 말해 우리의 Visual Studio 에선 fflush 함수가 버퍼를 비우는 훌륭한 역할을 하지만 다른 것 - 예를 들면 gcc 같은 데에서는 이러한 작업을 하지 않을 가능성이 매우 매우 큽니다. 다시 말해, 위 방법은 그다지 권장하고 싶은 방법은 아니지만 적어도 우리의 수준에서는 정확하게 작동하고 편리하기 때문에 많이 사용합니다.

/* getchar 함수 이용 */
#include <stdio.h>
int main() {
  int num;
  char c;

  printf("숫자를 입력하세요 : ");
  scanf("%d", &num);

  getchar();

  printf("문자를 입력하세요 : ");
  scanf("%c", &c);

  return 0;
}

성공적으로 컴파일 했다면

실행 결과

숫자를 입력하세요 : 1
문자를 입력하세요 : c

오. 이번에도 제대로 작동하고 있습니다.

printf("숫자를 입력하세요 : ");
scanf("%d", &num);

일단, 위 부분까지만 실행하면 역시 stdin 에는 \n 이 남아있게 됩니다. 상당히 곤란한 일이죠.

getchar();

그렇데 말이죠, 우리가 getchar 이라는 함수를 호출했습니다. 이 함수의 역할은 'stdin 에서 한 문자를 읽어와서 그 값을 리턴한다' 입니다. 물론 한 문자를 읽어오면 읽어온 문자는 stdin 에서 사라지게 되지요. 따라서 위 함수를 호출 함으로써 \n 을 stdin 에서 읽어와 지워버릴 수 있는 것이지요.

만일 우리가

ch = getchar();
prinf("%c", ch);

을 해서 getchar 함수가 리턴한 값을 출력해보았다면 화면상에 한 칸 엔터(== \n)가 쳐진 것이 출력될 것입니다. (여러분이 한 번 해보세요~) 이제, 버퍼가 비워진 상태에서 scanf 함수를 호출하게 되면 성공적으로 사용자의 입력을 받게 되는 것입니다. 상당히 단순하지요?

getchar 함수를 호출한 방법은 여러 모로 많이 쓰이는 방법 입니다. 기본적으로 scanf 에서 %c 형식을 사용하는 것을 권하고 싶지는 않지만 정 사용하고자 한다면 getchar()scanf 이전에 호출해서 버퍼를 비워주기 바랍니다. 그런데 말이지요. 위 방법도 문제가 어지간히 있습니다. 만일 버퍼에 한 문자만 남겨져 있는 것이 아니면 어떡할까요? 한 번 숫자를 입력할 때 123abc 를 쳐 보았습니다.

/* c 에 무엇이 들어가는지 살짝 보아야 하므로 코드를 약간 수정했습니다 */
#include <stdio.h>
int main() {
  int num, i;
  char c;

  printf("숫자를 입력하세요 : ");
  scanf("%d", &num);

  getchar();

  printf("문자를 입력하세요 : ");
  scanf("%c", &c);

  printf("입력한 문자 : %c", c);
  return 0;
}

성공적으로 컴파일 했다면

실행 결과

숫자를 입력하세요 : 123abc
문자를 입력하세요 : 입력한 문자 : b

아.. 역시 제가 우려했던 대로 scanf 에서 사용자의 입력을 기다리지 않고 지나쳐 버렸습니다. 뿐만 아니라 c 에도 우리가 원하지 않던 b 라는 값이 들어가 있습니다. 도대체 왜 이런 일이 발생한 것일까요? 일단 버퍼에서 무슨 일이 벌어지고 있는지 차근 차근 살펴보도록 합시다.

printf("숫자를 입력하세요 : ");
scanf("%d", &num);

일단 위 코드가 실행되어서 사용자로 부터 입력을 기다립니다. 사악한 Psi 는 123abc 를 쳤습니다. 그렇다면 버퍼에 다음과 같이 들어가겠지요.

이제, scanf 함수가 stdin 에서 차례 차례 데이타를 읽어옵니다. 그 때도 말했들이 데이타를 읽어올 때 공백문자나 숫자가 아닌 것들을 만나게 되면 stdin 에서 부터 그만 읽어온다고 했죠? 이 때 a 가 숫자가 아니기 때문에 123 까지 읽은 후 stdin 에서 부터 그만 읽어 옵니다. 따라서 stdin 은 다음과 같은 모습이 되겠군요.

그렇다면 아래의 문장이 실행됩니다.

getchar();

이는 이전의 문제점을 말끔히 해결해 주었죠. stdin 으로 부터 한 문자를 얻어오는 방법으로 말이지요. 여기서도 getchar 은 똑같은 역할을 수행합니다. 즉 stdin 으로 부터 한 문자, 위 경우 a 를 읽어옵니다.

아 이런. 버퍼가 깔끔하게 비워지지 않았습니다. 이러한 우려 속에서 아래의 코드가 실행됩니다.

printf("문자를 입력하세요 : ");
scanf("%c", &c);

음.. scanf 의 입장에서 버퍼에 읽어올 것들이 잔뜩 있으니 행복할 것 같습니다. 버퍼에서 한 문자를 읽어 옵니다. 그것이 바로 b 가 됩니다. 따라서 c 에는 우리가 원하지 않던 b 가 들어가게 됩니다. 그리고 물론 bstdin 에서 사라지게 되죠. 다음에 또 scanf("%c", &c"); 를 하게 되면 이번에는 c 가, 한 번 더하면 \n 이 읽어지겠죠?

아무튼. 여기서 내릴 수 있는 결론은 "되도록이면 %c 를 사용하지 말자" 입니다. scanf 에서 %c 를 사용하는 것은 정말로 권장하고 싶지 않은 일입니다. 만일 정말로 문자 하나만을 입력받는 프로그램을 만드려면 scanf 에서 %s 형태로 문자열을 입력 받은 뒤에 맨 앞의 한 문자만 취하는 식으로 만들면 되겠습니다.

결론 : 문자 대신 문자열을 입력 받도록 하자!

생각해보기

문제 1

키보드로 부터 입력을 받는 함수는 scanfgetchar 말고도 여러가지가 있습니다. 이들에 대해 조사해 보는 것이 어떨까요? (난이도 : 無 - 쉬운 것도 아니고 어려운 것도 아님)

문제 2

화면에 출력하는 함수도 printf 만 있는 것이 아닙니다. 화면에 출력하는 함수에 대해서 알아보는 것이 어떨까요?(난이도 : 無)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <씹어먹는 C 언어 - <15 - 2. 일로와봐, 문자열(string) - 버퍼에 관한 이해>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 112 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요

    댓글을 불러오는 중입니다..