scanf는 printf의 거울입니다. 같은 헤더, 비슷해 보이는 형식 문자열, 반대 방향이죠. 표준 입력에서 텍스트를 읽고, 형식에 따라 변환하고, 여러분이 준 포인터를 통해 결과를 저장합니다.
동시에 C의 어떤 함수보다 헷갈리는 입문자를 많이 만들어 내는 함수이기도 합니다. 동작 셋이 놀랍고 그중 하나는 보안 구멍이기 때문입니다. 이 페이지는 넷 다 다룹니다.
기본 모양, 그리고 &인 이유
숫자를 입력하고 엔터를 눌러 실행해 보세요. &age는 장식이 아닙니다. C는 모든 인자를 값으로 전달하므로, scanf("%d", age)라고 썼다면 함수는 age의 현재 (쓰레기) 값의 사본을 받고 변수 자체에는 닿을 방법이 없습니다. &age는 변수의 주소를 넘기고, scanf가 그리로 씁니다. 전체 그림은 포인터를 보세요.
&를 빠뜨리는 것이 가장 흔한 scanf 실수입니다. -Wall을 켠 컴파일러가 잡아 줍니다(format '%d' expects argument of type 'int *'). 경고를 켜지 않으면 컴파일되고 어딘가 임의의 주소에서 메모리를 망가뜨립니다.
여러 값 읽기
한 번의 호출로 여러 값을 읽을 수 있습니다. 입력의 공백 - 스페이스, 탭, 줄바꿈 - 이 그것들을 구분하고, 숫자 지정자는 그것을 얼마든지 건너뜁니다.
"%d %d %d"의 실제 공백들은 사실 없어도 됩니다. %d가 이미 앞쪽 공백을 건너뛰니까요. 하지만 형식을 읽기 좋게 만듭니다. 없어도 되지 않는 것은 아래에서 다룰 반환값 확인입니다.
형식 안의 공백 아닌 문자는 입력과 정확히 일치해야 하고, 그것이 구조화된 입력을 읽는 방법입니다.
형식의 :는 입력에 :을 요구합니다. 9:30을 입력하면 파싱되고, 9 30을 입력하면 9를 읽은 뒤 실패합니다.
반환값은 항상 확인하세요
scanf는 성공적으로 대입된 항목의 개수를 반환합니다. 문자 수도 아니고 성공 플래그도 아닙니다. 아무것도 변환되기 전에 입력이 끝나면 EOF를 반환합니다.
프롬프트에 hello를 입력해 보세요. scanf가 0을 반환하고, n은 손대지 않은 채 처음의 쓰레기 값을 그대로 담고 있으며, 문제의 텍스트는 여전히 입력 스트림에 앉아 있습니다. 그 마지막 부분이 잘못된 입력 하나를 무한 반복으로 바꿉니다.
/* 버그: 숫자가 아닌 입력에서 영원히 돈다 */
while (scanf("%d", &n) != 1) {
printf("다시 시도: ");
}
scanf는 변환하지 못한 텍스트를 절대 소비하지 않으므로, 다음 호출이 같은 문자들에서 영원히 실패합니다. 회복하려면 줄의 나머지를 직접 버려야 합니다.
EOF 확인은 중요합니다. 입력 스트림이 끝나면(사용자가 Ctrl+D를 누르거나 파이프로 넘긴 파일이 다 떨어지면) scanf는 그 뒤로 계속 EOF를 돌려주므로, != 1만 검사하는 루프는 영원히 재시도합니다. "잘못된 입력"과 "더 이상 입력이 없음"은 서로 다른 두 경우로 다루세요. 그렇게만 해두면 이 discard_line 헬퍼는 사람의 입력에 scanf를 쓰는 어떤 프로그램에서도 곁에 둘 만합니다.
%s 버퍼 오버플로
%s는 문자들을 char 배열로 읽습니다. &가 없다는 점을 보세요. 배열 이름은 이미 첫 원소를 가리키는 포인터로 붕괴하므로, &name은 잘못된 타입이 됩니다.
문제는 그냥 %s가 배열이 얼마나 큰지 전혀 모른다는 것입니다.
char name[10];
scanf("%s", name); /* 위험: 40글자를 입력하면 41바이트를 쓴다 */
배열 끝에서 쓰기를 멈추는 것은 없습니다. 메모리에서 그다음에 오는 것 - 다른 변수들, 반환 주소 - 으로 쭉 밀고 들어가는데, 이것이 고전적인 버퍼 오버플로이고 역사적으로 C에서 가장 많이 악용된 버그 부류입니다. 해법은 %와 s 사이의 폭이고, 종료 '\0' 자리를 남기려고 배열 크기보다 하나 작게 씁니다.
폭은 형식 문자열 안에 리터럴로 써야 하므로 배열 크기와 맞춰 두기가 성가시고, 실무 C 대부분이 fgets를 쓰는 이유가 하나 더 늘어납니다.
%s의 두 번째 놀라움은 첫 공백에서 멈춘다는 것입니다. Ada Lovelace를 입력하면 name은 Ada를 담고, Lovelace는 다음 읽기를 위해 스트림에서 기다립니다. %s는 줄이 아니라 단어 하나를 읽습니다.
남은 줄바꿈 버그
모두가 부딪히는 "scanf가 안 돼요"입니다. 숫자 지정자는 앞쪽 공백을 건너뛰고, %c는 그러지 않습니다.
그 공백이 없으면 이렇게 됩니다. 30을 입력하고 엔터를 누릅니다. %d가 3과 0을 소비하고 줄바꿈에서 멈춰 '\n'을 버퍼에 남깁니다. 다음의 scanf("%c", &initial)이 그 줄바꿈을 문자로 읽고 즉시 반환합니다. 프롬프트가 나타나는 동시에 건너뛰어집니다.
scanf 형식 문자열의 공백은 "여기서 공백을 얼마든지 건너뛰라"는 뜻이므로, " %c"는 떠돌던 줄바꿈을 넘어가 진짜 문자를 기다립니다. 공백은 %c와 %[ 앞에서만 필요합니다. 다른 모든 지정자는 스스로 공백을 건너뜁니다.
실수와 문자 읽기
지정자는 대부분 printf와 공유하지만 중요한 차이가 하나 있습니다. scanf는 포인터를 통해 쓰고 있으므로 목적지의 정확한 크기를 알아야 합니다.
1.5 2.25 7 x를 시도해 보세요.
%f는 float를, %lf는 double을 읽습니다. printf에서는 float가 호출 전에 double로 승격되므로 둘이 같은 방식으로 출력되지만, scanf에는 승격이 없어서 double *에 %f를 쓰면 8바이트 변수에 4바이트만 써 손상된 채로 둡니다. 여기서 %c에 앞 공백이 필요 없는 것은, 앞의 %d가 줄바꿈이 아니라 공백에서 멈췄기 때문입니다. (전체 표는 형식 지정자에 있습니다.)
fgets: 견고한 대안
사람이 입력하는 것이라면 줄 전체를 읽고 그다음에 파싱하세요. fgets는 버퍼 크기를 인자로 받아 물리적으로 넘칠 수 없고, 줄이 다 들어갔을 때는 줄바꿈까지 소비하므로 다음 읽기를 혼란스럽게 할 것이 남지 않습니다. (버퍼보다 긴 줄은 그 꼬리가 읽히지 않은 채 남습니다 - 받은 것에 '\n'이 없다는 점이 그것을 감지하는 방법입니다.)
이 패턴에 대한 세 가지 점.
- **
sizeof line**은 진짜 크기를 넘기므로,%19s리터럴과 달리 배열 크기를 바꿔도 다른 수정이 필요 없습니다. - 줄이 다 들어갔다면
fgets는 줄바꿈을 남깁니다. 그래서strcspn잘라내기가 있습니다. 그것이 버퍼보다 긴 줄을 감지하는 방법이기도 합니다. 받은 것에'\n'이 없으면 그런 경우죠. - 파싱은 나중에. 정수에는
strtol(파싱이 어디서 멈췄는지도 알려 줍니다), 부동소수점에는strtod, 또는 이미 안전하게 쥐고 있는 문자열에 scanf 형식을 쓰려면sscanf(line, "%d %d", &a, &b)입니다.
sscanf는 따로 언급할 만합니다. 입력 스트림이 아니라 문자열에서 읽는 scanf이고, fgets와 완벽하게 어울립니다. scanf의 편리한 파싱을 그 스트림 문제 없이 얻습니다.
흔한 실수
- 배열이 아닌 인자에
&빠뜨리기.-Wall로 컴파일하세요. - 폭 없는
%s. 긴 입력을 기다리는 버퍼 오버플로입니다. - 반환값 무시. 그러고는
scanf가 대입한 적 없는 변수를 읽습니다. - 실패한
scanf를 입력을 버리지 않고 반복. 무한 반복입니다. - 숫자를 읽은 뒤 앞 공백 없는
%c. 프롬프트가 건너뛰어지는 버그입니다. double에%f쓰기. 조용한 손상입니다.scanf에는%lf가 필요합니다.%s가 줄을 읽으리라 기대하기. 공백으로 구분된 단어 하나를 읽습니다.
자주 묻는 질문
C에서 scanf에 왜 &가 필요한가요?
C가 인자를 값으로 전달하기 때문입니다. scanf는 여러분의 변수를 바꿔야 하므로 값의 사본이 아니라 변수의 주소가 필요합니다. scanf("%d", &age)처럼요. 유일한 예외는 문자 배열인데, 그 이름 자체가 이미 주소이므로 scanf("%19s", name)에는 &가 없습니다.
scanf가 왜 문자나 문자열 입력을 건너뛰나요?
앞선 scanf("%d", ...)가 엔터 키의 줄바꿈을 입력 버퍼에 남겨 뒀기 때문입니다. %d와 %f는 앞쪽 공백을 건너뛰지만 %c는 그러지 않으므로, 기다리는 대신 그 줄바꿈을 읽습니다. 형식에 공백 하나를 넣는 것이 해법입니다. scanf(" %c", &ch)는 공백을 먼저 건너뛰라고 알려 줍니다.
C에서 scanf로 문자열을 안전하게 읽으려면 어떻게 하나요?
%s에 버퍼보다 하나 작은 최대 폭을 주세요. char name[20]; scanf("%19s", name);처럼요. 폭이 없으면 %s는 사용자가 입력한 만큼 쓰고 기꺼이 배열 끝을 넘어갑니다. 또한 %s는 첫 공백에서 멈추므로 공백이 들어간 전체 이름은 읽을 수 없습니다.
C에서 scanf를 써야 하나요, fgets를 써야 하나요?
사람이 입력하는 것에는 fgets를 쓰세요. 크기가 정해진 버퍼에 한 줄 전체를 읽으므로 넘칠 수 없고, (줄이 다 들어갔다면) 줄바꿈까지 소비하므로 고전적인 혼합 입력 버그가 사라집니다. 버퍼보다 긴 줄은 그 꼬리가 입력 스트림에 읽히지 않은 채 남습니다. 그다음 sscanf나 strtol로 그 줄을 파싱하세요. scanf는 작은 연습과 형식이 잘 갖춰진 기계 입력에는 괜찮습니다.