Subsections


12. The Standard I/O Library

프로그램에게 할 일을 말할 수 없거나, 프로그램이 자기가 처리한 일을 사용자에게 알려줄 수 없다면, 쓸모있는 프로그램이라고 말할 수 없을 것입니다. 따라서 거의 모든 프로그램은 입출력(I/O)을 처리한다고 말할 수 있습니다. C 언어에서 I/O는 라이브러리 함수를 통해서 제공됩니다 -- 표준 I/O 또는 “stdio” 라이브러리12.1 -- 그리고 이 함수들은 가장 많이 쓰이는 C 라이브러리 중의 하나입니다.

C 언어의 minimalist philosophy에 동조하기 위해, stdio 함수들은 간단한, 그러나 꼭 필요한(least-common-denominator) I/O 모델입니다. 여러분은 파일을 열고(open), 읽고(read), 쓸(write) 수 있습니다. 파일은 연속적인 문자의 스트림(sequential character stream)으로 취급되며, 찾기(seeking12.2)가 가능합니다. 필요하다면 파일을 텍스트(text)와 이진(binary) 로 구별하여 처리할 수 있으며, 파일에 문자열을 써서 이름을 지어줄 수 있습니다; 파일 이름에 관한 자세한 것은 OS에 따라 달라집니다. 파일 이름을 지을 때를 제외하고 디렉토리에 관한 개념은 C 언어에 없습니다. 즉, 파일 이름을 지정할 때 디렉토리 이름을 쓸 수 있지만, 디렉토리 자체를 취급하는 일, 디렉토리를 만들거나, 디렉토리를 나열하는 일을 처리하는 표준 방법은 없습니다 (19 절을 참고하기 바랍니다). 모든 프로그램은 처음 시작할 때 자동적으로 세 개의 predefined I/O 스트림이 열려지게 됩니다: 여러분은 대개 키보드로 연결된 stdin에서 읽을(read) 수 있고, 대개 스크린으로 연결된 stdout이나 stderr를 써서 쓸(write) 수 있습니다. 그렇지만 키보드나 스크린과 관련된 기능은 매우 적습니다 (다시 19 절을 참고하기 바랍니다).

이 절의 많은 부분이 printf (질문 [*]12.6에서 [*]12.11까지)와 scanf (질문 [*]12.12에서 [*]12.20까지)에 연관되어 있습니다. 질문 [*]12.21에서 [*]12.26은 다른 stdio 함수들을 설명합니다. 특정 파일을 access할 필요가 있다면, fopen (질문 [*]12.27부터 [*]12.32까지)을 써서 직접 열어서(open) 쓰거나(use), 표준 stream을 redirect해서 (질문 [*]12.33에서 [*]12.36까지) 쓸 수 있습니다. text I/O를 하려는 마음이 없다면, “binary” stream을 쓸 수 (질문 [*]12.37에서 [*]12.42까지) 있습니다. 이 모든 것을 자세히 알아보기 전에 먼저 간단한 것부터 알고 넘어갑시다. 다음 질문들을 읽어보기 바랍니다.


12.1 Basic I/O



Q 12.1
이 코드에서 잘못된 부분이 있나요?
  char c;
  while ((c = getchar()) != EOF) ...
Answer
일단, getchar의 리턴 값을 저장하는 변수는 반드시 int이어야 합니다. getchar()는 어떠한 문자 값이나, EOF를 리턴할 수 있습니다. EOF는 int 타입이기 때문에 이 리턴 값을 char에 저장하는 것은 EOF를 잘못 해석하게 할 소지가 있습니다 (특히 char의 타입이 unsigned인 경우 문제가 심각합니다).

위의 코드처럼 getchar()의 리턴값을 char에 담을 경우, 두 가지 결과를 예상할 수 있습니다.

이 버그는 char가 signed이고, 입력이 모두 7 비트 문자인 경우, 발견하기가 매우 힘듭니다 (char가 signed인지 unsigned인지는 implementation-defined입니다.)

References
[K&R1] § 1.5 p. 14
[K&R2] § 1.5.1 p. 16
[C89] § 6.1.2.5, § 7.9.1, § 7.9.7.5
[H&S] § 5.1.3 p. 116, § 15.1, § 15.6
[CT&P] § 5.1 p. 70
[PCS] § 11 p. 157



Q 12.2
이 코드는 마지막 문장을 두번 복사하는데 왜 그럴까요?
  while (!feof(infp)) {
    fgets(buf, MAXLINE, infp);
    fputs(buf, outfp);
  }
Answer
C 언어에서 end-of-file은 단지 입력 루틴이 읽으려 했으나 실패했다는 것을 의미합니다. (다시 말해 C의 I/O는 Pascal과는 다릅니다.) 일반적으로 다음과 같이 입력 루틴의 리턴 값을 검사해야 합니다:

  while (fgets(buf, MAXLINE, infp) != NULL)
    fputs(buf, outfp);

대개 feof()를 직접 쓸 이유는 없습니다 ( stdio 함수가 EOFNULL을 리턴한 경우, feof()ferror()를 써서 정말로 파일의 끝인지, 읽기(read) 에러인지 검사할 필요가 있긴 합니다.)

References
[K&R2] § 7.6 p. 164
[ANSI] § 4.9.3, § 4.9.7.1, § 4.9.10.2
[C89] § 7.9.3, § 7.9.7.1, § 7.9.10.2
[H&S] § 15.14 p. 382

Note
예를 들어 fread()fwrite(), fgets(), fputs()와 같은 함수들은 에러가 발생하거나 파일 끝에 다다랐을 때, EOF를 리턴합니다. 이런 함수들이 정말 파일 끝에 다다랐는지 확인할려면 feof()ferror()를 써야 하는 것입니다.

이런 경우를 제외하고, feof()를 직접 쓰는 경우는 매우 드뭅니다. 따라서 일반적으로 다음과 같은 코드보다:

  while (!feof(fp))
    fgets(line, max, fp);

다음과 같은 코드를 쓰기 바랍니다:

  while (fgets(line, max, fp) != NULL)
    ...;

즉, feof()는, end-of-file인지 아닌지 검사하기 위해 쓰는 함수가 아니라, end-of-file과 error인 상황을 구별하기 위해 쓰는 함수라고 기억하는 것이 좋습니다. (물론 이 목적으로, ferror()를 대신 쓸 수 있습니다.)



Q 12.3
fgets()를 써서, 파일에서 한 줄씩 읽어 그 내용을 포인터의 배열에 저장하려 합니다. 왜 모든 줄의 내용이 마지막 줄로 되어 있을까요?

Answer
질문 [*]7.4를 보기 바랍니다.



Q 12.4
제 프로그램의 프롬프트와 중간 단계까지의 출력은 (특히 파이프를 써서 출력을 다른 프로그램에 넘길 때), 때때로 스크린에 출력되지 않습니다.
Answer
출력이 당장 스크린에 반영되기를 원한다면12.4 (특히 텍스트가 \n으로 끝나지 않는 경우), fflush(stdout)을 불러 주는 것이 좋습니다. 대개의 메커니즘이 적절한 시기에 자동으로 fflush()를 불러주지만, 이 메커니즘들은 stdout이 interactive한 터미널로 연결되어 있다고 가정한 것이기 때문에, 가끔 제대로 동작하지 않을 수 있습니다. (덧붙여 질문 [*]12.24도 참고하시기 바랍니다.)

References
[ANSI] § 4.9.5.2
[C89] § 7.9.5.2

Note
질문 21.11의 예를 보면, fflush를 부르는 예를 볼 수 있습니다.



Q 12.5
RETURN 키를 누르지 않고 한 글자를 즉시 입력받을 수 있는 방법이 있을까요?
Answer
질문 [*]19.1을 참고하기 바랍니다.


12.2 printf Formats



Q 12.6
printf로 `\%'를 출력할 수 있을까요? \%를 써 보았지만 출력되지 않더군요.
Answer
%%를 쓰면 됩니다. % 문자를 출력하는 것이 까다로운 이유는, printf()% 문자를 escape 문자로 쓰기 때문입니다. 아시다시피, printf% 문자를 보면, 다음 글자를 읽어서 어떤 값을 출력해야 하는지 조사하게 됩니다. 이때, 다음 글자로 %를 쓰면 -- 즉 %%를 쓰면 -- `%'를 출력하게 됩니다.

\%는 동작하지 않습니다. 이 방법이 왜 동작하지 않는지 이해하려면, 먼저 일단 백 슬래시(`\')는 컴파일러의 escape 문자이며, 컴파일러가 여러분의 코드를 이해하는 길을 보여주는 목적으로 쓰인다는 것을 아셔야 합니다. 컴파일러 입장에서는 \%는 정의되지 않은 escape 문자이며, 대개 `%' 한 글자를 의미하게 됩니다. 따라서 만약 printf()가 `\'를 특별하게 처리할 수 있도록 만들어져 있다 하더라도, 컴파일러가 이를 먼저 처리해 버리게 되므로, `%'를 출력할 수 없게 됩니다.

덧붙여 질문 [*]8.8, [*]19.17도 참고하시기 바랍니다.

References
[K&R1] § 7.3 p. 147
[K&R2] § 7.2 p. 154
[C89] § 7.9.6.1



Q 12.7
다음 코드는 왜 동작하지 않나요?
  long int n = 123456;
  printf("%d\n", n);

Answer
long int를 출력할 때는, 반드시 l (L의 소문자) modifier를 printf 포맷에 써야 (예를 들어 %ld) 합니다. 왜냐하면 printf()는 여러분이 전달한 인자의 (여기서는 변수 n의 값) 타입을 알 수 없기 때문입니다. 그러므로 반드시 올바른 포맷 (format specifier)을 써야 됩니다.



Q 12.8
Aren't ANSI function prototypes supposed to guard against argument type mismatches?
Answer
질문 [*]15.3을 참고하기 바랍니다.



Q 12.9
printf()에서 %lf를 쓰지 말라고 한 것을 들었습니다. double 타입을 쓸 때, scanf()는 %lf를 쓰면서 왜 printf()는 %f를 쓰는 것인가요?
Answer
printf의 %f specifier는 floatdouble 두 타입에 모두 쓰는 것은 사실입니다.12.5 “default argument promotion” 때문에 (printf와 같이 가변 인자12.6를 사용하거나, 프로토타입이 없는 함수에게 적용됨) float 타입의 값은 인자로 전달될 때, double 타입으로 바뀌게 됩니다. 따라서 printf는 결국 무조건 double 타입만 전달받는 셈입니다. 덧붙여 질문 [*]15.2도 참고하시기 바랍니다. (참고로 printflong double 타입을 쓸 경우에는 %Lf를 사용합니다.)

scanf는 인자로 포인터를 받기 때문에 그러한 promotion이 일어나지 않습니다. 따라서 완전히 다른 상황입니다. (포인터를 써서) float에 값을 저장하는 것은 double에 값을 저장하는 것과 다릅니다. 따라서 scanf%f%lf로서 구별합니다.

아래 표에서는 printfscanf에서 쓰이는, 각각의 타입에 따른 format specifier를 보여줍니다.

Format printf scanf
%c int char *
%d, %i int int *
%o, %u, %x unsigned int unsigned int *
%ld, %li long int long int *
%lo, %lu, %lx unsigned long int unsigned long int *
%hd, %hi int short int *
%ho, %hu, %hx unsigned int unsigned short int *
%e, %f, %g double float *
%le, %lf, %lg n/a double *
%s char * char *
%[...] n/a char *
%p void * void **
%n int * int *
%% none none
(엄격하게 말해서, 대부분의 시스템이 받기는 하지만, printf에서 %lf는 정의되어 있지 않습니다. 이식성을 위해서 항상 %f를 쓰기 바랍니다.)

덧붙여 질문 [*]12.13, [*]15.2도 참고하시기 바랍니다.

References
[K&R1] § 7.3 pp. 145-47, § 7.4 pp. 147-50
[K&R2] § 7.2 pp. 153-44, § 7.4 pp. 157-59
[C89] § 7.9.6.1, § 7.9.6.2
[H&S] § 15.8 pp. 357-64, § 15.11 pp. 366-78
[CT&P] § A.1 pp. 121-33



Q 12.9b
printfsize_t와 같은, 즉, 그 크기가 long인지, 다른 것인지 알 수 없는 typedef 타입을 출력할 때에는 어떤 포맷을 써야 하나요?

Answer
주어진 타입을 알려진, 적절한 타입으로 캐스팅해서 출력하면 됩니다. 예를 들어, 어떤 타입의 크기를 출력할 때에는 다음과 같이 할 수 있습니다:
  printf("%lu", (unsigned long)sizeof(thetype));



Q 12.10
printf()에서 가변 폭(field width)을 지정하려 합니다. 예를 들어, %8d를 쓰지 말고 실행 시간에 이 폭을 지정하려 합니다.
Answer
printf("%*d", width, x)를 쓰면 됩니다. Format specifier에서 별표는 필드 폭을 지정하기 위해 int 값이 인자로 들어온다는 것을 뜻합니다. (인자 목록에서 폭은 출력할 값보다 먼저 지정합니다.) 덧붙여 질문 [*]12.15도 참고하시기 바랍니다.

References
[K&R1] § 7.3
[K&R2] § 7.2
[C89] § 7.9.6.1
[H&S] § 15.11.6
[CT&P] § A.1



Q 12.11
천 단위로 수치에 콤마(comma)를 찍어서 출력하고 싶습니다. 화폐 단위같은 수치를 출력할 수 있는 방법이 있나요?
Answer
<locale.h>는 이러한 연산을 수행하기 위한 간단한 기능을 제공합니다. 그러나 이런 일을 하기 위한 표준 루틴은 없습니다. (printf()가 할 수 있는 일은, locale 설정에 따라 소수점을 찍는 것 뿐입니다.12.7.

Locale 설정에 따른, 콤마로 구분되는 수치를 출력하기 위한, 간단한 함수를 제공합니다:

  #include <locale.h>

  char *
  commaprint(unsigned long n)
  {
    static int comma = '\0';
    static char retbuf[30];
    char *p = &retbuf[sizeof(retbuf) - 1];
    int i = 0;

    if (comma == '\0') {
      struct lconv *lcp = localeconv();
      if (lcp != NULL) {
        if (lcp->thousands_sep != NULL &&
            *lcp->thousands_sep != '\0')
          comma = *lcp->thousands_sep;
        else
          comma = '.';
      }
    }
    *p = '\0';
    do {
      if (i % 3 == 0 && i != 0)
        *-p = comma;
      *-p = '0' + n % 10;
      n /= 10;
      i++;
    } while (n != 0);
    return p;
  }

좀 더 나은 방법은 세개의 숫자로 구별된다고 가정하지 말고, lconv 구조체의 grouping 필드를 쓰는 것입니다. retbuf의 적당한 크기는 다음과 같습니다:

  4 * (sizeof(long) * CHAR_BIT + 2) / 3 / 3 + 1
질문 [*]12.21을 참고하기 바랍니다.

References
[C89] § 7.4
[H&S] § 11.6 pp. 301-4



Q 12.A
int16_t과 같은 타입을 출력하기 위해서, 어떤 포맷을 써야 하나요?
Answer
헤더 파일 <inttypes.h>를 포함시키면 다음과 같은 매크로를 쓸 수 있습니다. 모든 매크로는 문자열 상수(character string literal)로 치환됩니다:

intN_t int_leastN_t int_fastN_t intmax_t intptr_t
PRIdN PRIdLEASTN PRIdFASTN PRIdMAX PRIdPTR
PRIiN PRIiLEASTN PRIiFASTN PRIiMAX PRIiPTR
uintN_t uint_leastN_t uint_fastN_t uintmax_t uintptr_t
PRIoN PRIoLEASTN PRIoFASTN PRIoMAX PRIoPTR
PRIuN PRIuLEASTN PRIuFASTN PRIuMAX PRIuPTR
PRIxN PRIxLEASTN PRIxFASTN PRIxMAX PRIxPTR
PRIXN PRIXLEASTN PRIXFASTN PRIXMAX PRIXPTR

모든 매크로는 PRI로 시작하며, printf() 계열의 함수에서 쓸 수 있는, %d, %i, %o, %u, %x, %X 포맷을 의미하는 글자인 d, i, o, u, x, X가 나온 다음, 각각의 타입을 대문자로 나타낸 이름이 나옵니다.

즉, uint_fast16_t를 썼고, %o를 쓴 효과를 얻으려면, PRIoFAST16을 쓰면 됩니다. 마찬가지로, 가장 큰 unsigned 타입의 정수값을 저장하고 싶다면 uintmax_t 타입의 오브젝트를 쓰면 됩니다. 예를 들면 다음과 같습니다:

#include <inttypes.h>
#include <stdio.h>

int
main(void)
{
  uint_fast16_t foo;
  uintmax_t i = UINTMAX_MAX;
  ...
  fprintf(stderr, "foo is %" PRIoFAST16 ".\n", foo);
  
  printf("The largest integer value is %020" PRIxMAX "\n",
         i);

  return 0;
}

덧붙여 질문 [*]1.1, [*]11.I, [*]12.B도 참고하시기 바랍니다.

References
[C99] § 7.8.1, § 7.18.1


12.3 scanf Formats



Q 12.12
scanf("%d", i)는 동작하지 않나요?
Answer
scanf()에 전달하는 인자는 항상 포인터이어야 합니다: 각각의 값이 변환될 때마다 scanf는 여러분이 전달한 포인터가 가리키는 위치에 그 값을 저장합니다. (덧붙여 질문 [*]20.1도 참고하시기 바랍니다.) 그렇기 때문에, scanf("%d", &i)로 써야 합니다.



Q 12.13
다음 코드는 왜 동작하지 않나요?
  double d;
  scanf("%f", &d);
Answer
printf()와는 달리, scanf()double 타입을 쓸 때에는 %lf를, float 타입을 쓸 때에는 %f를 씁니다.12.8 scanf%f format을 보면 float을 가리키는 포인터를 받게 됩니다. 따라서 여러분이 준 double을 가리키는 포인터는 잘못된 것입니다. %lf을 쓰거나 변수를 float으로 선언하기 바랍니다. 덧붙여 질문 [*]12.9도 참고하시기 바랍니다.



Q 12.14
이 코드는 왜 동작하지 않나요?
  short int s;
  scanf("%d", &s);

Answer
%d를 쓰면, scanfint를 가리키는 포인터를 받습니다. short int를 가리키는 포인터를 쓰려면 %hd를 쓰기 바랍니다. (질문 [*]12.9의 표를 참고하기 바랍니다.)



Q 12.15
scanf 포맷 문자열에 가변 폭을 지정할 수 있을까요?
Answer
안됩니다. scanf()에서 별표(*)는 대입을 생략하기 위한 것입니다. ANSI 문자열 관련 함수를 사용하거나, 원하는 폭 크기를 나타내는 preprocessor macro를 써서 scanf format string을 만들어야 합니다:

  #define WIDTH         3

  #define Str(x)        #x
  #define Xstr(x)       Str(x)    /* 질문 11.17 참고 */

  scanf("%" Xstr(WIDTH) "d", &n);

폭의 크기가 실행 시간에만 알 수 있다면, format string을 실행 시간에 만들어야 할 것입니다:

  char fmt[10];
  sprintf(fmt, "%%%dd", width);
  scanf(fmt, &n);

이렇게 만든 scanf format은 표준 입력에서 읽을 때보다는, fscanfsscanf에서 쓸모있을 것입니다. 덧붙여 질문 [*]11.17, [*]12.10도 참고하시기 바랍니다.



Q 12.16
파일에서 데이터를 미리 지정한 형식으로 읽으려면 어떻게 해야 할까요? scanf()에서 %f를 10번 쓰는 방법 말고, 어떻게 10개의 float을 파일에서 읽을 수 있을까요? How can I read an arbitrary number of fields from a line into an array?

Answer
일반적으로 데이터를 parsing(파싱)하는 방법은 크게 세 가지로 나눌 수 있습니다:
  1. 적당한 format string과 함께 fscanfsscanf를 씁니다. 이 chapter에서 지적한 제한 사항에도 불구하고 (질문 [*]12.20 참고), scanf 계열의 함수는 매우 강력합니다. 공백으로 구별된 필드가 가장 다루기 쉽지만, scanf format string은 좀 더 compact, column-oriented인 FORTRAN-style의 데이터를 처리하는 데에도 쓰입니다. 예를 들어, 아래와 같은 줄은:
      1234ABC5.678
    
    "%d%3s%f"를 써서 읽을 수 있습니다. (질문 [*]12.19의 마지막 예를 참고하기 바랍니다.)

  2. 데이터를 공백으로 (또는 다른 delimeter를 써서) 구별되는 필드로 쪼개고, strtok 함수나 이와 같은 기능을 가진 함수를 (질문 [*]13.6 참고) 쓴 다음, atoiatof와 같은 함수를 쓰면 됩니다. (일단 이렇게 쪼개고 나면, 각각의 field를 다루는 코드는 main()에서 argv 배열을 다루는 것과 거의 같아집니다; 질문 [*]20.3 참고) 이 방법은 특히 데이터가 몇 개의 필드로 이루어졌는지 모를 때 (실제 데이터를 읽기 전까지) 특히 쓸모가 있습니다.

    아래는 10개의 실수를 배열로 복사하는 간단한 예제입니다:

      #include <stdlib.h>
      #define MAXARGS 10
    
      char *av[MAXARGS];
      int ac, i;
      double array[MAXARGS];
    
      ac = makeargv(line, av, MAXARGS);
      for (i = 0; i < ac; i++)
        array[i] = atof(av[i]);
    
    (makeargv의 정의는 질문 [*]13.6을 참고하기 바랍니다.)

  3. 특별한 방법으로 (in ad hoc way) 데이터를 분석할 수 있는 아무런 포인터 연산이나 라이브러리 함수를 쓰면 됩니다. (ANSI strtolstrtod 함수는 어디서 분석을 멈추었는가를 포인터로 알려주기 때문에 특히 이런 스타일의 parsing에 쓸모 있습니다.) 이 방법은 물론 가장 일반적인 방법이나, 가장 에러를 발생하기도 쉽고 어려운 방법입니다: The thorniest parts of many C programs are those that use lots of tricky little pointers to pick apart strings.

가능하다면 데이터 파일의 형식과 입력 형식을 복잡한 파싱 과정이 필요없게 디자인하고, 첫번째 또는 두번째 방법을 써서 분석할 수 있도록 만드는 것이 좋습니다.



Q 12.B
int16_t와 같은 타입을 입력받기 위해서, 어떤 포맷을 써야 하나요?
Answer
헤더 파일 <inttypes.h>를 포함시키면 다음과 같은 매크로를 쓸 수 있습니다. 모든 매크로는 문자열 상수(character string literal)로 치환됩니다:
intN_t int_leastN_t int_fastN_t intmax_t intptr_t
SCNdN SCNdLEASTN SCNdFASTN SCNdMAX SCNdPTR
SCNiN SCNiLEASTN SCNiFASTN SCNiMAX SCNiPTR
uintN_t uint_leastN_t uint_fastN_t uintmax_t uintptr_t
SCNoN SCNoLEASTN SCNoFASTN SCNoMAX SCNoPTR
SCNuN SCNuLEASTN SCNuFASTN SCNuMAX SCNuPTR
SCNxN SCNxLEASTN SCNxFASTN SCNxMAX SCNxPTR
SCNXN SCNXLEASTN SCNXFASTN SCNXMAX SCNXPTR

위 매크로들은 질문 [*]12.A에서 설명한 것과 동일한 규칙을 가지며, PRI 대신 SCN으로 시작한다는 것만 다릅니다.

#include <inttypes.h>
#include <stdio.h>

main(void)
{
  uint_fast16_t foo;
  uintmax_t i = UINTMAX_MAX;

  sscanf("123", "%" SCNuFAST16, &foo);
  printf("foo is %" PRIoFAST16 ".\n", foo);

  return 0;
}

덧붙여 질문 [*]1.1, [*]11.I, [*]12.A도 참고하시기 바랍니다.

References
[C99] § 7.8.1, § 7.18.1


12.4 scanf Problems

Though it seems to be an obvious complement to printf, scanf has a number of fundamental limitations that lead some programmers to recommend avoiding it entirely.



Q 12.17
scanf("%d\n", ...)을 썼더니, 추가적인 줄을 입력하기 전까지는 멈춰버립니다. 왜 그럴까요?
Answer
놀랍게도 scanf 포맷 문자열의 \n은 newline에 해당하는 것이 아니고, 공백(whitespace) 문자가 나오는 동안, 입력을 읽어서 무시하라는 뜻입니다. (사실, scanf format string에 나오는 어떠한(any) 공백 문자라도, 공백 문자를 읽어서 무시하라는 뜻입니다. 게다가 %d와 같은 format도 역시 공백 문자를 무시합니다. 따라서 일반적으로 여러분이 공백 문자를 scanf format string에 쓸 이유는 없습니다.

따라서 "%d\n"\nscanf가 공백 문자가 아닌 문자가 나올 때 까지 읽으라는 뜻이 되고, 따라서 공백 문자가 아닌 문자를 찾기 위해 다음 줄까지 읽어 버리는 상황이 된 것입니다. 이 경우 단순히 "%d"만 쓰면 해결됩니다. (물론 여러분의 프로그램은 수치를 입력받은 다음에 나오는 \n을 처리해야 합니다. 질문 [*]12.18을 참고하기 바랍니다.)

scanf 함수는 free-format input을 처리하기 위해 디자인되었습니다. 따라서 키보드에서 사용자에게 입력받기 위한 목적으로 쓰이는 경우는 거의 없습니다. “Free format”은 scanf\n를 다른 공백문자와 똑같이 취급한다는 것을 뜻합니다. 따라서 "%d %d %d" format은 입력이

  1 2 3
인 경우나
  1
  2
  3
인 경우 모두 쓸 수 있습니다.

(다른 언어들과 비교해 보면, C, Pascal, 그리고 LISP이 free-format이며, traditional BASIC과 FORTRAN이 free-format이 아닙니다.)

여러분이 꼭 scanf가 newline을 다르게 처리하게 만들고 싶다면, “scanset” directive를 쓰면 됩니다:

  scanf("%d%*[\n]", &n);
Scanset은 강력하기는 하지만, scanf의 모든 문제를 해결해 주지는 않습니다. 덧붙여 질문 [*]12.20도 참고하시기 바랍니다.

References
[K&R2] § B1.3 pp. 245-6
[C89] § 7.9.6.2
[H&S] § 15.8 pp. 357-64



Q 12.18
scanf("%d", ...)를 써서 여러 수치를 읽은 다음, gets()를 써서 문자열을 읽으려 합니다:
  int n;
  char str[80];

  printf("enter a number: ");
  scanf("%d", &n);
  printf("enter a string: ");
  gets(str);
  printf("You typed %d and \"%s\"\n", n, str);

그런데 컴파일러는 gets() 호출을 무시합니다! 왜 그런가요?

Answer
여러분이 위의 프로그램에 데이터를 다음과 같이 입력했다고 가정해 봅시다:
  42
  a string
이 때 scanf는 42를 읽지만, 그 뒤에 나오는 newline을 읽지 않습니다. 이 newline은 input stream에 남아 있고, gets()가 바로 이 newline을 읽게 됩니다. gets()는 newline이 나올 때까지의 입력을 문자열로 리턴해 주므로, 바로 빈 줄을 리턴하게 됩니다. 따라서 입력의 두번째 줄, “a string”은 아예 읽히지 않습니다. 만약 여러분이 다음과 같이 입력을 주었다면:
  42 a string
프로그램이 예상한 대로 동작할 것입니다.

일반적으로 scanf() 다음에 바로 gets()과 같은 다른 입력 루틴을 쓰는 것은 바람직하지 않습니다. scanf()의 이런 이상한(peculiar) 방식은 항상 문제를 일으킬 소지가 있기 때문입니다. 따라서 전체 입력을 모두 scanf로 받거나 아예 scanf를 쓰지 않는 것이 좋습니다.

덧붙여 질문 12.20, 12.23도 참고하시기 바랍니다.

References
[C89] § 7.9.6.2
[H&S] § 15.8 pp. 357-64



Q 12.19
scanf()의 리턴 값을 조사하면 사용자가 실제로 수치 값을 입력했는지 체크할 수 있기에 좀 더 안전하다고 배웠습니다:

  int n;

  while (1) {
    printf("enter a number: ");
    if (scanf("%d", &n) == 1)
      break;
    printf("try again: ");
  }
  printf("You typed %d\n", n);
그런데, 때때로 무한 루프에 빠지는 경우가 있더군요.12.9 왜 그럴까요?

Answer
scanf()가 수치 값을 처리할 경우, 수치가 아닌 문자가 나오게 되면 변환을 중지하고 그 문자 데이터를 입력 스트림에 그대로 놔 둡니다. 만약 이 상태에서 또 수치 값을 입력받으려고 scanf()를 부르게 되면, 문자 데이터가 있기 때문에 바로 중단합니다. 따라서 수치 값을 처리하는 scanf를 루프 안에 둘 경우, 잘못된 입력이 들어가게 되면 무한 루프에 빠지는 경우가 가끔 발생합니다. 예를 들어 사용자가 `x'와 같은 글자를 입력했고, scanf가 이를 %d나 %f와 같은 포맷으로 처리할 경우, 바로 중단하고, 다음 scanf 호출에선 또 `x'를 같은 방식으로 처리하게 되는 것입니다.

scanf가 처리할 수 없는 문자들을 input stream에 그대로 두는지 궁금하게 생각하실 것입니다. 여러분이 공백으로 구분되지 않은, 다음과 같은 입력을 처리해야 한다고 생각해보시기 바랍니다:

  123CODE
이 데이터를 scanf를 써서 처리하려 한다면, "%d%s"를 쓸 것입니다. 이 때 scanf가 매치되지 않은 문자를 input stream에 놓아 두지 않고 제거한다면, 여러분이 받은 데이터는 "CODE"가 아닌 "ODE"가 될 것입니다. (The problem is a standard one in lexical analysis: When scanning an arbitrary-length numeric constant or alphanumeric identifier, you never know where it ends until you've read “too far.” This is one reason that ungetc exists.)

덧붙여 질문 [*]12.20도 참고하시기 바랍니다.

References
[C89] § 7.9.6.2
[H&S] § 15.8 pp. 357-64



Q 12.20
왜 대부분의 사람들은 scanf()를 쓰지 말라고 할까요? 또 scanf()를 쓰지 않는다면 대신 어떤 함수를 써야 하죠?
Answer
scanf()는 많은 문제를 가지고 있습니다 -- 질문 [*]12.17, [*]12.18, [*]12.19를 참고하기 바랍니다. 또 gets()가 가지고 있는 문제가 %s 포맷을 사용할 때에도 발생합니다 (질문 [*]12.23 참고) -- 즉 입력받을 버퍼가 오버플로우가 일어나지 않는다고12.10 보장할 수 없습니다.

일반적으로, scanf()는 아주 형식화된(relatively structured, formatted) input을 위해 design된 것입니다. (이름부터 “scan formatted”의 약자입니다). 따라서 주의깊게 사용하면 작업이 실패했는지의 여부는 알려 주지만, 왜, 어떻게 작업이 실패했는지는 알려주지 않습니다. 따라서 scanf()에서 에러가 발생했다면, 이를 처리하기가 거의 불가능합니다.

User input은 가장 구조화되지 않은 input이라 할 수 있습니다. 잘 디자인된 user interface는 사용자가 어떤 문자라도 입력할 수 있다는 것을 염두에 두어야 합니다 -- 숫자가 필요한 경우에 문자나 구두점을 입력하는 단순한 경우를 포함해서, 사용자가 그냥 Return 키만 친다거나, 바로 EOF를 입력한다거나 하는 모든 경우를 말합니다. 이런 모든 경우를 scanf로 처리한 다는 것은 거의 불가능합니다; (fgets()와 같은 함수를 써서) 줄 전체를 입력 받은 다음, 이를 sscanf와 같은 함수를 써서 원하는 형태로 끊어내는(strtol, strtok, atoi와 같은 함수를 써서) 방법을 쓰는 것이 좋습니다; 덧붙여 질문 [*]12.16, [*]13.6도 참고하시기 바랍니다.) sscanf를 쓴다면, 리턴 값을 검사해서 원하는 만큼 입력을 처리했는지 확인하는 것이 중요합니다.

scanf의 이런 단점은 scanf에만 적용되는 것이지, fscanfsscanf와는 상관없다는 점을 아셔야 합니다. 문제가 되는 것은 표준 입력이 키보드로 연결되어 있고, scanf는 표준 입력을 처리하게 되어 있어서, least constrained인 사용자 입력을 scanf가 처리해버린다는 것입니다. 데이터 파일이 formatted인 경우, fscanf를 쓰는 것은 좋습니다. sscanf를 써서 줄을 parsing하는 것은 (리턴값을 검사하는 경우에) 매우 좋습니다. 왜냐하면, 입력을 취소하거나, 다시 입력을 검사하는 등 여러가지 작업을 할 수 있기 때문입니다.12.11

Note
User input은 제멋대로 들어올 수 있기 때문에, user input을 바로 atoi를 써서, 수치로 변경하는 것은 좋지 않습니다. 질문 21.11를 참고하기 바랍니다.
References
[K&R2] § 7.4 p. 159


12.5 Other stdio Functions



Q 12.21
sprintf 호출에서 필요한 버퍼의 크기를 미리 계산할 수 있는 방법이 있을까요? 즉, 어떻게 하면 sprintf()에서 버퍼 오버플로우를 피할 수 있을까요?
Answer
sprintf에 사용한 포맷이 상대적으로 간단하면 여러분은 필요한 버퍼의 크기를 미리 계산할 수 있습니다. 포맷에 하나 이상의 `%s'가 있다면 포맷 문자열에 사용한 고정된 문자들의 갯수를 직접 세어서 (또는 sizeof 연산자를 써서) 계산해서 그 결과를 집어넣을 문자열에 strlen을 취한 값과 더해서 계산할 수 있습니다. 예를 들면:
  sprintf (buf, "You typed \"%s\"", answer);
이 때의 버퍼 크기는 다음과 같이 계산합니다:
  int bufsize = 13 + strlen (answer);
정수에 대해서는 %d의 출력 결과에 필요한 문자 갯수가 다음 식을 넘지 않습니다:
  ((sizeof (int) * CHAR_BIT + 2) / 3 + 1)   /* +1 for '-' */
(CHAR_BIT<limits.h>에 정의되어 있음) 그러나 이런 식으로 매번 계산하는 것은 지나치게 조심스러운 것12.12입니다. (위의 식은 수치를 8 진수로 표기한다고 가정했을 때의 계산 결과입니다. 10 진법을 쓴다면 위의 계산 결과와 같거나, 그보다 작은 공간을 차지할 수 있습니다.)

포맷 문자열이 매우 복잡하거나 런타임이 되기 전에는 알 수 없다면 버퍼 크기를 예측하는 것은 sprinf()를 새로 만드는 것처럼 어려운 일입니다. 그리고 복잡한 만큼 에러가 발생하기 쉽습니다 (그래서 권장하지 않습니다). 마지막 방법은 일단 그 포맷과 같은 포맷을 fprintf()를 써서 임시 파일에 저장한 다음, fprintf()의 리턴값을 받거나 그 파일의 크기를 조사하는 방법입니다 (그러나 질문 [*]19.12를 꼭 읽기 바랍니다. 그리고 출력 에러를 생각해야 합니다).

주어진 버퍼가 크지 않을 경우, 오버플로우가 일어나지 않는다는 확실한 보장없이는 sprintf()를 부르기 꺼릴 것입니다. 포맷 스트링을 알 수 있다면 %s를 쓰는 대신 %.Ns(이 때 N은 어떤 수치)를 쓰거나 %.*s를 (덧붙여 질문 [*]12.10도 참고하시기 바랍니다.) 쓸 수 있습니다.

가장 확실한 오버플로우 방지책은 길이를 제한하는 snprintf()를 쓰는 것입니다. 실행 방법은 다음과 같습니다:

  snprintf(buf, bufsize, "You typed \"%s\"", answer);

snprintf()는 많은 stdio 라이브러리에서 (GNU와 4.4BSD 포함) 제공되고 있습니다. 그리고 이 함수는 [C9X] 표준에서 채택될 예정입니다.

[C9X] snprintf()를 쓸 수 있다면 배열의 크기를 미리 계산할 수 있습니다. [C9X] snprintf()는 버퍼에 기록한 문자의 수를 리턴하는 것이 아니라, 버퍼에 기록할 문자의 수를 리턴합니다. 게다가 이때 버퍼의 크기에 0, 또는 버퍼에 널 포인터를 전달할 수 있습니다. 그러므로 다음과 같은 호출을 사용하면:

  nch = snprintf (NULL, 0, fmtstring, /* 다른 인자들 */);
주어진 포맷 스트링(fmtstring)에 필요한 버퍼의 크기를 계산할 수 있습니다.
References
[C9X] § 7.13.6.6



Q 12.22
sprintf의 리턴 값이 왜 그리 문제인가요? int 타입을 리턴하나요, 아니면 char * 타입을 리턴하나요?

Answer
표준은 (printffprintf와 같이 write한 글자 수) int를 리턴한다고 말하고 있습니다. 그러나 예전의 어떤 라이브러리는 sprintf가 첫 번째 인자로 들어온 char *를 리턴합니다 (i.e. strcpy의 리턴값과 같은 목적).

References
[ANSI] § 4.9.6.5
[C89] § 7.9.6.5
[PCS] § p. 175



Q 12.23
왜 모두들 gets()를 쓰지 말라고 할까요?
Answer
fgets()와는 달리 gets()는 버퍼의 크기를 지정하지 않습니다. 따라서 버퍼 오버플로우를 방지할 길이 없습니다 -- 머피의 법칙에 의하면 예상되는 입력보다 큰 값이 언젠가는 들어오게 됩니다.12.13 일반적으로는 항상 fgets()를 써야 합니다. (물론 입력은 어떤 특정한 크기 이상 들어올 수 없지만, 실수할 경우를 방지하기 위해12.14, 항상 fgets를 쓰는 것이 좋습니다.)

fgetsgets의 다른 점 하나는, fgets는 문자열 마지막에 \n을 제거하지 않는다는 것입니다. 그러나 이 newline을 없애기는 아주 쉽습니다. 질문 [*]7.1을 보면 gets 대신 fgets()를 사용한 코드를 볼 수 있습니다.

Note
머피의 법칙(Murphy's Law)에 관한 것은 [Raymond]를 참고하기 바랍니다.

줄의 길이에 상관없이 한 줄을 완벽하게 읽을 수 있는 getline 함수는 질문 [*]21.6을 참고하기 바랍니다.

References
[ANSI Rationale] § 4.9.7.2
[H&S] § 15.7 p. 356



Q 12.24
printf를 호출한 다음에 errno를 검사해서 printf가 실패했는지 검사했다고 생각합니다:

  errno = 0;
  printf("This\n");
  printf("is\n");
  printf("a\n");
  printf("test.\n");
  if (errno != 0)
    fprintf(stderr, "printf failed: %s\n", strerror(errno));

그런데 왜 “printf failed: Not a typewriter”라는 이상한 메시지를 출력할까요?

Answer
많은 stdio 패키지들은 stdout이 터미널일 경우는 특별하게 취급합니다. 따라서 stdout이 터미널인지 검사하기 위해서, 내부적으로 어떠한 연산을 하게 되며, 그 결과 stdout이 터미널이 아닌 경우에는 errnoENOTTY로 설정합니다. 따라서 printf가 성공적으로 수행되었다 하더라도, errno에 설정한 값은 그대로 남게 됩니다. 이러한 일은 조금 혼동스럽기는 하지만, 틀렸다고 말할 수는 없습니다. 왜냐하면 errno의 값은 에러가 난 다음에만 의미를 가지기 때문입니다. (좀 더 정확히 말한다면, errno는, errno를 셋팅하는 라이브러리 함수가 에러 코드를 리턴한 경우에만 의미가 있습니다 (meaningful).) 일반적으로 일단 함수의 리턴 값을 검사해야 합니다. 여러 stdio 함수를 호출한 다음 그 동안 연산의 에러를 알고 싶다면 ferror 함수를 쓰면 됩니다. 덧붙여 질문 [*]12.2, [*]20.4도 참고하시기 바랍니다.

Note
에러가 발생하지 않았을 때에는 errno가 0이라는 것을 보장할 수 없습니다.
References
[C89] § 7.1.4, § 7.9.10.3
[CT&P] § 5.4 p. 73
[PCS] § 14 p. 254



Q 12.25
fgetpos/fsetposftell/fseek이 차이가 있나요? fgetpos/fsetpos는 어디에 쓰는 거죠?
Answer
새로운 fgetpos/fsetpos 함수는 파일 offset을 나타내기 위해, typedeffpos_t를 사용합니다. 따라서 올바르게 만들어졌다면 아무리 큰 파일이더라도 offset을 표현할 수 있습니다. 이와는 달리 ftellfseeklong int를 쓰기 때문에, offset의 범위가 long int의 범위로 제한됩니다. (long int type은 $2^{31}-1$보다 큰 수를 가진다고 보장할 수 없습니다. 따라서 파일의 옵셋이 20억 바이트 ($2^{31}-1$)로 제한됩니다. 또한 fgetpos/fsetpos는 다중바이트(multibyte) 스트림에도 쓸 수 있습니다. 덧붙여 질문 [*]1.4도 참고하시기 바랍니다.
References
[K&R2] § B1.6 p. 248
[C89] § 7.9.1, § 7.9.9.1, 7.9.9.3
[H&S] § 15.5 p. 252



Q 12.26
입력을 방출(flush)시킬 방법이 있을까요? 즉 사용자가 미리 입력한 입력을 무시해서, 다음 프롬프트를 출력할 때 이 입력이 출력되지 않도록 하고 싶습니다. fflush(stdin)을 쓰면 될까요?
Answer
C 언어 표준에 따르면, fflush()는 output stream에 대해서만 동작합니다. “flush”라는 정의가 buffering된 문자들을 쓰는 것을 완료시킨다는12.15 것이므로 문자를 취소시키는 것(discard)과는 아무런 관계가 없습니다. 따라서 입력 스트림의 입력을 무시하는 기능과는 전혀 상관이 없습니다.

아직 읽지 않은 문자(unread characters)들을 stdio input stream에서 무시하는(discard) 표준 방법은 존재하지 않습니다. 어떤 컴파일러 회사들은 fflush(stdin)이 읽지 않은 문자들을 취소할 수 있도록 라이브러리를 제공하기도 하지만, 이식성이 뛰어난 프로그램을 만들려면 절대로 써서는 안되는 기능입니다. (어떤 stdio 라이브러리는 fpurgefabort를 같은 기능으로 제공하기도 하지만, 마찬가지로 표준은 아닙니다.) 또한 input buffer를 flush하는 것이 꼭 해결책이라고 할 수도 없습니다. 왜냐하면 일반적으로 아직 읽히지 않은 입력은 OS-level input buffer에 존재하기 때문입니다.

Input을 flush할 방법이 필요하다면 (질문 [*]19.1과 [*]19.2에 나온 것처럼) 시스템 의존적인 방법을 찾아야 할 것입니다. 게다가 사용자가 매우 빠른 속도로 입력하고 있고, 여러분의 프로그램이 그 입력을 무시해버릴 수 있다는 것을 꼭 염두에 두어야 합니다.

또는 \n이 나오기 전까지 문자를 읽어서 무시하거나, curses에서 제공하는 루틴인 flushinp()를 쓰면 됩니다. 덧붙여 질문 [*]19.1, [*]19.2도 참고하시기 바랍니다.

References
[C89] § 7.9.5.2
[H&S] § 15.2


12.6 Opening and Manipulating Files



Q 12.27
다음과 같이 파일을 여는(open) 함수를 만들었습니다:
  myfopen(char *filename, FILE *fp)
  {
    fp = fopen(filename, "r");
  }

그런데 다음과 같이 호출하고 나면:

  FILE *infp;
  myfopen("filename.dat", infp);

infp 변수가 제대로 설정(setting)되지 않습니다. 왜 그럴까요?

Answer
C 언어에서 함수들은 항상 자신에게 전달된 인자의 사본을 받습니다. 따라서 함수는 절대로 자신에게 전달된 인자에 값을 지정(assignment)하는 것으로 값을 리턴할 수 없습니다. 질문 [*]4.8을 참고하기 바랍니다.

주어진 질문을 고치려면 다음과 같이 myfopen 함수가 FILE *를 리턴하도록 고치면 됩니다:

  FILE *
  myfopen(char *filename)
  {
    FILE *fp = fopen(filename, "r");
    return fp;
  }

그 다음 다음과 같이 씁니다.

  FILE *infp;
  infp = myfopen("filename.dat");

또는 다음과 같이 myfopenFILE *에 대한 포인터12.16를 인자로 받게하면 됩니다:

  myfopen(char *filename, FILE **fpp)
  {
    FILE *fp = fopen(filename, "r");
    *fpp = fp;
  }

그리고 다음과 같이 씁니다:

  FILE *infp;
  myfopen("filename.dat", &infp);



Q 12.28
아주 간단한 fopen 조차도 동작하지 않습니다. 아래 코드에서 잘못된 점이 있나요?
  FILE *fp = fopen(filename, 'r');

Answer
문제는 fopen의 두 번째 인자인 mode는 "r"과 같은 string이어야지, 'r'과 같은 문자가 아니라는 것입니다. 덧붙여 질문 [*]8.1도 참고하시기 바랍니다.



Q 12.29
왜 절대 경로를 써서 파일을 열면 항상 실패할까요? 다음 코드는 동작하지 않더군요:
  fopen("c:\newdir\file.dat", "r");

Answer
두 개의 backslash 문자를 써야 할 것입니다. 질문 [*]19.17을 참고하기 바랍니다.



Q 12.30
파일의 내용을 고치려고 합니다. 일단 "r+" 모드로 파일을 연 다음, 원하는 문자열을 읽고 다시 이 문자를 쓰려고 했으나 제대로 동작하지 않습니다.
Answer
쓰기 전에 fseek 함수를 불렀는지 검사해보기 바랍니다. 읽은 다음에, 그 위치에 쓰기 위해서는, 읽은 만큼 파일 위치를 앞쪽으로 옮겨 주어야 합니다. 즉, read/write "+" 모드로 연 파일에서 읽기(reading)나 쓰기(writing)을 한 다음에는 반드시 fseekfflush를 써 주어야 합니다. 또 덮어쓰기 위해서는 기존의 문자열과 덮어 쓸 문자열의 길이가 같아야 합니다; 특정 위치에 문자들을 추가하거나(insert) 지우는(delete) 방법은 존재하지 않습니다. 텍스트 파일에서 덮어쓰는 작업을 하면, 파일을 덮어쓴 위치까지 잘려버릴 수도 있습니다12.17. 덧붙여 질문 [*]19.14도 참고하시기 바랍니다.

References
[ANSI] § 4.9.5.3
[C89] § 7.9.5.3



Q 12.31
파일 중간에 한 줄을 (또는 레코드) 추가하거나 지우는 방법이 있나요?
Answer
질문 [*]19.14를 참고하기 바랍니다.



Q 12.32
열려진 stream에서 파일 이름을 다시 얻어낼 수 있습니까?
Answer
질문 [*]19.15를 참고하기 바랍니다.


12.7 Redirecting stdin and stdout



Q 12.33
프로그램 안에서 stdin이나 stdout을 파일로 `redirect'할 수 있을까요?
Answer
freopen() 함수를 쓰기 바랍니다. f()라는 함수가 있고, 이 함수는 보통 stdout으로 데이터를 출력한다고 가정해 봅시다. 이런 경우에는 단순히 freopen 함수를 쓰면 됩니다:

  freopen(file, "w", stdout);
  f();

(그러나 질문 [*]12.34를 꼭 읽어보시기 바랍니다).

References
[ANSI] § 4.9.5.4
[C89] § 7.9.5.4
[H&S] § 15.2. pp. 347-8



Q 12.34
일단 freopen()을 쓴 다음, 다시 원래의 stdout으로 (또는 stdin으로) 복귀할 수는 없나요?
Answer
좋은 해결책이 없습니다. 다시 돌아올 필요가 있을 때, freopen()을 쓰는 것은 별로 좋은 방법이 아닙니다. 차라리 따로 output (또는 input) stream 변수를 만들어서 작업해서 stdout을 (또는 stdin) 건드리지 않는 것이 좋습니다. 예를 들어 다음과 같이 전역 변수를 만들고:

  FILE *ofp;

printf(...)를 전부 fprintf(ofp, ...)로 바꿉니다. (물론, putcharputs를 썼다면 이 함수들도 바꾸어야 할 것입니다.) 그런 다음, ofpstdout를 가리키도록 하는 등의 작업을 하면 됩니다.

혹시 다음과 같은 코드로 freopen을 대신할 수 있다고 생각하실지도 모르겠습니다:

  FILE *savestdout = stdout;
  stdout = fopen(file, "w");    /* WRONG */

그 다음, 아래 코드로 stdout을 복원할 수 있다고 생각하실지도 모르겠습니다:

  stdout = savestdout;          /* WRONG */

이러한 코드는 동작한다고 말하기 어렵습니다. 왜냐하면 stdout은 (또 stdin, stderr) 보통 상수(constant)이기 때문에 다른 값으로 assign할 수 없기 때문입니다. (그렇기 때문에 freopen이라는 함수가 제공되는 것입니다.)

원래의 스트림에 관한 정보를 freopen()을 쓰기 전에 복사해 두었다가 복원시키는 방법도 있지만, 이는 시스템 의존적인, dup()과 같은 함수를 쓰기 때문에 이식성이 (portability) 떨어지며, 비안정적입니다 (unreliable.)

어떤 시스템에서는 controlling terminal을 (질문 [*]12.36 참고) 직접 open하는 방법을 제공하기도 하지만, 이는 여러분이 원하는 것이 아닐 것입니다. 왜냐하면 원래의 input 또는 output이 (즉 freopen을 부르기 전의 stdin이나 stdout의 값) command line에서 redirect되어 들어올 수 있기 때문입니다.

어떤 subprogram을 실행시키고 그 결과를 capture하고 싶다면, freopen은 제대로 동작할 수 없습니다; 질문 [*]19.30을 대신 참고하기 바랍니다.



Q 12.35
프로그램 안에서 표준 입력이나 표준 출력이 redirect되었는지 확인할 수 있을까요? 다시 말해 command line에서 “<”나 “>”을 썼는지 확인할 수 있을까요?

Answer
먼저 무엇 때문에 그러한 방법이 필요한 것인지 잘 생각해 보시기 바랍니다. 만약 프로그램에 전달한 입력 파일이 없을 때, stdin으로 데이터를 받고자 한다면 간단히 argv를 써서 할 수 있습니다 (질문 [*]20.3 참고). 또는 파일 이름을 쓰는 대신 “-”와 같은 placeholder를 쓰게 만들 수 있습니다. 입력이 interactive terminal에서 온 경우가 아닌 경우, prompt를 출력하게 않도록 만들고 싶다면, 시스템에 따라서 (예를 들어 UNIX나 MS-DOS에서) isatty(0)이나 isatty(fileno(stdin))을 써서 terminal인지 아닌지 알아낼 수 있습니다.



Q 12.36
“more”와 같은 프로그램을 만들려고 합니다. stdin이 redirect되었다면 어떻게 keyboard를 다시 쓸 수 있을까요?

Answer
이런 일을 할 수 있는 portable한 방법은 없습니다. UNIX에서는 special file인 /dev/tty를 열면 됩니다. 또, MS-DOS에서는, 파일 CON을 열거나, getch처럼 BIOS call을 이용하는 루틴을 쓰면 input이 redirect되었는지와는 상관없이 keyboard를 쓸 수 있습니다.



Q 12.36b
출력을 동시에 두곳으로, 예를 들면 스크린과 파일로 보낼 수 있는 방법이 있을까요?
Answer
없습니다. 대신 여러분이 모든 것을 두 번 반복하는 printf 스타일의 함수를 만들어 쓰면 됩니다. 질문 [*]15.5를 참고하기 바랍니다.


12.8 “Binary” I/O

보통의 stream은 출력할 수 있는 text로 이루어져 있고, 저수준의 운영체제에서 쓰는 어떤 convention에 맞도록 변환(translation)을 할 수 있는 것으로 여겨지고 있습니다. 이러한 translation없이, 정확히 바이트 단위로 read/write를 할려면 “binary” I/O를 써야 합니다.



Q 12.37
메모리와 파일에서, fprintffscanf가 쓰는 formatted string이 아닌, 한 번에 한 바이트씩 읽거나 쓰려고(write) 합니다. 어떻게 하면 될까요?
Answer
이런 일을 대개 “binary” I/O라고 부릅니다. 먼저 fopen을 부를때 ("rb""wb"와 같은) "b" modifier를 썼는지 (질문 [*]12.38 참고) 확인하기 바랍니다.

그 다음, &sizeof operator를 써서 몇 바이트를 transfer할 것인지 확인하기 바랍니다. 보통 freadfwrite같은 함수를 써서 작업할 수 있습니다; 예제는 질문 [*]2.11을 보기 바랍니다.

freadfwrite 자체가 binary I/O를 수행하는 함수는 아닙니다. 일단 binary mode로 파일을 열었다면, 어떤 I/O 함수라도 binary I/O를 할 수 있습니다 (질문 [*]12.42의 예제 참고); Text mode로 파일을 열었다해도 원한다면 freadfwrite를 쓸 수 있습니다.

마지막으로, binary data file은 portable하지 않습니다; 질문 [*]20.5 참고. 덧붙여 질문 [*]12.40도 참고하시기 바랍니다.



Q 12.38
바이너리 데이터 파일을 적절하게 읽을 수 있는 방법을 알려주세요. 때때로 0x0a와 0x0d 값이 멋대로 들어오고 데이터가 0x1a을 포함할 때 EOF가 리턴되는 경우가 있습니다.

Answer
Binary data를 읽어 들이려면 fopen()을 호출할 때 "rb" mode를 써서 텍스트 파일 변환(text file translation)이 일어나지 않도록 해야 합니다. 비슷하게, 바이너리 데이터를 쓰기 위해서는 "wb"를 써야 합니다. (UNIX와 같이 text와 binary 파일을 구별하지 않는 운영체제에서는 "b"를 쓸 필요는 없지만, 썼다고 하더라도 문제될 것은 없습니다.)

텍스트/바이너리의 차이는 파일을 열 때에만 적용됩니다. 일단 파일이 열린 다음에는 어떤 I/O 루틴을 써도 상관없습니다. 덧붙여 질문 [*]12.40, [*]12.42, [*]20.5도 참고하시기 바랍니다.

References
[ANSI] § 4.9.5.3
[C89] § 7.9.5.3
[H&S] § 15.2.1 p. 348



Q 12.39
Binary file에 쓰기 위한 일종의 “filter”를 만들고 있습니다. 그런데 stdinstdout은 이미 text stream으로 열려 있는 상태라서 문제가 됩니다. 어떻게 하면 이 stream을 binary mode로 바꿀 수 있을까요?
Answer
이런 일을 하기 위한 표준 방법은 없습니다. UNIX와 같은 OS는 text와 binary를 따로 구별하지 않으므로, mode를 바꿀 필요가 없습니다. 어떤 MS-DOS 컴파일러는 setmode라는 함수를 제공하기도 합니다. 다른 OS의 경우에는 여러분 스스로 찾아야 할 것입니다.



Q 12.40
Text I/O와 binary I/O의 차이점이 뭔가요?
Answer
Text mode의 파일은 출력 가능한(printable) 문자로 (탭 같은 문자 포함) 이루어져 있는 것이 상식입니다. Stdio 라이브러리에 있는 함수는 (getc, putc와 같은 모든 함수 포함) OS에서 쓰는 end-of-line 문자 표현 방법을 하나의 \n 문자로 바꾸어서 처리합니다. 따라서 C 프로그램에서 OS에서 사용하는 end-of-line 문자 표현 방식을 신경쓸 필요가 없습니다: C 프로그램에서 '\n' 문자를 쓰면(write), stdio 라이브러리는 이 문자를 OS에서 사용하는 end-of-line 문자 표현 방식으로 바꾸어 쓰며(write), 읽을 때에는 OS가 사용하는 end-of-line 문자 표현 방식을 보면 단순히 \n을 프로그램에 돌려줍니다.12.18

이와는 달리 binary mode에서는 프로그램에서 읽고 쓰는(write) 바이트와 파일 저장 방식과 완전히 같습니다. 즉 어떠한 변환도 이루어지지 않습니다. (MS-DOS 시스템에서는 control-Z를 end-of-file 문자로 해석하는 것도 이루어지지 않습니다.)

Text mode translation은 파일의 크기에도 영향을 끼칩니다. 왜냐하면 프로그램에서 처리하는 문자가 file에 저장될 문자와 1:1로 match될 필요가 없기 때문입니다. 같은 이유에서 fseekftell이 파일에서 정확한 byte offset을 나타낼 이유도 없습니다. (엄격히 말해서 text mode에서는 fseekftell이 리턴하는 값을 프로그래머가 직접 해석하려고 하면 안됩니다: ftell이 리턴하는 값은 fseek의 인자로만 써야 하며, 또한 ftell이 리턴하는 값만이 fseek의 인자로 쓰여야 합니다.)

Binary mode에서는 fseekftell이 byte offset을 나타냅니다. 그러나 어떤 시스템에서는 완전한 레코드를 만들기 위해 여러 개의 null byte를 파일 뒤에 추가할 수도 있습니다.

덧붙여 질문 [*]12.37, [*]19.12도 참고하시기 바랍니다.

References
[ANSI] § 4.9.2; [C89] § 7.9.2
[ANSI Rationale] § 4.9.2
[H&S] § 15 p. 344, § 15.2.1 p. 348



Q 12.41
Structure를 어떻게 파일에서 읽거나(read) 쓸(write) 수 있을까요?
Answer
질문 [*]2.11을 참고하기 바랍니다.



Q 12.42
오래된 binary data file에서 data를 읽어오려면 어떻게 해야 할까요?
Answer
Word size와 byte-order 차이, floating-point format, structure padding 때문에 어렵습니다. 이런 것들을 극복하려면 아마도 바이트 단위로 data를 읽거나 써야(write) 할지도 (shuffling 또는 rearranging한 다음) 모릅니다. (이런 작업이 항상 나쁜 것은 아닙니다. 왜냐하면 코드의 portability를 높여주며, 여러분이 확실히 제어할 수 있기 때문입니다.)

예를 들어 여러분이 문자와 32-bit integer, 16-bit integer로 이루어진 structure를 주어진 fp에서 다음 structure 안으로 읽을 필요가 있다고 가정해 봅시다:

  struct mystruct {
    char c;
    long int i32;
    int i16;
  }

다음과 같은 코드를 쓰면 해결될 것입니다:

  s.c = getc(fp);

  s.i32 = (long)getc(fp) << 24;
  s.i32 |= (long)getc(fp) << 16;
  s.i32 |= (unsigned)(getc(fp) << 8);
  s.i32 |= getc(fp);

  s.i16 = getc(fp) << 8;
  s.i16 |= getc(fp);

이 코드는 getc가 8-bit 문자를 읽고 데이터가 최상위 바이트가 우선하여 (most significant byte first, “big endian”) 저장되어 있다고 가정했습니다. (long) 타입으로 casting한 것은 16-bit 또는 24-bit shift 연산이 long 값에 확실히 동작하도록 (질문 [*]3.14 참고) 보장해주며, (unsigned)로 casting한 것은 부호 확장(sign extension)을 막기 위한 것입니다. (일반적으로 이러한 코드를 작성할 때에는 모든 data type을 unsigned로 하는 것이 좋습니다. 그러나 먼저 질문 [*]3.19를 참고하기 바랍니다.)

Structure의 내용을 파일에 쓰는(write) 코드는 다음과 같습니다:

  putc(s.c, fp);

  putc((unsigned)((s.i32 >> 24) & 0xff), fp);
  putc((unsinged)((s.i32 >> 16) & 0xff), fp);
  putc((unsigned)((s.i32 >> 8) & 0xff), fp);
  putc((unsigned)(s.i32 & 0xff), fp);

  putc((s.i16 >> 8) & 0xff, fp);
  putc(s.i16 & 0xff, fp);

덧붙여 질문 [*]2.12, [*]12.38, [*]16.7, [*]20.5도 참고하시기 바랍니다.

Seong-Kook Shin
2018-05-28