C에서 사용자 입력을 읽는 함정: scanf와 Stdin에 관한 이야기
Source: Dev.to
C에서 사용자 입력을 읽을 때의 함정: scanf와 stdin에 관한 이야기
최근에 stdin에서 입력을 읽고, 개행 문자를 무시하며, 버퍼를 초과하는 부분은 버리고, 이를 반복하는 C 코드를 작성해야 했습니다.
최초 시도
#include <stdio.h>
void take_input(void)
{
char buf[20];
printf("> ");
scanf("%19[^\n]", buf);
printf("input=`%s`\n", buf);
}
int main(void)
{
for (int i = 0; i < 5; ++i) {
take_input();
}
return 0;
}
$ ./main
> hello world↵
input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
$ █
문자열을 한 번만 소비하고 같은 값을 다섯 번 출력했습니다. 왜일까요?
스택 동작
char buf[20]은 지역 변수이지만, take_input()이 호출될 때마다 정확히 같은 스택 메모리 주소를 사용합니다.
/* ... */
printf("address=%p, input=`%s`\n", (void*)buf, buf);
/* ... */
$ ./main
> hello world
address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
...
버퍼가 초기화되지 않았고 이후 scanf 호출이 이를 덮어쓰지 못했기 때문에 이전 내용(“유령 데이터”)이 그대로 남아 있었습니다. 버퍼를 0으로 초기화하면 이 특정 문제는 해결됩니다.
char buf[20] = {0};
이제 버퍼는 매번 초기화되지만 scanf는 여전히 이상하게 동작합니다:
$ ./main
> hello world
address=0x7ffea5c2d840, input=`hello world`
> address=0x7ffea5c2d840, input=``
> address=0x7ffea5c2d840, input=``
...
실제 문제: %[^\n]은 개행을 소비하지 않는다
스캔셋 %19[^\n]은 개행이 아닌 최대 19자를 읽지만, 개행 문자는 입력 스트림에 남겨 둡니다.
GDB로 stdin을 살펴보면 개행이 아직 남아 있음을 확인할 수 있습니다:
$ gcc -g3 -O0 main.c -o main
$ gdb main
(gdb) break 8
Breakpoint 1 at 0x11d3: file main.c, line 8.
(gdb) run
...
> hello world↵
...
(gdb) p *stdin
$1 = {
_flags = -72539512,
_IO_read_ptr = 0x55555555972b "\n",
_IO_read_end = 0x55555555972c "",
_IO_read_base = 0x555555559720 "hello world\n",
}
stdin->_IO_read_ptr가 아직 개행을 가리키고 있습니다. 루프가 다시 실행될 때 scanf("%19[^\n]", buf)는 즉시 개행을 만나 0자를 매치하고 중단합니다. 버퍼는 비어 있거나(또는 0으로) 그대로이며, 루프는 같은 출력을 반복합니다.
작동 가능한 해결책
1. 추가 scanf 호출 사용
라인의 나머지와 개행을 강제로 소비합니다:
void take_input(void)
{
char buf[20];
printf("> ");
scanf("%19[^\n]", buf);
/* 19자를 초과하는 경우 라인의 나머지를 버림 */
scanf("%*[^\n]");
/* 개행을 버림 */
scanf("%*c");
printf("input=`%s`\n", buf);
}
코드는 다소 지저분해 보이지만, 공백, 짧은 입력, 길게 잘린 입력 모두에 대해 신뢰할 수 있게 동작합니다:
$ ./main
> hey
input=`hey`
> hello world
input=`hello world`
> a b c d e f g h i j k l m o p q r s t u v w x y z
input=`a b c d e f g h i j k l m o p q r s t u v w x y z`
> /* 5개의 공백 */
input=` `
> abcdefghijklmnpoqrstuvwxyz
input=`abcdefghijklmnpoqrs`
2. fgets – 표준적인 방법
보다 예측 가능하고 명시적인 방법은 fgets로 한 줄을 읽고 개행을 제거하는 것입니다:
#include <stdio.h>
#include <string.h>
void take_input_2(void)
{
char buf[20];
printf("> ");
if (fgets(buf, sizeof(buf), stdin)) {
char *newline_ptr = strchr(buf, '\n');
if (newline_ptr) {
/* \n을 \0으로 바꿔서 트림 */
*newline_ptr = '\0';
} else {
/* 개행이 없으면 – 입력이 잘린 경우;
라인의 나머지를 소비 */
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
}
printf("input=`%s`\n", buf);
}
3. getline – 힙을 이용한 방법
getline(POSIX, ISO C는 아님)은 전체 라인을 읽으며 힙에 버퍼를 할당(또는 재할당)합니다. 메모리는 직접 해제해야 합니다:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void take_input_3(void)
{
char *line = NULL; /* 포인터 초기화 */
size_t cap = 0; /* getline이 업데이트할 용량 */
ssize_t n;
printf("> ");
n = getline(&line, &cap, stdin);
if (n > 0) {
/* 뒤쪽 개행 제거 */
if (line[n-1] == '\n') {
line[n-1] = '\0';
}
printf("input=`%s`\n", line);
}
free(line); /* 힙 메모리는 항상 해제 */
}
정리
이 연습은 C에서 입력 처리가 얼마나 까다로울 수 있는지를 일깨워 주었습니다. scanf는 편리해 보이지만 공백과 개행에 관한 엣지 케이스에 부딪히면 “편리함”이 오히려 부담이 됩니다.
- 잘라내기(truncation) 처리를 포함한 예측 가능하고 이식 가능한 라인 기반 입력을 원한다면
fgets가 거의 항상 더 좋은 선택입니다. - POSIX와 힙 할당이 가능한 환경이라면
getline이 유연성을 제공합니다. - 입력 형식이 정확히 알려진 경우에만
scanf를 사용하세요.