[CodeEater와 제로부터 시작하는 C언어] 5.1장 - scanf

주의!!
아래의 내용은 안배운 개념들을 많이 써야하고,
중요하긴 하지만 중요하지 않기 때문에?
그냥 띄어넘어도 상관없다. 정말로 몰라도 사는데 별 지장없다.
다만 조금 원리를 이해하고 쓰면 특별한 상황에서 편하기 때문에,
그리고 우리 갓갓 교수님이 어렵게 내는 경우도 있기 때문에,
부득이하게 설명을 하고 넘어가려고한다.

scanf라는 함수를 쓸 때 생각보다 문제가 자주 일어 날 것이다.
이 놈의 특징이라면 "너가 알아서 재주껏 조심해서 써라"라는 것이다.
그런데 당연히 입문자들 입장에서는 뭘 알아야 쓰지 않을 것인가?
그래서 필자도 좀 대책없다고 생각할 때가 많다.
사실 이걸 제대로 알 필요가 있을까 생각들 때도 있다.
어짜피... C안쓸거 잖아...
그러니까 그냥 잘 피해서 쓰면 되지 않나?
그런데 이렇게 말하는건 내가 대책없는거 같아서 좀 상세히 설명하려고 한다.
상세히 설명하려다 보니 안배운 개념들을 아주 많이 동원해야한다.
그런데 어쩌겠나.. 동원해야지...

우리가 알아야 될것은 문자(Character), 문자열(String), 포인터(Pointer), 주소(Address), 배열(Array), 버퍼(Buffer)이다.
이는 추후에 블로그에 언급이 될 녀석들이고 이를 알아야만 아래의 내용을 이해할 수 있다.
만약 모른다면?? 그냥 안보는걸 추천한다.
위에도 말했지만 몰라도 인생 사는데 지장없다.

scanf는 버퍼(Buffer)를 사용하는 함수이다.
이를 좀더 풀어서 설명하자면 입력을 바로 변수에 저장하는게 아니라
잠시 버퍼라는 배열(Array)에 저장한다. 그 후에 변수로 만들 때 적절히 빼내게 된다.
문제는 이 적절히라는 부분이다.
이 적절한것은 룰이 있지만 초보자들은 당연히 이 룰을 이해하지 못한다.
정확히 말하면 모른다는게 맞겠지.

아래는 서식지정자가 %c가 아닌, 즉 문자가 아닐 경우의 이야기이다.
일단 scanf는 동작시에는 버퍼가 차있는지 비어있는지 확인한다.
여기서 말하는 비어있다는 정말로 버퍼의 크기가 0인것 뿐만 아니라,
\n, \t, 공백등의 문자열도 비어있다고 가정한다.

일반적으로 맨처음에는 당연히 버퍼는 비어있다.
그렇기 때문에 버퍼를 채우는 작업을 제일 먼저한다.
버퍼를 채우는 방법은 무엇일까?
뭐 예상했겠지만 사용자의 입력이다.
사용자의 입력은 당연히 키보드로 받게된다.
사용자의 입력의 종료는 강제개행(\n: Line Feed)으로 판단한다.
여기서 강제개행은 키보드의 엔터를 의미한다.(넓은 의미에서 보면 조금 다르지만.)
즉 사용자가 입력하고 나서 엔터를 치게 되면 종료가 된다.
버퍼가 채워지고나면 이제 버퍼에서 필요한 부분만큼 적절히 자른다.
또... 적절하다는 이야기가 나왔는데 여튼 적절하다는 이유는 상황마다 조금 다르기 때문이다.
그래서 그 자른 부분을 다시 변수화 시킨다.
요약하면 아래와 같다.
1. 버퍼가 비었는지 확인한다. 차있다면 2번을 생략하고 3번으로 간다.
2. 버퍼가 비어있다면 사용자에게 입력을 받아 버퍼를 채운다.
3. 버퍼에서 자료형에 맞게 필요한 데이터까지만 적절히 빼서 변수화 시킨다.
그럼 이제 상황을 들어서 설명을 듣도록하자.
#include <stdio.h>
#pragma warning(disable:4996)
int main() {
int num;
scanf("%d", &num);
return 0;
}
이러한 코드는 아주 흔하다.
여기서 만약 100을 입력하는 상황이라고 가정해보자.

사실 버퍼는 사용자의 입력을 무조건적으로 문자(charachter)로 인식한다.
즉 위의 경우 숫자 100이 아니라, 문자 (1, 0, 0, \n)로 인식하게 된다.
마지막에 \n이 들어가는 이유는 우리가 마지막에 \n(엔터)을 입력했기 때문이다.
일단 버퍼에 입력되어있으니 num을 숫자로 채워야한다.
그러면 과연 어디까지 숫자인지 판단해서 그 숫자만큼 채워넣게 된다.
위의 경우 1,0,0까지는 숫자이므로 이 까지 버퍼에서 빠진다.
그런데 4번째의 경우 강제개행문자이므로 이 녀석을 버퍼에서 빼지는 않고
앞에 100까지만 잘라서 가져간다.
그래서 최종적으로 num에는 100이 저장되게 된다.
#include <stdio.h>
#pragma warning(disable:4996)
int main() {
int num1;
int num2;
scanf("%d", &num1);
scanf("%d", &num2);
printf("%d %d", num1, num2);
return 0;
}
그러면 위같은 상황은 어떻게 될까?
이 경우 일반적으로 문제없이 동작하는데 그 이유는 진행과정을 보면 알 수 있다.

1. 버퍼가 비어있으므로 사용자의 입력을 받는다. [1,0,0,\n]
2. 그 다음 숫자부분까지만 버퍼에서 빼내서 변수로 만든다 [\n]
3. 다시 scanf를 사용했기 때문에 버퍼가 비어있는지 확인한다.
일반적으로는 비어있지 않지만 공백 문자들은 비어있다고 가정하므로 버퍼에서 빼낸뒤 []
-> 다시 숫자를 채운다 [3,0,0,\n]
4. 그 다음 숫자부분까지만 버퍼에서 빼내서 변수로 만든다 [\n]
이는 정수 뿐만이 실수와 문자열도 동일하다.
#include <stdio.h>
#pragma warning(disable:4996)
int main() {
char str1[100];
char str2[100];
scanf("%s", str1);
scanf("%s", str2);
printf("%s %s", str1, str2);
return 0;
}
이 경우에도 정수와 동일하게 동작하는걸 확인할 수 있다.
문자열을 받을 때는 별 문제가 없다.
왜냐하면 문자열은 받을 수 있는 값의 제한이 없기 때문이다.
어짜피 사용자가 받은 입력을 고스란이 저장하기만 하면 되니까.
문제는 정수와 실수를 입력받을 때이다.
정수를 넣어야하는데 실수를 넣거나
실수를 넣어야하는데 문자열을 넣는 상황이 벌어졌을 때 실제로 어떻게 되느냐일 것이다.
이러한 문제는 꽤 심각한데 그 이유는 아래와 같다.

#include <stdio.h>
#pragma warning(disable:4996)
int main() {
int num1;
int num2;
printf("입력하세요 : ");
scanf("%d", &num1);
printf("입력한 값은 : %d\n", num1);
printf("입력하세요 : ");
scanf("%d", &num2);
printf("입력한 값은 : %d\n", num);
return 0;
}
이 예제는 입력받고 출력, 입력받고 출력하는 예제이다.
num1과 num2는 모두 정수를 입력받게 되어있는데
만약 여기에 실수를 입력한다면 어떠한 일이 벌어질까?

일단 세가지 문제점이 있는데
첫번째로는 3.14가 아니라 3이 변수에 입력되었다는거,
두번째로는 두번째 scanf는 동작하지 않았다는거,
세번째로는 num2는 초기화 되지 않았다는 점이다.
복기를 해보자.

문제점은 .은 정수로 전환할 수 없다는 점이다.
이제 적절하다는 것의 비밀이 풀렸을 것이다.
정수는 흔히 우리가 아는 정수 형태(ex: 10, +5, 0, -1),
실수는 흔히 우리가 아는 실수 형태(ex: .5, 1.5, -3, -3.14)
만을 받을 수 있다는 점이다.
만약 받을 수 없는 형태가 온다면??
그 값에서 종료해 버린다.
이제 문제가 왜 일어나는지 파악했을 것이다.
그래서 scanf는 항상 자신에게 맞는 타입을 받아야만 한다.
또한 항상 scanf를 동작시키려면 scanf를 동작시킨다음에 반드시 버퍼를 비워준다.
근데 문제는 버퍼를 어떻게 비우냐이다.
그 방법 역시 알려주겠다.

문자를 받는 상황은 특이한 상황인데 왜냐하면 문자는 못받는 타입이 없기 때문이다.
어짜피 입력 그자체를 저장한다는 특성 때문에
문자를 받는 상황은 항상 값을 받을 수 있다는 기대가 있다.
가령 아래의 예제를 보자.
#include <stdio.h>
#pragma warning(disable:4996)
int main() {
char c1, c2, c3;
printf("값을 입력 : ");
scanf("%c", &c1);
printf("값을 입력 : ");
scanf("%c", &c2);
printf("값을 입력 : ");
scanf("%c", &c3);
printf("입력한 값은 : %c %c %c\n", c1, c2, c3);
return 0;
}
이 예제는 마치 문자를 3개 받는 예제처럼 보인다.
실제로 이런 의도로 작성하는 사람이 많을 것이다.
그런데 실제로 이러한 코드는 생각한것 처럼 동작하지는 않을 것이다.
가령 입력으로 a,b,c를 받고 출력하는 상황을 가정하자.

하지만 생각처럼 잘 안되지않아?
그 이유는 버퍼 상황을 보도록하자.

쉽게 이야기해서 c1을 입력할때는 버퍼가 비어있어서 사용자에게 입력을 받았다.
그리고 a를 빼내서 c1의 값을 초기화 한다.
그 후 두번째 scanf를 호출 했으나 공백문자가 아직 들어있다.
그래서 사용자에게 입력을 받지 못한다.
이게 정수, 실수, 문자열에서는 "비어 있다고 판단하지만"
문자는 공백문자도 "비어 있다고 판단하지 않는다"
그러다보니 c1은 a, c2는 \n, c3는 b가 들어가게 된다.
그러면 원래대로 우리가 의도한대로 할려면 어떻게 해야할까?
이를 역이용하면된다.
#include <stdio.h>
#pragma warning(disable:4996)
int main() {
char c1, c2, c3, tmp;
printf("값을 입력 : ");
scanf("%c", &c1);
scanf("%c", &tmp);
printf("값을 입력 : ");
scanf("%c", &c2);
scanf("%c", &tmp);
printf("값을 입력 : ");
scanf("%c", &c3);
scanf("%c", &tmp);
printf("입력한 값은 : %c %c %c\n", c1, c2, c3);
return 0;
}
바로 맨 끝에 받는 공백 문자를 강제로 뽑아버리는 방법이다.
tmp라는 변수를 한번 더 호출해서 \n을 호출하는 방법인데 효과가 발군이다.
다만 이 방법은 모든 상황에서 완벽하지는 않다는 점을 알아뒀으면 한다.

이 방법으로 잘못 들어온 값을 지워버리는것 역시 가능하다.
힌트는 항상 마지막 값에는 공백문자(\n, \t, 스페이스 등)이 들어 있다는 점이다.
이를 예제로 담고 싶지만 이걸 알려면 반복문을 알아야하기 때문에...
부득이 하게 숙제로 남겨두겠다.
'제로부터 시작하는 프로그래밍 > C' 카테고리의 다른 글
[CodeEater와 제로부터 시작하는 C언어] 6.1장 - 자료형끼리의 연산 (0) | 2020.04.13 |
---|---|
[CodeEater와 제로부터 시작하는 C언어] 6장 - 연산자 (0) | 2019.12.08 |
[CodeEater와 제로부터 시작하는 C언어] 5장 - 입력과 출력 (0) | 2019.12.01 |
[CodeEater와 제로부터 시작하는 C언어] 4.2장 - 서식 지정자 (0) | 2019.11.30 |
[CodeEater와 제로부터 시작하는 C언어] 4.1장 - 변수는 어떻게 표현되는가 (0) | 2019.11.23 |