Proven C BookEnglish GitHub

25 입력

먼저 알아야 할 것

22장 출력 · 출력의 두 갈래
10장 스트림의 기원 · 스트림은 양방향이다

돌아보기

10장에서 입력도 출력처럼 “문자가 한 줄씩 흐르는 띠”라 했고, 사람의 입력은 엔터로 끝나는 줄 단위가 자연스럽다고 했다(행 버퍼링). 그러면 프로그램이 입력을 받는 자연스러운 단위도 — 줄인가?

답. 바로 그렇다 — 그리고 그것이 이 장의 설계 원리다. 사람은 한 줄을 적고 엔터를 친다. 그러니 프로그램도 한 줄을 통째로 받아 온 다음, 그 안에서 필요한 값을 찾아 읽는 것이 흐름의 결을 따르는 방식이다. 글자의 띠에서 줄을 뜨는 일과, 뜬 줄을 해석하는 일 — 이 둘을 나누면 각각이 단순해진다.

이 장의 필요성과 맥락

입력을 출력(22장)보다 뒤에 둔 데는 이유가 있다 — 읽은 값을 담을 곳이 있어야 하는데, 그것이 23장의 변수다. 그리고 이 장은 일부러 절반만 갚는다: 안전한 입력의 나머지는 배열과 문자열을 배운 뒤인 43장의 몫이다. 위험을 여기서 예고만 하고 넘기는 것이 이 책이 택한 두 단계다.

이 장이 끝나면

22장의 예고를 완결한다 — 저장할 곳(변수)이 생겼으니 입력을 받는다. 이 책이 처음부터 가르치는 방식은 현대적 관행 그대로다: 한 줄을 통째로 읽고, 그 줄을 해석한다. 읽기와 해석의 분리 — 이 두 단계 구조가 편리함과 안전함의 원천이다. fgets 가 정확히 무엇을 약속하고 무엇은 내 몫으로 남기는지(개행과 잘림), 그리고 해석이 실패할 때 무엇이 남는지까지 본다. 제5부가 여기서 완성된다.

이 장에서 답할 질문

  1. 왜 두 단계로 나누는가? 입력에서 바로 %d를 읽어 주는 함수(scanf)도 있다고 들었는데.
  2. 그릇은 얼마나 크게 잡아야 하는가?

25.1 두 단계 — 읽고, 해석한다

시연부터 본다. 표준 입력에서 정수 하나를 받아 제곱을 알려 주는 프로그램이다. (이 실행에서 표준 입력으로 준 내용을 가운데 상자에 보였다.)

examples/ch25/read.c

#include <stdio.h>

int main(void)
{
    char line[100];   /* 글자 100칸짜리 공간 — 정식 설명은 33장 */
    int n = 0;

    fgets(line, sizeof line, stdin);   /* 1단계: 한 줄을 통째로 읽는다 */
    sscanf(line, "%d", &n);            /* 2단계: 그 줄에서 정수를 해석한다 */

    printf("%d squared is %d.\n", n, n * n);
    return 0;
}

표준 입력으로 준 것

7

실행 결과

7 squared is 49.

새 얼굴이 둘 있다 — 한 단계에 하나씩이다.

1단계, fgets — 한 줄을 통째로 읽는다. fgets(line, sizeof line, stdin)은 표준 입력(stdin이 그 이름이다)에서 한 줄을 읽어 line 이라는 공간에 담는다. 첫 줄의 char line[100];은 “글자 100칸짜리 공간을 잡고 line이라 부른다”는 선언인데 — 여러 칸짜리 공간(배열)의 정식 문법은 38장의 것이라, 지금은 “줄을 담는 그릇”이라고만 알아 두면 된다(의도된 외상이다). 둘째 재료 sizeof line은 “그릇의 칸 수” — fgets에게 그릇 크기를 알려서 넘치게 담는 일이 없게 하는 안전장치다 (sizeof 연산자의 정식 취급은 35장).

2단계, sscanf — 담아 온 줄을 해석한다. sscanf(line, "%d", &n)은 22장 printf의 짝이다 — 같은 서식 언어를 반대 방향으로 쓴다. printf가 값을 글자로 바꿔 내보냈다면, sscanf는 글자(line"7")에서 서식(%d)에 맞는 값을 찾아 변수에 담는다. 이름 앞의 &는 “값을 담을 변수의 자리를 알려 주는 표시”다 — 5장에서 배운 주소가 문법에 처음 얼굴을 내민 순간인데, 정식 취급은 35장에 있다 (이것이 제5부의 마지막 의도된 외상이다).

문. 왜 두 단계로 나누는가? 입력에서 바로 %d를 읽어 주는 함수(scanf)도 있다고 들었는데.

답. 있고, 많은 입문서가 그것부터 가르친다 — 이 책이 다른 길을 택한 이유는 두 가지다. 첫째, 실패의 뒤처리가 깨끗하다. 해석에 실패해도 (숫자가 아닌 것이 들어와도) 줄은 이미 내 그릇에 있으므로, 입력의 띠가 어중간한 자리에서 멈춰 꼬이는 일이 없다 — 입력 띠에서 직접 읽다 실패하면 남은 글자들이 띠에 걸린 채 다음 읽기를 오염시키는데, 이것이 scanf 직접 사용의 고전적 골칫거리다. 둘째, 안전의 결이 같다. 줄 읽기는 그릇 크기를 알려 주며 읽는 구조라 넘침이 원천 차단된다 — 그릇 크기를 말하지 않는 옛 방식들이 어떤 사고를 냈는지, 그리고 이 두 단계 관행이 어떻게 그 사고를 봉쇄하는지가 42장의 이야기다. 처음부터 안전한 결로 손에 익히는 것 — 그것이 이 책의 선택이다.

25.2 fgets의 계약 — 무엇을 주고, 무엇은 말해 주지 않는가

두 단계의 첫 단계를 정확히 알아 둔다. fgets 는 세 가지를 약속한다.

약속무슨 뜻인가
그릇 크기를 넘기지 않는다sizeof line 을 받았으니 그보다 많이 담지 않는다 — 넘침이 원천 차단된다
끝에 NUL 을 붙인다담긴 것이 문자열로 쓰일 수 있게 한다(42장)
줄바꿈까지 담아 준다읽은 줄의 \n 이 그릇 안에 들어 있다 — 떼어 내는 것은 내 몫이다

표 25.1

세 번째가 초보자를 자주 무는 자리다. 시연으로 눈에 보이게 한다 — 개행을 @ 로 바꿔 찍었다.

examples/ch25/lines.c

/* fgets 의 계약 — 무엇을 담아 주고, 무엇을 알려 주지 않는가. */
#include <stdio.h>
#include <string.h>

int main(void)
{
    char line[16];          /* 일부러 작게 잡았다 — 잘림을 보이려고 */

    puts("[read one line at a time and echo it back]");
    while (fgets(line, sizeof line, stdin) != NULL) {
        size_t len = strlen(line);
        int has_nl = (len > 0 && line[len - 1] == '\n');

        /* 담긴 것을 눈에 보이게 — 개행은 기호로 바꿔서 */
        printf("  got: \"");
        for (size_t i = 0; i < len; i++)
            putchar(line[i] == '\n' ? '@' : line[i]);
        printf("\"  (%zu bytes, newline at end: %s)\n",
               len, has_nl ? "yes" : "no");

        if (!has_nl)
            puts("    -> no newline means the line was longer than the buffer and was cut");
    }

    puts("\n[two reasons reading stops]");
    puts("  when fgets returns NULL it is either end of file or an error.");
    puts("  feof and ferror tell them apart; chapter 63 covers that properly.");
    return 0;
}

표준 입력으로 준 것

hi
this line is definitely longer than the box
bye

실행 결과

[read one line at a time and echo it back]
  got: "hi@"  (3 bytes, newline at end: yes)
  got: "this line is de"  (15 bytes, newline at end: no)
    -> no newline means the line was longer than the buffer and was cut
  got: "finitely longer"  (15 bytes, newline at end: no)
    -> no newline means the line was longer than the buffer and was cut
  got: " than the box@"  (14 bytes, newline at end: yes)
  got: "bye@"  (4 bytes, newline at end: yes)

[two reasons reading stops]
  when fgets returns NULL it is either end of file or an error.
  feof and ferror tell them apart; chapter 63 covers that properly.

25.2.1 잘렸는지는 어떻게 아는가

시연의 가운데 두 줄이 그 답이다. 그릇보다 긴 줄이 들어오면 fgets그릇이 차는 데까지만 담고 돌아온다 — 그리고 그때는 끝에 개행이 없다.

그릇 끝의 모습
… \n 으로 끝난다줄 하나를 온전히 받았다
개행 없이 끝난다줄이 잘렸다 — 나머지는 아직 띠에 남아 있고, 다음 fgets 가 그 뒤를 이어 받는다

표 25.2

「끝에 개행이 있는가」가 잘림의 신호다. 시연에서 긴 한 줄이 세 번에 나뉘어 들어온 것이 그 증거다.

그래서 실무의 관용구가 둘이다.

line[strcspn(line, "\n")] = '\0';   /* 개행이 있으면 떼어 낸다 */

첫째는 개행 떼기다. 위 한 줄이 관용구인데, strcspn(42장)은 「\n 이 처음 나오는 자리」를 돌려주므로 그 자리에 NUL 을 놓으면 개행이 사라진다. 개행이 없으면 문자열의 끝을 가리키므로 아무 일도 일어나지 않는다 — 한 줄로 두 경우가 다 처리된다.

둘째는 잘림을 오류로 다루기다. 「잘렸지만 성공」을 성공으로 넘기면 잘린 이름으로 파일을 찾는 사고가 뒤따른다 — 43장에서 다섯 규율의 하나로 정식으로 다룬다.

문. 그릇은 얼마나 크게 잡아야 하는가?

답. 정답은 없고, 판단의 기준이 있다.

사람이 치는 한 줄이라면 넉넉히 잡는다 — 이름·경로·명령이라면 256이나 1024가 흔한 선택이다. 시연이 16으로 잡은 것은 잘림을 보이려고 일부러 작게 한 것이다.

줄 길이에 상한이 없다면 그릇을 키우는 것으로는 끝나지 않는다. 그때는 잘림을 검사해 이어 붙이거나(반복해서 fgets), 줄 길이만큼 그릇을 늘려 주는 도구를 쓴다 — POSIX 의 getline 이 그것인데 표준 C 에는 없다(66장).

기억할 것은 하나다. 크기를 넘겨받는 함수를 쓰는 한, 그릇이 작아서 생기는 일은 「잘림」이지 「넘침」이 아니다. 잘림은 검사할 수 있고, 넘침은 프로그램이 무너진다.

25.3 해석은 실패할 수 있다

출력과 달리, 입력에는 본질적인 새 문제가 하나 있다 — 상대가 무엇을 보낼지 모른다. 7을 기대했는데 일곱이 올 수 있다. 그래서 sscanf의 반환값은 해석에 성공한 값의 개수다 — 위 예제라면 성공 시 1, 실패 시 0이다.

지금의 우리는 이 반환값을 확인하고도 “성공이면 이 길, 실패면 저 길”로 갈라설 문법 — 분기 — 이 없다. 그래서 위 예제는 반환값을 버렸고, n을 0으로 초기화해 두는 것(23장의 습관)으로 실패 시의 바닥을 깔아 두었다. 갈림길은 다음 부(30장)에서 생기고, “실패를 값으로 알리고 반드시 확인한다”는 규율은 51장에서 정식으로 세운다 — 입력 검사는 그 규율의 첫 실전이 될 것이다.

흔한 오해. “입력은 키보드에서 온다”

대개 그렇지만, 본질은 아니다 — 입력은 표준 입력 스트림에서 온다 (10장). 사실 방금의 시연이 그 증거다: 이 책의 예제 검증 기계에는 키보드를 두드리는 사람이 없다 — 위 실행의 입력 7은 파일에 담겨 표준 입력의 띠로 흘러 들어간 것이다(리다이렉션). 22장의 출력이 화면행이 아니듯 입력도 키보드행이 아니라서, 같은 프로그램이 사람과의 대화에도, 파일 처리에도, 프로그램들의 연결에도 고치지 않고 쓰인다. 스트림 설계의 힘이 입력 쪽에서도 반복되는 것이다.

25.4 제5부를 닫으며

이름을 만드는 법을 다 배웠다 — 값의 이름(23장), 일의 이름(24장), 그리고 바깥세상에서 값을 받아 이름에 담는 법(25장)까지. 15장의 외상 장부는 청산되었고, 새로 진 외상은 정확히 둘 — 줄 그릇(char line[100], 38장에서)과 자리 표시(&, 35장에서) — 뿐이며, 둘 다 청산 기일이 적혀 있다.

무엇보다, 이제 프로그램이 대화할 수 있게 됐다. 받아서, 계산하고, 답한다 — 다음 부에서는 그 계산 자체를 벼린다: 정수의 세계를 정면으로 다루고(27·28장), 비교하고 판단하고(30·31장), 반복하고(32장), 함수의 의미를 완성한다(33장). 값과 흐름의 부다.