Subsections


13. Library Functions

예전에는, 특정 run-time library는 C 언어의 표준의 한 부분이 아니었습니다. ANSI/ISO Standard C의 출현으로 대부분의 전통적인 (traditional) run-time library는 (Chapter 12의 stdio 함수들을 포함) 표준이 되었습니다.

특별히 중요한 라이브러리 함수들은 각각 다른 section에 설명되었습니다; 메모리 할당에 관련된 mallocfree에 관한 것은 7 장에 설명되었으며, <stdio.h>에 나온 “standard I/O” 함수들은 Chapter 12에 설명되었습니다. 이 chapter는 다음과 같이 나누어집니다:

String Function: [*]13.1- [*]13.7
Sorting: [*]13.8- [*]13.11
Date and Time: [*]13.12- [*]13.14
Random Numbers: [*]13.15- [*]13.21
Other Library Functions: [*]13.22- [*]13.28

마지막 몇 개의 질문들은 ( [*]13.25부터 [*]13.28까지) link시 문제가 되는 (예를 들면, “undefined external” 에러) 것을 다루었습니다.

13.1 String Functions



Q 13.1
수치를 문자로 바꾸고 싶은데 (atoi 함수의 반대 기능), itoa()라는 함수가 있나요?
Answer
그냥 sprintf()를 쓰시면 됩니다. (sprintf가 너무 복잡하고, 큰 비용(시간이나 크기 면에서)이 든다고 생각할 지 모르나, 사실 효과적으로 동작합니다.) 질문 [*]7.5a에 대한 답변에 나온 예를 보기 바랍니다. 질문 [*]12.21을 참고하기 바랍니다.

long이나 실수도 sprintf()를 써서 바꿀 수 있습니다 (각각 %ld%f를 쓰면 됨); 즉, sprintfatol이나 atof의 반대 역할을 하는 함수라고 생각할 수 있습니다. 게다가, 어떤 식으로 출력할 것인지도 제어할 수 있습니다. (그렇기 때문에, C 언어는 itoa 식의 함수보다, sprintf를 제공하는 것입니다.)

itoa를 직접 만들어야 한다면, 다음과 같은 사항들을 고려해야 합니다:

질문 [*]12.21과 [*]20.10도 참고 바랍니다.

References
[K&R1] § 3.6 p. 60
[K&R2] § 3.6 p. 64

Note
C99 표준에 새로 추가된 long long 타입을 쓰려면, ll length modifier를 써야 합니다. 즉 long을 출력하기 위해 %ld를 쓴 것처럼, %lld를 씁니다.

마찬가지로, size_t 타입의 값을 쉽게 출력할 수 있도록 printf, scanf 형태의 함수에 z modifier를 추가했습니다. 정수 출력에 쓰이는 conversion인, d, i, o, u, x, Xz를 붙여서 size_t를 쉽게 출력할 수 있습니다. 예를 들면:

  size_t sz;
  ...
  printf("%zu\n", sz);
References
[C99] § 7.19.6.1, § 7.19.6.2
[H&S2002] § 15.11.6



Q 13.2
strncpy()는 대상 문자열에 항상 \0을 써 주지 않는 것일까요?
Answer
strncpy()의 원래 개발 목적은 고정 크기를 갖고, \0으로 끝나지 않는 “문자열”13.1도 처리할 수 있게 하는 것이 그 목적이었습니다. 따라서, 다른 목적으로 strncpy()를 쓸 때에는 좀 번거로운 작업이 필요합니다; (strncpy()의 한 가지 단점은 여분의 공간에 0을 계속 기록한다는 것입니다.) 복사한 문자열이 저장된 버퍼의 끝에 수동으로 '\0'을 채워주어야 할 필요가 있습니다. 그래서 그리 자주 쓰이는 함수가 아닙니다. 그래서, strncpy() 대신 strncat을 쓰기도 합니다: 대상 버퍼가 비어있는 경우, strncat은 여러분이 strncpy에서 기대한 작업을 수행해 줍니다:
  *dest = '\0';
  strncat(dest, source, n);
위 코드는 최대 n개의 문자를 복사하며, 항상 마지막에
0
을 붙여 줍니다.

또 다른 방법은 다음과 같습니다:

  sprintf (dest, "\%.*s", n, source);
정확하게는 위 코드는 n이 509 이하일때에만 동작합니다.

문자열이 아닌 어떤 크기의 바이트를 복사하고자 한다면 strncpy() 대신 memcpy()를 쓰는 것이 더 효과적입니다.



Q 13.3
다른 언어에서 제공하는 “substr” (문자열에서 부분 문자열을 빼내는) 함수가 있나요?
Answer
없습니다. (그 이유 중 하나는, C 언어에는 문자열을 취급하는 전용의 데이터 타입이 없기 때문입니다. 질문 [*]7.2와 Chapter 8 참고하기 바랍니다.)

원본 문자열의 POS번째에서 시작해서 길이가 LEN인 부분 문자열을 꺼내기 위해서 다음과 같이 할 수 있습니다:

  char dest[LEN + 1];
  strncpy(dest, &source[POS], LEN);
  dest[LEN] = '\0';           /* ensure \0 termination */
또, 질문 [*]13.2에서 다룬 것처럼 다음과 같이 할 수 있습니다:
  char dest[LEN + 1] = "";
  strncat(dest, &source[POS], LEN);
다음과 같이 포인터 형태로 할 수도 있습니다:
  strncat(dest, source + POS, LEN);
(정의에 의해, &source[POS]source + POS와 완벽히 같은 표현입니다; Chapter 6 참고)



Q 13.4
모든 문자열을 대문자로 또는 소문자로 바꾸려면 어떻게 하죠?
Answer
어떤 라이브러리들은 이 역할을 위해, struprstrlwr, 또는 strupperstrlower를 제공합니다만, 이식성도 없고, 표준도 아닙니다. 헤더 파일 <ctype.h>에 정의되어 있는 매크로인 touppertolower를 써서 이 기능을 수행하는 함수를 만들면 됩니다; 질문 [*]13.5를 참고 바랍니다. (매우 간단하지만, 만들 함수가 전달받은 문자열을 고칠 것이냐, 아니면 따로 공간을 할당하고 변경된 문자열을 그 곳에 저장할 것이냐는 미리 생각해 두어야 합니다; 질문 [*]7.5를 참고 바랍니다.)

(다국적(multinatinoal) 문자 셋을 쓸 때에는 문자 또는 문자열을 대문자 또는 소문자로 바꾸는 것은 더 복잡합니다.)



Q 13.5
어떤 버전의 toupper()에, 대문자를 인자로 줄 경우, 이상하게 동작합니다. 이런 것을 예상해서인지 toupper()를 부르기 전에 islower()를 부르는 코드도 보았습니다.
Answer
오래된 버전의 toupper()tolower()의 경우, 변환이 필요없는 값이 인자로 전달되면, 제대로 동작하지 않습니다. (즉 알파벳이 아닌 문자나, 또는 이미 변환이 된 알파벳인 경우). 그래서 오래된 (또는 이식성이 아주 높은) 코드는 toupper 또는 tolower를 부르기 전에, islowerisupper를 부르는 경향이 있습니다.

그러나, C 표준은 이러한 함수들이 어떤 값이 전달되더라도 안전하게 동작한다고 말하고 있습니다. 즉, 변경이 필요없는 문자들은 변경되지 않습니다.

References
[ANSI] § 4.3.2
[C89] § 7.3.2
[H&S] § 12.9 pp. 320-1
[PCS] p. 182



Q 13.6
문자열 내용을 공백 문자를 기준으로 구별된 단어들로 끊어내고 싶습니다. main()argcargv를 처리하는 것처럼 할려면 어떻게 해야 할까요?

Answer
단어 (“token”) 단위로, 문자열을 끊어주는 표준 함수 strtok()이 있습니다. 물론, 이 함수가 이러한 모든 일을 다 처리할 수 있는 것은 아닙니다. (예를 들어, 인용(quoting)된 문자열을 제대로 처리하기 힘듭니다.) 아래에 각각 필드를 얻어서 출력하는 예를 보입니다:
  #include <string.h>
  char string[] = "this is a test"; 
    /* not char *; see Q16.6 */
  char *p;
  for (p = strtok(string, " \t\n"); p != NULL;
                     p = strtok(NULL, " \t\n"))
    printf("\"%s\"\n", p);

또, 아래는 argv를 한꺼번에 만들어 주는 함수입니다:

  #include <ctype.h>

  int
  makeargv(char *string, char *argv[], int argvsize)
  {
    char *p = string;
    int i;
    int argc = 0;

    for (i = 0; i < argvsize; i++) {
      /* skip leading whitespace */
      while (isspace(*p))
        p++;
      if (*p != '\0')
        argv[argc++] = p;
      else {
        argv[argc] = 0;
        break;
      }

      /* scan over arg */
      while (*p != '\0' && !isspace(*p))
        p++;
      /* terminate arg: */
      if (*p != '\0' && i < argvsize - 1)
        *p++ = '\0';
    }
    return argc;
  }
makeargv는 다음과 같이 호출합니다:
  char *av[10];
  int i, ac = makeargv(string, av, 10);
  for (i = 0; i < ac; i++)
    printf("\"%s\"\n", av[i]);
만약 구분(separator) 문자가 중요하다면 -- 즉, 두 개의 탭이 동시에 나올 때, 한 필드가 생략되었다는 것을 알리고 싶으면 -- strchr 함수를 쓰는 것이 더 편할 수 있습니다:
  #include <string.h>

  char *p = string;

  while (1) {     /* break in middle */
    char *p2 = strchr(p, '\t');
    if (p2 != NULL)
      *p2 = '\0';
    printf("\"%s\"\n", p);
    if (p2 == NULL)
      break;
    p = p2 + 1;
  }

여기에 나온 모든 코드는 원본 문자열에, 각 필드가 끝날 때마다
0
을 넣어서 각 필드를 구분할 수 있게 만듭니다. 따라서 원본 문자열이 나중에 다시 필요하다면, 이 곳에 있는 코드를 실행하기 전에 먼저 복사해 두어야 합니다.

References
[K&R2] § B3 p. 250
[ANSI] § 4.11.5.8
[C89] § 7.11.5.8
[H&S] § 13.7 pp. 333-4
[PCS] p. 178



Q 13.7
와일드 카드(wildcard)와 정규식(regular expression)을 처리하려고 합니다.
Answer
다음과 같은 차이점을 먼저 알아두기 바랍니다:

정규식 매칭에 사용되는 패키지는 많습니다. 그리고 대부분의 패키지들은 두 가지 함수를 사용합니다: 하나는 정규식을 “컴파일(compile)”하기 위한 것이고, 다른 하나는 이 “컴파일된” 정규식과 문자열을 비교해주는(execute) 함수입니다. 먼저 헤더 파일 <regex.h><regexp.h>가 있는지, 그리고 regcmp/regex, regcomp/regexec, re_comp/re_exec 함수가 존재하는 지 (이 함수들은 대개 각각의 독립적인 라이브러리 형태로 제공됩니다.) 체크해보시기 바랍니다. 인기있고, 공개인 Henry Spencer씨의 regexp 패키지를 ftp.cs.toronto.edu에서 pub/regexp.shar.Z로 얻을 수 있습니다. GNU 프로젝트에서는 rx라고 하는 패키지를 제공합니다. 덧붙여 질문 [*]18.16도 참고하시기 바랍니다.

Filename wildcard matching은 (때때로 “globbing”이라고 함) 각각의 시스템에 따라 다르게 이루어집니다. UNIX에서는 와일드 카드를 shell이 처리해 주므로, 각각의 프로그램들은 이 와일드 카드에 대해 전혀 고려하지 않아도 됩니다. 그러나 MS-DOS에서는 shell이라 할 수 있는 command interpreter가 이를 처리해 주지 않으므로, (컴파일러가 제공하는) 와일드 카드를 처리해주는 특별한 오브젝트 파일과 함께 링크해야 합니다. (이 오브젝트 파일은 agrv를, 와일드 카드 처리한 다음의 상태로 만들어 줍니다) 그리고 (MS-DOS와 VMS를 포함한) 대부분의 시스템에서는 와일드카드로 파일을 리스팅해주거나, 파일을 open하는 시스템 서비스들을 제공합니다. 먼저 컴파일러/라이브러리의 문서를 참고하기 바랍니다. 덧붙여 질문 [*]19.20, [*]20.3도 참고하시기 바랍니다.

아래는 Arjan Kenter씨가 제공한 간단한 형태의 wildcard를 처리하는 함수입니다:

  int match(char *pat, char *str)
  {
    switch (*pat) {
    case '\0':  return !*str;
    case '*':   return match(pat + 1, str) ||
                       *str && match(pat, str + 1);
    case '?':   return *str && match(pat + 1, str + 1);
    default:    return *pat == *src && 
                       match(pat + 1, str + 1);
  }
이 함수를 match("a*b.c", "aplomb.c")로 부르면, 1을 리턴합니다.
References
Schumacher, ed., Software Solutions in C § 3 pp. 35-71

13.2 Sorting



Q 13.8
qsort() 함수를 써서 문자열의 배열을 (array of strings) 정렬하려고 합니다. strcmp() 함수를 비교 함수로 사용했는데 동작하지 않습니다.
Answer
질문하신 “문자열의 배열 (array of strings)”은 아마도 “문자에 대한 포인터의 배열 (array of pointers to char)”를 의미한 것일 것입니다. qsort에 전달되는 비교 함수는 정렬하고자 하는 오브젝트에 대한 포인터를 인자로 받아야 합니다. 이 경우, 문자에 대한 포인터를 가리키는 포인터를 입력 받아야 합니다. 그런데 strcmp()는 단순히 문자를 가리키는 포인터를 입력 받습니다. 그러므로 strcmp()를 직접 비교 함수로 사용할 수는 없습니다. 다음과 같이 wrapper 함수를 만들어야 합니다:

  /* compare strings via pointers */
  int pstrcmp(const void *p1, const void *p2)
  {
    return strcmp(*(char * const *)p1, *(char * const *)p2);
  }

비교 함수의 인자는 “범용 포인터”인 const void *이어야 합니다. 그리고 함수 내부에서 이를 원하는 형태로 (예를 들면 문자를 가리키는 포인터) 바꾸어 씁니다. 즉 이 타입의 인자는 함수 내부에서 원래의 타입인 char **로 바뀌어지고, 다시 실제로 가리키는 타입인 char *를 얻어서 strcmp 함수에 전달됩니다. (ANSI 이전 컴파일러에서 쓸 때에는 void * 대신에 char *를 쓰고, 모든 const를 빼면 됩니다.)

실제 qsort는 다음과 같이 부릅니다:

  #include <stdlib.h>
  char *strings[NSTRINGS];
  int nstrings;

  /* nstrings cells of strings[] are to be sorted */
  qsort(strings, nstrings, sizeof(char *), pstrcmp);

([K&R2] § 5.11 pp. 119-20에 나온 것을 혼동하면 안됩니다. 그것은 표준 함수인 qsort와는 관계없으며, char *void *가 서로 같다는 불필요한 암묵적인 가정을 하고 쓴 것입니다.)

qsort 비교 함수에 대한 더 자세한 정보는 -- 어떻게 선언되고 어떻게 불리워지는가에 대한 -- 질문 [*]13.9를 보기 바랍니다.

References
[ANSI] § 4.10.5.2
[C89] § 7.10.5.2
[H&S] § 20.5 p. 419



Q 13.9
Structure에 대한 배열을 qsort()로 정렬하려 합니다. 제가 만든 비교 함수는 이 structure에 대한 포인터를 받도록 했지만, 컴파일러는 qsort에 잘못된 타입의 비교 함수가 전달되었다고 불평합니다. 어떻게 함수 포인터를 캐스팅해야 컴파일러가 불평하지 않을까요?

Answer
질문 [*]13.8에서 다룬 것처럼, 캐스팅은 비교 함수 안에서 이루어져야 합니다. 즉 비교 함수는 “generic pointer”인 const void * 타입을 인자로 받도록 만들어야 합니다. 예를 들어 아래처럼 structure를 만들었다고 합시다:

  struct mystruct {
    int year, month, day;
  }
이 때, 비교 함수는 다음과 같이 만들어야 합니다:

  int mystructcmp(const void *p1, const void *p2)
  {
    const struct mystruct *sp1 = p1;
    const struct mystruct *sp2 = p2;

    if (sp1->year < sp2->year) return -1;
    else if (sp1->year > sp2->year) return 1;
    else if (sp1->month < sp2->month) return -1;
    else if (sp1->month > sp2->month) return 1;
    else if (sp1->day < sp2->day) return -1;
    else if (sp1->day > sp2->day) return 1;
    else return 0;
  }
(Generic 포인터를 struct mystruct 포인터로 변경하는 것은, 초기화할 때인 sp1 = p1, sp2 = p2에서 이루어졌습니다; p1p2void 포인터이기 때문에, 컴파일러는 이 conversion을 자동으로 해 줍니다. ANSI 이전의 컴파일러에서 쓰려면 강제로 캐스팅하고, char * 포인터를 쓰면 됩니다. 질문 [*]7.7 참고)

이런 형태의 mystructcmp를 쓰기 위해, 다음과 같이 qsort를 부릅니다:

  #include <stdlib.h>
  struct mystruct dates[NDATES];
  int ndates;
  /* ndates cells of dates[] are to be sorted */
  qsort(dates, ndates, sizeof(struct mystruct),
        mystructcmp);

만약, structure를 가리키는 포인터를 정렬하고 싶다면, 질문 [*]13.8에 나온 것처럼 indirection이 필요합니다. 이 때 비교 함수는 다음과 같이 시작합니다:

  int myptrstructcmp(const void *p1, const void *p2)
  {
    struct mystruct *p1 = *(struct mystruct * const *)p1;
    struct mystruct *p2 = *(struct mystruct * const *)p2;
그리고, 다음과 같이 부릅니다:
  struct mystruct *dateptrs[NDATES];
  qsort(dateptrs, ndates, sizeof(struct mystruct *),
                          myptrstructcmp);
qsort 함수에서 쓰이는 비교 함수에 왜 이런 포인터 변환이 필요한지 이해하기 위해서 (그리고 왜 qsort를 부를때 함수 포인터를 캐스팅하는 것이 불가능한지) qsort가 어떤 식으로 동작하는지 알 필요가 있습니다: qsort는 정렬할 대상이 어떤 타입인지, 어떤 식으로 표현되어 있는지 전혀 알지 못합니다. qsort는 단지, 전달된 조그만 메모리 블럭(little chunks of memory)들을 섞어 줄 뿐입니다. (이 메모리 블럭에 대해 알고 있는 것은, qsort의 세번째 인자로 받은 블럭의 크기입니다.) 두 개의 블럭을 교환(swap)해야 할 경우, qsort는 여러분이 만든 비교 함수를 부릅니다. (실제로 교환하는 것은 memcpy가 하는 것과 같은 방식으로 이루어집니다.)

qsort는 어떤 타입인지 알지 못하는, 일반적인 타입에 대한 정렬을 하므로, 정렬 대상을 가리키기 위해, generic 포인터를 (void *) 씁니다. qsort가 비교 함수를 부를때, 여러분이 만든 비교 함수는 void *를 인자로 받도록 만들어져 있어야 하며, 실제 정렬 대상에 대해 어떤 작업을 (예를 들어 실제 비교를 하기 위해) 수행하기 위해서, 인자로 받은 포인터를 원하는 타입으로 다시 변경해야 합니다. 어떤 시스템에서는 void 포인터가 structure 포인터와 서로 다른 표현 방식을 씁니다; 예를 들어, 실제 포인터의 길이가 다를 수 있으며, 내부 표현 방식이 다를 수 있습니다. (그렇기 때문에 캐스팅이 필요합니다.)

어떤 Structure에 대한 배열을 정렬한다고 가정해봅시다. 그리고 비교 함수가 이 structure에 대한 포인터를 받는다고 하면 다음과 같습니다:

  int mywrongstructcmp(struct mystruct *, 
                       struct mystruct *);
만약 다음과 같이 qsort를 부르게 되면:
  qsort(dates, ndates, sizeof(struct mystruct),
        (int (*)(const void *,
                 const void*)mywrongstructcmp);
        /* WRONG */
이 것은, 컴파일러가 “qsort에 전달된 비교 함수가 제대로 동작하지 않을 수 있다”고 경고하는 것만을 없애 줄 수 있을 뿐입니다. 여기서 사용한 캐스팅은 실제 qsort가 여러분의 비교 함수를 부를 때에는 이미 알 수 없는 것입니다: 즉, qsortconst void * 타입의 인자를 만들 것이고, 여러분이 만든 비교 함수는 이 인자를 반드시 받을 수 있어야 합니다. 어떠한 메카니즘도, qsortmywrongstructcmp를 부를 때, void 포인터를 struct mystruct 포인터로 바꿔 줄 수 없습니다.

따라서, 일반적으로 위와 같이 단순히 컴파일러 경고를 없애기 위한 캐스팅은 매우 나쁜 방법입니다. 컴파일러 경고는 보통 여러분에게 어떤 사항을 알려 줄려고 나온 것이기 때문에, 여러분이 어떤 식으로 동작하는지 잘 모르고 이 경고를 무시한다면, 그 결과 매우 위험한 상태가 될 수 있습니다. 덧붙여 질문 [*]4.9도 참고하시기 바랍니다..

References
[ANSI] § 4.10.5.2
[C89] § 7.10.5.2
[H&S] § 20.5 p. 419



Q 13.10
`linked list'를 정렬하는 방법은?
Answer
때때로, 리스트를 만들 때, 정렬하며 만드는 방법이 더 쉬울 때가 많습니다. (또는 리스트 대신에 트리(tree)를 쓰는 것이 더 편할 수 있습니다.) 삽입 정렬(insertion sort)이나 병합 정렬(merge sort) 같은 알고리즘은 리스트를 쓸 때, 매우 효과적입니다. 만약 표준 라이브러리 함수를 써서 정렬하고 싶다면, 먼저 이들 리스트를 가리키는 포인터의 배열을 (임시로) 만들고, qsort()를 쓰면 됩니다. 그리고, 이 정렬된 포인터를 이용하여, 리스트를 정리하면 됩니다.
References
[Knuth] § 5.2.1 pp. 80-102, § 5.2.4 pp. 159-168
[Robert] § 8 pp. 98-100, § 12 pp. 163-175



Q 13.11
메모리의 크기보다 더 큰 데이터를 정렬하고 싶은데, 어떻게 하죠?
Answer
외부 정렬을 (external sort) 써야 합니다. 여기에 대한 것은 [Knuth]에 잘 나와 있습니다. 기본적인 아이디어는, 데이터를 (메모리에 불러올 수 있을 정도의) 작은 조각으로 나누고, 이 데이터를 정렬하고 난 다음, 이를 임시 파일에 저장하고, 다 반복하였으면, 이 임시 파일들을 합치는 (merge) 것입니다. 운영 체제에서 이러한 정렬 프로그램을 제공할 수도 있으니 프로그램 안에서 이러한 프로그램을 실행시키는 것을 생각할 수도 있습니다; 질문 [*]19.27과 [*]19.30, 그리고 질문 [*]19.28의 예를 참고하기 바랍니다.
References
[Knuth] § 5.4 pp. 247-378
[Robert] § 13 pp. 177-187

13.3 Date and Time



Q 13.12
현재 날짜와, 요일을 알아내려면 어떻게 하죠?
Answer
time(), ctime(), localtime(), 또는 strftime()을 쓰면 됩니다. (이 함수들은 매우 오래 전부터 제공되었으며, 현재 ANSI 표준입니다.) 예를 들면 다음과 같습니다13.2:
  #include <stdio.h>
  #include <time.h>

  int main()
  {
    time_t now;
    time(&now);
    printf("It's %.24s.\n", ctime(&now));
    return 0;
  }

localtimestrftime은 다음과 같이 씁니다:

  struct tm *tmp = localtime(&now);
  char fmtbuf[30];
  printf("It's %d:%02d:%02d\n",
         tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
  strftime(fmtbuf, sizeof fmtbuf, "%A, %B %d, %Y", tmp);
  printf("on %s\n", fmtbuf);

(이 함수들이 주어진 값을 고치지 않는데도 time_t 변수에 대한 포인터를 인자로 받는 것에 주의하기 바랍니다.13.3

References
[K&R2] § B10 pp. 255-7
[ANSI] § 4.12
[C89] § 7.12
[H&S] § 18



Q 13.13
라이브러리 함수 localtime()time_tstruct tm 형태로 바꾸어 줍니다. 그리고 ctime()time_t를 문자열로 바꾸어 줍니다. 거꾸로 변환하고 싶으면 어떻게 해야 할까요? 즉, 문자열이나 struct tmtime_t 타입으로 변경하고 싶습니다.
Answer
ANSI C에서는 struct tm 타입을 time_t로 바꾸어 주는 mktime() 함수를 제공합니다.

문자열을 time_t로 바꾸는 것은 조금 힘듭니다. 왜냐하면, 각 나라 혹은 지역별로 사용하는 날짜와 시간 형식이 각각 다르기 때문입니다. 어떤 시스템은 strptime()이라는 함수를 제공하며, 그 기능은 strftime()의 반대입니다. 또 (RCS 패키지와 함께 제공되는) partime()이라는 함수와 (최신의 C news 배포판에서 제공하는) getdate()라는 함수도 자주 쓰입니다. 질문 [*]18.16을 보기 바랍니다.

References
[K&R2] § B10 p. 256
[ANSI] § 4.12.2.3
[C89] § 7.12.2.3
[H&S] § 18.4 pp. 401-2



Q 13.14
주어진 날짜에 (N days) 날짜를 더하는 기능이 있나요? 또 주어진 두 날짜를 비교하는 함수가 있을까요?

Answer
이러한 문제를 해결하기 위해서, ANSI/ISO C 표준은 mktimedifftime 함수를 제공합니다. mktime()은 정규화되지 않은 (non-normailized) 날짜를 struct tm 타입으로 입력받아 이것을 정규화시켜 줍니다. 즉, struct tm의 변수에 기준 날짜 값을 넣고, tm_mday에서 원하는 날짜만큼 더하거나 뺀 다음, mktime을 부르면 normalize된 년, 월, 일을 얻을 수 있습니다. (추가적으로 이 값을 time_t 타입으로 변경시켜 줍니다.) difftime()은 두 개의 time_t 값을 입력받아 초 단위로 비교해 줍니다; 이 때 필요한 time_t 값은 mktime()을 써서 얻습니다.

그러나 이 해결책들은 주어진 날짜 값이 time_t 범위 안에 있을 때만 동작합니다. tm_days 필드는 int이기 때문에 일(day) 수가 int 값을 넘는 값일 경우 (예를 들어 32,736 이상), 오버플로우가 발생할 수 있습니다. 또한 daylight saving time(써머 타임)을 고려하면 하루는 24 시간이 아닐 수도 있습니다 (따라서 86400 seconds/day로 나누면 해결될 것이라는 생각을 하면 안됩니다).

다음은 October 24, 1994에서 90일을 빼는 코드입니다:

  #include <stdio.h>
  #include <time.h>

  tm1.tm_mon = 10 - 1;
  tm1.tm_mday = 24;
  tm1.tm_year = 1994 - 1900;
  tm1.tm_hour = tm1.tm_min = tm1_tm_sec = 0;
  tm1.tm_isdst = -1;

  tm1.tm_mday -= 90;

  if (mktime(&tm1) == -1)
    fprintf(stderr, "mktime failed.\n");
  else
    printf("%d/%d/%d\n",
           tm1.tm_mon + 1, tm1.tm_mday, tm1.tm_year + 1900);
(tm_isdst를 -1로 설정하거나 tm_hour를 12로 설정하면 daylight saving time 문제로부터 보호받는데 도움을 줍니다.)

아래 코드는 2000년 2월 28일과 2000년 3월 1일 사이의 날짜 차이를 구하는 코드입니다:

  struct tm tm1, tm2;
  time_t t1, t2;

  tm1.tm_mon = 2 - 1;
  tm1.tm_mday = 28;
  tm1.tm_year = 2000 - 1900;
  tm1.tm_hour = tm1.tm_min = tm1.tm_sec = 0;
  tm1.tm_isdst = -1;

  tm2.tm_mon = 3 - 1;
  tm2.tm_mday = 1;
  tm2.tm_year = 2000 - 1900;
  tm2.tm_hour = tm2.tm_min = tm2.tm_sec = 0;

  t1 = mktime(&tm1);
  t2 = mktime(&tm2);

  if (t1 == -1 || t2 == -1)
    fprintf(stderr, "mktime failed\n");
  else {
    long d = (difftime(t2, t1) + 86400L/2) / 86400L;
    printf("%ld\n", d);
  }
(위에서 추가적으로 쓴 86400L / 2는 계산 결과를 원하는 근사값으로 만들어 줍니다; 질문 [*]14.6 참고)

“Julian day number”를 (또는 4013 BC13.4 1월 1일) 사용하는 방법도 있습니다. 아래는 Julian day로 바꾸는 함수의 선언입니다:

  /* returns Julian for month, day, year */
  long ToJul(int month, int day, int year);

  /* returns month, day, year for jul */
  void FromJul(long jul, 
               int *monthp, int *dayp, int *yearp);
이 때, n일(day)을 더하는 것은 다음과 같이 합니다:
  int n = 90;
  int month, day, year;
  FromJul(ToJul(10, 24, 1994) + n, &month, &day, &year);
또, 두 날짜의 차이(일 수 단위로)는 다음과 같이 구합니다:
  ToJul(3, 1, 2000) - ToJul(2, 28, 2000)
Julian day를 다루는 함수들은 Snippets 콜렉션에 (질문 [*]18.15c 참고) 있습니다. 그리고 이 콜렉션은 Simtel/Oakland 아카이브(archive)에서 (파일 JULCAL10.ZIP, 질문 [*]18.16 참고) 얻을 수 있습니다. 아래의 “Date conversions” 기사도 참고하기 바랍니다.

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

References
[K&R2] § B10 p. 256
[ANSI] § 4.12.2.2, § 4.12.2.3
[C89] § 7.12.2.2, 7.12.2.3
[H&S] § 18.4,18.5 pp. 401-2
[Burki]



Q 13.14b
C 언어는 2000년 문제에 어떤 문제가 있을까요?
Answer
없습니다. 단지, 허술하게 작성된 C 프로그램에 문제가 있을 뿐입니다.

struct tmtm_year 필드는 1900년대에서 시작한 년도를 가지고 있습니다. 따라서 2000년일 경우에 이 값은 100이 됩니다. tm_year를 제대로 사용하는 코드라면 (변환할 때, 1900을 더하거나 빼는) 전혀 문제될 것이 없습니다. 그러나 이 값을 두 자리 숫자로 바로 사용한다거나 다음과 갈은 코드를 썼다면:

  tm.tm_year = yyyy % 100;        /* WRONG */
또는, 다음과 같이 했다면:
  printf("19%d", tm.tm_year);     /* WRONG */
2000년 문제가 발생합니다. 덧붙여 질문 [*]20.32도 참고하시기 바랍니다.
References
[K&R2] § B10 p. 255
[C89] § 7.12.1
[H&S] § 18.4 p. 401

13.4 Random Numbers



Q 13.15
난수(random number) 발생기가 필요합니다.
Answer
표준 C 라이브러리에는 rand()라는 함수가 있습니다. 물론 각각의 시스템에 따라 이 함수의 구현(implementation)은 완전하지 않을 수 있습니다. 그러나, 이 함수보다 더 좋은 성능을 가진 함수를 직접 만들기란 쉬운 일이 아닙니다.

직접 난수 발생기를 만들려고 한다면, 읽어야 할 책들이 꽤 많습니다; 이 글 뒷부분에 있는 Reference를 보시기 바랍니다. 또한 인터넷 상에 많은 패키지가 이미 올라와 있습니다; r250, RANLIB, FSULTRA로 검색해 보기 바랍니다. (질문 [*]18.16도 참고.)

아래는 Park씨와 Miller씨에 의해 제안된, “minimal standard” generator입니다.

  #define a 16807
  #define m 2147483647
  #define q (m / a)
  #define r (m % a)

  static long int seed = -1;

  long int PMrand()
  {
    long int hi = seed / q;
    long int lo = seed % q;
    long int test = a * lo - r * hi;
    if (test > 0)
      seed = test;
    else
      seed = test + m;
    return seed;
  }
(The “minimal standard” is adequately good; it is something “against which all others should be judged” and is recommended for use “unless one has access to a random number generator known to be better.”)

이 코드는 a = 16807이고, m = 2147483647(이 값은 $2^{31}-1$입니다)이고, c = 0인, 다음 generator를 구현합니다:

\begin{displaymath}X \leftarrow (aX + c) \bmod m \end{displaymath}

나머지(modulus) 연산이 소수(prime) 연산이기 때문에, 이 generator는 질문 [*]13.18에 나온 문제가 발생하지 않습니다. 위에서 사용한 곱하기는 Schrage씨에 의해 소개된 기법을 써서, 곱한 결과인 aX가 overflow를 발생시키지 않습니다. 위 코드는 값의 범위가 [1, 2147483647]를 만족하는 long int 값을 돌려주며, 이 범위는 C 언어의 rand 함수가 RAND_MAX보다 작은 값을 돌려 주는 것과 비슷한 방식입니다. (단지 위 코드가 만들어 내는 값에는 0이 들어가지 않는다는 것만 다릅니다.) 위 코드를 (Park씨와 Miller씨의 논문에 나온 것처럼) 범위 (0, 1) 사이의 실수를 만들도록 고치려면, 위 함수의 선언을 다음과 같이 고치고:
  double PMrand(void);
마지막 줄을 다음과 같이 고칩니다:
  return (double)seed / m;
통계적으로 좀 더 좋은 결과를 얻으려면, (Park씨와 Miller씨의 추천에 따르면) a = 48271을 쓰기 바랍니다.
References
[K&R2] § 2.7 p. 46, § 7.8.7 p. 168
[ANSI] § 4.10.2.1
[C89] § 7.10.2.1
[H&S] § 17.7 p. 393
[PCS] § 11 p. 172
[Knuth] Vol. 2 Chap. 3 pp. 1-177
[Park]



Q 13.16
어떤 범위를 갖는 난수를 발생시키고 싶습니다.
Answer
다음과 갈이 하는 것은 (0에서 N-1까지의 수치를 리턴함) 매우 서투른 방법입니다:
  rand() % N         /* POOR */
왜냐하면, 대부분의 난수 발생기에서 하위(low-order) 비트들은 그리 랜덤하지 않기 때문입니다. (질문 [*]13.18을 보기 바랍니다.) 따라서, 다음과 같이 하는 것이 좋습니다:
  (int)((double)rand() / ((double)RAND_MAX + 1) * N)
실수를 쓰기 싫다면, 다음과 같이 해도 좋습니다:
  rand() / (RAND_MAX / N + 1)
두 방법 모두 (ANSI 표준에 의해 <stdlib.h>에 정의되어 있는) RAND_MAX를 사용하고, NRAND_MAX보다 아주 작은 값이라는 것을 가정한 방법입니다.

만약 NRAND_MAX에 가깝고, random number generator의 범위가 N의 배수가 아니라면 (즉, RAND_MAX + 1 % N != 0인 경우) 위 모든 방법이 나쁜 방법이 됩니다: 어떤 범위의 수치들이 다른 범위보다 훨씬 더 많이 나타나게 됩니다. (실수를 쓴다고 해결될 문제가 아닙니다. 문제는 rand 함수가 서로 다른 RAND_MAX + 1가지의 값을 리턴하고, 이 것을 N개의 묶음으로 공평하게 나눌 수 없기 때문에 발생하는 것입니다.) 만약 이것이 문제가 된다면, rand를 여러 번 불러서, 특정 값들을 무시하면 됩니다:

  unsigned int x = (RAND_MAX + 1u) / N;
  unsigned int y = x * N;
  unsigned int r;

  do {
    r = rand();
  } while (r >= y);
  return r / x;
또, [M, N]의 범위를 가지는 random number를 구하고 싶다면, 위 결과를 shift해서 얻을 수 있습니다:
  M + rand() / (RAND_MAX / (N - M + 1) + 1)
(어쨋든, RAND_MAXrand()가 리턴할 수 있는 최대 수치를 나타내는 상수라는 것을 알아 두시기 바랍니다. RAND_MAX에 여러분이 다른 수치를 대입할 수는 없으며, rand()가 다른 범위의 수치를 리턴하도록 해 주는 방법은 존재하지 않습니다.)

만약에 여러분이 (질문 [*]13.15의 마지막 PMrand 함수 또는 질문 [*]13.21의 drand48 함수처럼) 0과 1 사이의 범위를 가지는 난수 발생기를 가지고 있다면, 그리고 0과 N-1 사이의 범위를 가지는 정수 난수를 만들고 싶다면, 단순히 그 난수 발생기의 수치에 N을 곱하면 됩니다:

  (int)(drand48() * N)

References
[K&R2] § 7.8.7 p. 168
[PCS] § 11 p. 172



Q 13.17
프로그램을 실행시킬 때마다, rand()는 일정한 순서로 난수를 (random number) 발생시킵니다. 왜 그러죠?
Answer
대부분 가상(pseudo) random generator의 특징은 (C 라이브러리의 rand도 마찬가지), 항상 같은 번호에서 시작해서 똑같은 sequence로 값이 나온다는 것입니다. (Among other things, a bit of predictability can make debugging much easier.) 예상 불가능한 난수를 발생시키고 싶으면, srand 함수를 불러서, 이 pseudo-random generator의 초기값(seed)으로, 진짜 random 값을 하나 주면 됩니다. 보통 이 seed 값으로, 현재 시간을 쓰거나, 사용자가 키를 누르기까지의 시간 간격 등을 쓰게 됩니다. (물론 키를 누르기까지의 경과된 시간을 얻는 것은 이식성 있는 범위 안에서 만들기가 힘듭니다. 질문 [*]19.37 참고) 아래는 현재 시간을 써서 seed 값을 설정하는 코드입니다.
  #include <stdlib.h>
  #include <time.h>

  srand((unsigned int)time((time_t *)NULL));

(일반적으로 프로그램 내에서는 srand() 함수를 한번만 불러주어도 충분합니다. rand()를 부를 때마다, srand()를 부른다면, random number를 얻을 수 없습니다.)

References
[K&R2] § 7.8.7 p. 168
[ANSI] § 4.10.2.2
[C89] § 7.10.2.2
[H&S] § 17.7 p. 393



Q 13.18
참/거짓을 나타내는 random number(난수)가 필요합니다. rand() % 2를 썼는데, 계속 0, 1, 0, 1, 0 만을 반복합니다.
Answer
(불행하게도 어떤 시스템에서 제공하는 것과 같은) 엉성하게 만들어진 pseudo-random number generator는 하위 (low-order) 비트들에 대해서는 그렇게 랜덤하지 않습니다. (In fact, for a pure linear congruential random number generator with period $2^e$, and this tends to be how random number generates for e-bit machines are written, the low-order n bits repeat with period $2^n$.) 이러한 이유에서, 상위 (high-order) 비트를 쓰는 것이 더 바람직합니다: 질문 [*]13.16을 보기 바랍니다.
References
[Knuth] § 3.2.1.1 pp. 12-14



Q 13.19
반복되지 않는, random number의 sequence를 얻으려면 어떻게 해야 할까요?
Answer
여러분이 찾고 있는 것은 보통, “random permutation(순열)”, 또는 “shuffle”이라고 부릅니다. 한가지 방법으로, 뒤섞이기 원하는 값들을 배열에 넣고, 이 배열의 임의의 두 element의 값을 서로 바꾸는 작업을 반복해서 얻을 수 있습니다:
  int a[10], i, nvalues = 10;

  for (i = 0; i < nvalues; i++)
    a[i] = i + 1;

  for (i = 0; i < nvalues - 1; i++) {
    int c = randrange(nvalues - i);
    int t = a[i]; a[i] = a[i + c]; a[i + c] = t;  /* swap */
  }
여기에서 randrange(N)rand() / (RAND_MAX / (N) + 1) 또는 질문 [*]13.16에 나온 코드를 씁니다.
References
[Knuth] § 3.4.2 pp. 137-8



Q 13.20
How can I generate random numbers with a normal or Gaussian distribution?

Answer
적어도 다음과 같은 세 가지 방법이 있습니다:

These methods all generate numbers with mean 0 and standard deviation 1. (To adjust to another distribution, multiply by the standard deviation and add the mean.) Method 1 is poor “in the tails” (especially if NSUM is small), but methods 2 and 3 perform quite well. See the references for more information.

References
[Knuth] Vol.2 § 3.4.1 p. 117
Box and Muller, “A Note on the Generation of Random Normal Deviates”
Marsaglia and Bray, “A Convenient Method for Generating Normal Variables”
Abramowitz and Stegun, Handbook of Mathematical Functions
Press et al., Numerical Recipes in C § 7.2 pp. 288-90.



Q 13.21
어떤 프로그램을 포팅하고 있는데, 이 프로그램이 drand48이란 함수를 씁니다. 그리고 이 함수는 제 시스템에서 제공하지 않습니다. 어떻게 이 함수를 만들 수 있을까요?
Answer
UNIX System V에서 제공하는 drand48() 함수는 (추측하건대, 48 비트의 precision을 갖는) 범위 [0, 1) 사이의 floating-point(실수) random number를 리턴합니다. (이 함수의 seed 값을 설정하는 함수는 srand48입니다; 두 함수 모두 C 표준이 아닙니다.) 정확성이 낮은 (low precision) 대체 함수는 다음과 같습니다:
  #include <stdlib.h>

  double drand48(void)
  {
    return rand() / (RAND_MAX + 1.);
  }

좀 더 정확한 값을 (drand48처럼 48 bit의 precision을 갖는) 돌려주는 함수는 다음과 같습니다:

  #define PRECISION 2.82e14      /* 2**48, rounded up */

  double drand48(void)
  {
    double x = 0;
    double denom = RAND_MAX + 1.;
    double need;

    for (need = PRECISION; need > 1;
         need /= (RAND_MAX + 1.)) {
      x += rand() / denom;
      denom *= RAND_MAX + 1.;
    }
    return x;
  }
Before using code like this, though, beware that it is numerically suspect, particularly if (as is usually the case) the period of rand is on the order of RAND_MAX. (If you have a longer-period random number generator available, such as BSD random, definitely use it when simulating drand48.)
References
[PCS] § 11 p. 149

13.5 Other Library Functions



Q 13.22
exit(status)가 정말로 main에서 status를 리턴하는 것과 같나요?
Answer
질문 [*]11.16을 보기 바랍니다.



Q 13.23
memcpymemmove가 다른 점이 있나요?
Answer
질문 [*]11.25를 보기 바랍니다.



Q 13.24
오래된 프로그램을 포팅하려고 합니다. 그런데 index, rindex, bcopy, bcmp, bzero 함수가 정의되어 있지 않은 것 같습니다. “undefined external”이라는 에러가 뜨는 군요.
Answer
안 쓰는 구식의 함수들입니다. 다음 함수들을 쓰기 바랍니다.

index strchr 함수를 쓰기 바랍니다.
rindex strrchr 함수를 쓰기 바랍니다.
bcopy 첫 번째 인자와 두 번째 인자를 바꾸어서 memmove 함수를 쓰기 바랍니다. 덧붙여 질문 [*]11.25도 참고하시기 바랍니다.
bcmp memcmp 함수를 쓰기 바랍니다.
bzero 두 번째 인자를 0으로 해서 memset 함수를 쓰기 바랍니다.

만약, 오래된 시스템을 쓰고 있고, 위 표의 오른쪽에 있는 함수들을 쓰는 프로그램을 포팅해야 한다면, 왼쪽에 있는 함수로 바꾸거나, 아니면 새 함수들을 만들 수 있습니다. 덧붙여 질문 [*]12.22, [*]13.21도 참고하시기 바랍니다.

References
[PCS] § 11
Note
index, rindex, bcopy, bcmp, bzero 모두 4.3 BSD 시스템에서 제공하는 함수로서, C 표준이 아닙니다. 이들 함수는 특히, 오래된 network 관련 프로그램 소스에서 많이 쓰며, 1:1 대응되는 표준 함수들이 (위에서 소개한 것처럼) 있으니, 표준 함수들을 쓰시기 바랍니다. 기존 소스를 수정할 수 없는 상황이라면, 이들 함수를 매크로로 표준 함수로 바꿔서 만들 수 있습니다. Richard Stevens씨는 UNIX Network Programming 책에서 모든 코드가 4.3 BSD 함수를 쓰도록 만들고 헤더 파일 <unp.h>에서 표준 함수로 (매크로를 써서) 예전 함수를 만들어 두었습니다.
References
[Stevens98]



Q 13.25
필요한 헤더 파일들을 모두 #include시켰는데도 라이브러리 함수가 정의되어 있지 않다고 에러가 발생합니다.
Answer
일반적으로, 헤더 파일은 선언(declaration)만을 포함하고 있습니다. 어떤 경우에는 (특히, 비표준 함수인 경우) 실제 정의를 포함시키기 위해서는 특별한 라이브러리를 같이 링크(link)하라고 명령을 주어야 합니다. (#include하는 것만으로는 불충분합니다.) 덧붙여 질문 [*]11.30, [*]13.26, [*]14.3도 참고하시기 바랍니다.



Q 13.26
올바른 라이브러리를 포함시키라고 했는데도 라이브러리 함수가 정의되어 있지 않다고 에러가 발생합니다.
Answer
대부분 linker들은, 주어진 오브젝트 파일과 라이브러리 파일들을 단 한번씩만 조사하고, 함수 호출에 필요한 정의가 발견되면 함께 링크합니다. 만약 주어진 파일 목록에서 (한번 시도해서) 함수 정의를 찾지 못한다면, 심볼이 정의되어 있지 않다고 에러가 생깁니다. 따라서, linker에게 전달하는 오브젝트 파일들과 라이브러리 파일들의 순서가 매우 중요합니다; 보통의 경우, 라이브러리 파일을 마지막에 써 두는 것이 좋습니다.

예를 들어, 대부분 UNIX 시스템에서 다음과 같이 명령을 실행하면, 보통 동작하지 않습니다:

  cc -lm myproc.c
대신, -l 옵션을 마지막에 주면 동작합니다:
  cc myproc.c -lm
라이브러리 파일을 먼저 주면, linker는 이 라이브러리에서 제공하는 정의들이 나중에 쓰일지 쓰이지 않을 지 알 수가 없습니다. (아직 이 라이브러리에 있는 함수를 쓰는 오브젝트 파일을 발견하지 못했기 때문) 질문 [*]13.28을 보기 바랍니다.



Q 13.27
제 프로그램은 단순히 “Hello, world”를 출력하는 일만 하는데, 실행 파일의 크기가 (수백 KB 정도로) 매우 큽니다. 왜 그럴까요? Include하는 헤더 파일을 줄여야 하나요?
Answer
그 문제는, 현재 라이브러리 디자인이 아주 서툰 수준이기 때문에 일어납니다. (특히 graphic user interface를 포함하는) run-time 라이브러리들은, 가능한 많은 기능을 라이브러리에 포함시키려하는 경향이 있습니다. 한 라이브러리가 어떤 일을 하기 위해 다른 라이브러리를 부른다면 (이런 방식은 매우 좋은 현상이며, 라이브러리가 왜 존재하는지 그 이유에 해당합니다.), (현실적으로) 그 라이브러리에 있는 거의 모든 내용을 한꺼번에 포함시키려 할 수 있습니다. 따라서 실행 파일의 크기가 커질수 있습니다.

포함하는 헤더 파일을 줄인다고 해결될 문제는 아닙니다. 왜냐하면, 선언된 함수를 줄인다고 해서 (보통 헤더 파일을 선언하고 그 헤더 파일에서 제공하는 함수를 쓰지 않는 경우), 여러분의 실행 파일에 포함되는 함수 정의와는 관계가 없기 (왜냐하면 쓰이지 않았기 때문에) 때문입니다. 덧붙여 질문 [*]13.25도 참고하시기 바랍니다.

힘든 방법이지만, 쓰이지 않는 함수들의 의존성을 검사한 다음, 필요없는 라이브러리를 link하지 않는 방법이 있습니다. 또는, 라이브러리 제작자에게 라이브러리를 좀 더 깔끔하게 만들어달라고 요구할 수 있습니다.

Note
Shared library (dynamic linked library)를 쓰면 이 문제를 해결할 수 있습니다. 즉 실행 파일이 필요한 함수들의 정의를 포함하지 않고, 단순히 그 함수를 부르는 stub code만이 들어가며, 실제 라이브러리는 필요에 따라서 시스템이 메모리에 load하는 경우에는, 실행 파일이 커지지 않습니다. 시스템에 따라서 shared library 기능이 없을 수 있으므로 확인 바랍니다.

보통 라이브러리는 여러 개의 오브젝트 파일을 묶은 형태로 존재합니다. 기능이 떨어지는 linker를 쓴다면, 위에서 설명한 것처럼 라이브러리 내용을 모두 실행 파일에 포함시키겠지만, 기능이 뛰어난 linker는 (라이브러리 안에 존재하는) 오브젝트 파일 단위로 link합니다. 따라서 의존성이 여러 오브젝트 파일에 거쳐서 존재하지 않은 한, 위와 같은 현상은 드물게 발생합니다. 물론 나중에는 정말로 뛰어난 linker가 나와서 이런 문제가 전혀 없을 수 있지만, 아직은 그런 때가 아닌 것 같습니다.

비슷한 이유에서 GNU C Library(glibc) 소스를 보면, (오브젝트 파일 단위로 링크되는 것을 가정하고, 최소한의 내용만 link될 수 있게 하기 위해서) 소스 파일 하나가 한 두개의 함수 정의만 포함하는 것을 알 수 있습니다.

References
GNU C Library - http://www.gnu.org/software/libc/



Q 13.28
_end가 정의되어 있지 않다고 링커가 에러를 내는데 이게 무엇을 의미하나요?
Answer
오래된 UNIX 링커에서 흔히 발생하는 것입니다. 대개 _end가 정의되어 있지 않다고 에러가 나는 것은 다른 심볼(symbol)들도 정의가 되어 있지 않다는 것을 의미합니다. 정의되어 있지 않은 다른 심볼들이 있나 검사해 보기 바랍니다. 그러면 _end가 정의되어 있지 않다고 하는 에러는 사라집니다. (덧붙여 질문 [*]13.25, [*]13.26도 참고하시기 바랍니다.)

Seong-Kook Shin
2018-05-28