Subsections


2. Structures, Unions, and Enumerations

Structure, union, enumeration은 여러분에게 새로운 data type을 정의할 수 있게 해준다는 공통점을 가집니다. 먼저, structure나 union은 여러분이 member나 field를 선언하여 새 data type을 정의할 수 있고, enumeration의 경우 상수를 (constant) 선언하여 새 data type을 정의할 수 있습니다. 동시에 여러분은 새로운 data type에 tag name을 줄 수 있습니다. 일단 새 type을 정의했다면, 정의와 동시에 또는 나중에 새 type의 instance(변수)를 선언할 수 있습니다.

복잡하게도, 기본 타입과 마찬가지로 user-defined type에도 typedef를 써서 새 이름을 줄 수 있습니다. 이렇게 했을 때, 여러분은 typedef 이름이 (만약 tag 이름이 존재할 경우) tag 이름과는 전혀 상관없다는 것을 아셔야 합니다.

이 chapter의 질문들은 다음과 같이 정리되어 있습니다: 질문 [*]2.1부터 [*]2.18은 structure에 대하여, 질문 [*]2.19부터 [*]2.20까지는 union에 대하여, 질문 [*]2.25부터 [*]2.26까지는 bitfield에 대해 다룹니다.

2.1 Structure Declaration



Q 2.1
다음 두 선언의 차이점이 무엇인가요?

  struct x1 { ...  };
  typedef struct { ...  } x2;

Answer
첫번째 선언은 “구조체 태그(structure tag)”를 선언한 것입니다; 두번째 선언은 “typedef”를 선언한 것입니다. 주 차이점은 첫번째 경우의 타입 이름은 struct x1이며, 두번째 경우의 타입 이름은 간단히 x2라는 것입니다. 즉 두번째 것이 조금 더 추상화된 타입이라 할 수 있습니다. -- 즉 사용자들이 struct이라는 키워드를 쓰지 않게되어 이 타입이 구조체인지 아닌지 알 필요가 없습니다:
  x2 b;
tag를 써서 정의된 구조체는 다음과 같이 선언해야 합니다.2.1
  struct x1 a;
(두 가지 방식 모두 적용해서 다음과 같이 정의할 수도 있습니다:
  typedef struct x3 { ... } x3;
조금 혼동스럽기는 하지만, tag 이름과 typedef 이름이 중복되는 것은, 서로 다른 namespace에 속하기 때문에, 전혀 상관없습니다. 질문 [*]1.29를 보기 바랍니다.)



Q 2.2
다음 코드가 왜 동작하지 않을까요?

  struct x { ...  };
  x thestruct;

Answer
C 언어는 C++이 아닙니다. 즉 구조체 태그(tag) 이름에 대해 자동으로 typedef 이름이 만들어지지 않습니다. 실제 구조체는 struct 키워드를 써서 정의합니다:
  struct x thestruct;
원한다면, 구조체를 선언할 때, typedef 이름을 같이 선언하고, 이 typedef 이름으로 실제 구조체 변수 또는 상수를 만들 때 쓸 수 있습니다:
  typedef struct { ... } tx;
  
  tx thestruct;
덧붙여 질문 [*]2.1도 참고하시기 바랍니다.



Q 2.3
구조체가 자신에 대한 포인터를 포함할 수 있나요?
Answer
당연히 포함할 수 있습니다. typedef와 함께 쓸 때 조금 문제가 될 수 있습니다. 이 점에 대해서는 질문 [*]1.14, [*]1.15를 보기 바랍니다.



Q 2.4
C 언어에서 추상화된 데이터 타입을 구현하는 가장 좋은 방법을 알려주세요.
Answer
한 가지 방법은 사용자들이 구조체 포인터를 쓰게 하는 것입니다 -- 이 때, typedef 이름을 쓰는 것이 좋습니다. 이 포인터는 어떤 구조체를 가리키는 것이며, 이 구조체의 세부 사항은 사용자에게 알려줄 필요가 없습니다. 다시 말해서, client는 이 구조체가 어떠한 멤버를 포함하고 있는 지, 전혀 알 필요없이, 이 구조체 포인터를 사용합니다. (함수를 부를때, 이 포인터를 넘겨주며, 이 구조체 포인터를 리턴하는 등) 구조체에 대한 자세한 사항을 몰라도 될 경우에 -- -> 연산자나 sizeof 연산자가 쓰이지 않는다면 -- C 언어로 완전하지 않은, incomplete type에 대한 포인터를 쓸 수 있습니다. 단지, 이 구조체를 실제 다루는 함수가 들어있는 파일안에서 완전한 정의를 제공하면 됩니다. 덧붙여 질문 [*]11.5도 참고하시기 바랍니다.

Note
표준 입출력 함수들이 FILE * 타입의 인자를 받는 것을 생각하면 쉽습니다.



Q 2.5
아래와 같이 선언했더니, 이상한 경고 메시지(“struct x introduced in prototype scope” 또는 “struct x declared inside parameter list”)가 발생합니다:
  extern f(struct x *p);
Answer
질문 [*]11.5를 보기 바랍니다.



Q 2.6
구조체를 다음과 같이 정의하는 코드를 봤습니다:
  struct name {
    int namelen;
    char namestr[1];
  };
그리고나서 namestr에 공간을 할당하고 (allocation), 배열 namestr이 여러 element를 가진 것처럼 쓰는 것을 봤습니다. 이게 안전한 방법인가요?
Answer
합법적인지(legal), 이식성이 뛰어난 지는 확실하지 않으나, 매우 인기있는 방법입니다. 보통 다음과 같은 코드를 써서 위 구조체를 사용합니다:
  #include <stdlib.h>
  #include <string.h>

  struct name *makename(char *newname)
  {
    struct name *ret =
      malloc(sizeof(struct name) - 1 + 
              strlen(newname) + 1);
        /* -1 for initial [1]; +1 for \0 */
    if (ret != NULL) {
      ret->namelen = strlen(newname);
      strcpy(ret->namestr, newname);
    }
    return ret;
  }
이 함수는 name 구조체가, 그 멤버인 namestr이 주어진 문자열을 충분히 포함할 수 있도록 (단순히 크기 1인 배열이 아닌) 공간을 할당합니다.

이것은 인기있는 테크닉 중의 하나이지만 Dennis Ritchie씨는 이 방법을 “unwarranted chumminess with the C implementation”이라고 부릅니다. 이 방법은 공식적인 C 표준에 정확히 부합하지는 않지만, (이것이 표준에 부합하는지 아닌지에 대한 열렬한 논쟁이 있지만, 이 책의 범위를 벗어나므로 생략합니다.) 현존하는 거의 모든 컴파일러에서 동작합니다. (배열의 경계를 검사해주는 컴파일러에서는 경고를 출력할 수도 있습니다.)

또 한가지 방법으로는 가변적인 요소를 매우 작게 잡든 대신 아주 크게 잡는 것입니다; 위의 예에 적용하자면:

  #include <stdlib.h>
  #include <string.h>

  #define MAX 100

  struct name {
    int namelen;
    char namestr[MAX];
  };

  struct name *makename(char *newname)
  {
    struct name *ret =
      malloc(sizeof(struct name) - MAX +
             strlen(newname) + 1);
        /* -1 for initial [1]; +1 for \0 */
    if (ret != NULL) {
      ret->namelen = strlen(newname);
      strcpy(ret->namestr, newname);
    }
    return ret;
  }
이 때 MAX는 예상되는 저장될 문자열보다 크게 잡습니다. 그러나 이 방법도 표준에 정확히 부합하는 방법도 아닙니다. 게다가 이런 구조체를 쓸 때에는 매우 주의를 기울여야 합니다. 왜냐하면 이런 경우에는 컴파일러보다 프로그래머가 그 크기에 대해 더욱 잘 알고 있기 때문에 (특히, 이런 경우 pointer로만 작업을 할 수 있습니다.) 컴파일러가 알려주는 정보(경고나 에러)가 의미없게 됩니다.

가장 올바른 방법은, 다음과 같이, 배열 대신에 문자 포인터를 쓰는 것입니다:

  #include <stdlib.h>
  #include <string.h>

  struct name {
    int namelen;
    char *namep;
  };

  struct name *makename(char *newname)
  {
    struct name *ret = malloc(sizeof(struct name));
    if (ret != NULL) {
      ret->namelen = strlen(newname);
      ret->namep = malloc(ret->namelen + 1);
      if (ret->namep == NULL) {
        free(ret);
        return NULL;
      }
      strcpy(ret->namestr, newname);
    }
    return ret;
  }
위와 같이 하면, 문자열의 길이와 실제 문자열을 하나의 메모리 블럭에 저장한다는 “편리함”이 사라집니다. 또한 위와 같이 할당한 메모리를 돌려주기 위해서는 free를 두 번 불러야 합니다; 질문 [*]7.23을 참고하기 바랍니다.

만약에, 위와 같이, 저장할 데이터 타입이 문자(character)라면, malloc을 두 번 부르는 것을 다시 한 번으로 줄여서, 연속성을 보장할 수 있는 방법이 있습니다. (따라서 free를 한 번만 불러도 됩니다):

  struct name *makename(char *newname)
  {
    char *buf = malloc(sizeof(struct name) +
                       strlen(newname) + 1);
    struct name *ret = (struct name *)buf;
    ret->namelen = strlen(newname);
    ret->namep = buf + sizeof(struct name);
    strcpy(ret->namep, newname);

    return ret;
  }
그러나, 위와 같이, malloc을 한 번 불러서, 두번째 영역까지 할당하는 것은, 두 번째 영역이 char 배열로 취급될 경우에만 이식성이 있습니다. 다른, 더 큰 데이터 타입을 쓴다면, alignment (질문 [*]2.12, [*]16.7 참고) 문제가 발생할 가능성이 높습니다.

[C9X]에서는 “flexible array member”라는 개념을 소개하고 있고, 이는 배열이 구조체의 마지막 멤버로써 쓰일 때에는 배열의 크기 지정을 생략할 수 있도록 해 줍니다.

References
[ANSI Rationale] § 3.5.4.2
[C9X] § 6.5.2.1

Note
C99 표준에 소개된, “flexible array member”를 쓰면, 이 문제를 깔끔하게 해결할 수 있습니다. 반드시 질문 [*]11.G를 읽기 바랍니다.

2.2 Structure Operations



Q 2.7
Structure가 변수에 대입되고 함수 인자로 전달될 수 있다고 들었지만, [K&R1]에서는 그렇지 않다고 하는군요.
Answer
[K&R1]에서도, 앞으로 만들어질 컴파일러에서는 이러한 제한들이 없을 것이라고 씌여 있습니다. 그리고 실제로도, Ritchie가 만든 컴파일러에서도 ([K&R1] 출판 이후) 구조체를 대입하고, 함수 인자로 전달하고, 구조체를 리턴하는 모든 연산들이 허용되었습니다. 아주 오래된 몇몇 C 컴파일러의 경우, 이 제한이 남아있지만 대부분의 컴파일러들은 구조체 연산을 지원합니다. 그리고 이 연산들은 이제 ANSI C 표준의 일부가 되었습니다. 따라서 전혀 거리낌없이 쓰셔도 좋습니다.2.2

(구조체가 복사, 전달, 리턴되는 경우, 복사 작업은 통채로(monolithically) 이루어지기 때문에 구조체 안의 포인터 멤버 필드가 가리키는 데이터는 복사되지 않는다는 것에 주의하시기 바랍니다.)

질문 [*]14.11의 코드를 참고하기 바랍니다.

References
[K&R1] § 6.2 p. 121
[K&R2] § 6.2 p. 129
[ANSI] § 3.1.2.5, § 3.2.2.1, § 3.3.16
[C89] § 6.1.2.5, § 6.2.2.1, § 6.3.16
[H&S] § 5.6.2 p. 133



Q 2.8
==!=를 써서 구조체를 비교할 수 없나요?
Answer
C 언어의 저수준성을 고려해서, 구조체 비교를 구현하기 위한 좋은 방법은, 컴파일러 입장에서, 존재하지 않습니다. 간단히 바이트 단위로 비교하는 것은, 구조체 필드 사이에 있을지도 모르는 “hole”들을 생각할 때 좋지 않습니다. (이러한 `hole'은 각 필드가 주어진 정렬(alignment) 방법에 맞도록 위치시키기 위해 필요합니다. 질문 [*]2.12를 참고하시기 바랍니다.) 또한 구조체안에 많은 필드가 있을 때, 모든 것을 필드 단위로 비교하는 것은, 반복되는 코드가 많아서 적당하지 않을 수도 있습니다. 또 컴파일러가 자동으로 만들어 낸 비교 코드가 모든 경우에, 구조체 포인터 멤버들을 제대로 비교할 수 있는 것도 어렵습니다; 예를 들어, 대부분의 경우에, char * 타입의 필드는 ==로 비교하는 것보다 strcmp로 비교하는 것이 더 바람직합니다. (질문 [*]8.2 참고)

여러분이 두 개의 구조체를 비교하길 원한다면, 필드 단위로 구조체를 비교하는 함수를 직접 만들어야 합니다.

References
[K&R2] § 6.2 p. 129
[ANSI] 4.11.4.1 footnote 136
[ANSI Rationale] § 3.3.9
[H&S] § 5.6.2 p. 133



Q 2.9
구조체를 전달하거나, 리턴할 때 쓰면, 실제로 어떻게 구현되는 것인가요?
Answer
함수의 인자로 구조체가 전달되면, 구조체의 모든 값이 스택에 저장됩니다. (대부분의 프로그래머가 이 복사에 해당하는 overhead를 줄이기 위해서 구조체를 직접 전달하지 않고, 대신 구조체의 포인터를 씁니다.) 어떤 컴파일러들은 자동으로 구조체를 가리키는 포인터를 전달하기도 하지만, 때때로 pass-by-value 개념을 위해, 따로 복사본을 만들어야 할 필요가 발생합니다.

함수의 리턴 타입으로 구조체가 쓰이면, 대부분은 따로 컴파일러가 마련한, 보이지 않은 곳, 일반적으로 함수의 인자 형태로 저장됩니다. 어떤 오래된 컴파일러는 구조체를 리턴할 때 쓸 목적으로 특별한, static 공간을 마련합니다. 이런 경우, 구조체를 리턴하는 함수가 다시 재진입(reentrant)할 수 없기 때문에, ANSI C에 부합하지 않습니다.

References
[ANSI] § 2.2.3
[C89] § 5.2.3



Q 2.10
구조체 인자를 받아들이는 함수에 상수값을 (constant value) 전달할 수 있나요?
Answer
이 글을 쓸 당시 C 언어에는 이름 없는(anonymous) 구조체를 만들 방법이 없었습니다. 따라서 임시 구조체 변수를 만들거나 구조체를 리턴하는 함수를 써야 합니다. 질문 [*]14.11을 보기 바랍니다. (GNU C 컴파일러는 구조체 상수를, 추가 기능으로 제공하며, 이후 C 표준에 채택될 가능성이 높습니다.) 질문 [*]4.10을 참고하기 바랍니다.

드디어, [C9X] 표준은 “compound literal”이라는 개념을 소개합니다; `compound literal'의 한가지 형태는 구조체 상수를 쓸 수 있게 해 줍니다. 예를 들어, struct point 타입의 인자를 받는 plotpoint()에 상수 구조체를 전달하려면 다음과 같이 할 수 있습니다:

  plotpoint((struct point){1, 2});

(또다른 [C9X] 표준인) “designated initializer”라는 개념을 함께 쓰면, 각각의 멤버 이름을 지정할 수도 있습니다:

  plotpoint((struct point){.x = 1, .y = 2});

References
[C9X] § 6.3.2.5, § 6.5.8



Q 2.11
구조체를 화일에서 읽거나 쓰는 방법은?
Answer
구조체를 화일에 쓸 경우는 대개 fwrite() 함수를 씁니다:

  fwrite(&somestruct, sizeof somestruct, 1, fp);

그리고, 이렇게 쓴 데이터는 fread()를 써서 읽을 수 있습니다. 그러면 fwrite가 구조체를 가리키고 있는 포인터를 써서, 주어진 구조체가 저장되어 있는 메모리의 내용을 파일에 기록합니다. (fread의 경우에는 파일에서 읽어옵니다.) 이 때 sizeof 연산자는 복사할 byte 수를 지정합니다.

ANSI 호환의 컴파일러를 쓰고, 함수 선언이 되어 있는 헤더 파일을 (대개 <stdio.h>) 포함했으면 위와 같이 쓰는 것이 좋습니다. 만약 ANSI 이전의 컴파일러를 쓰고 있다면, 첫번째 인자를 다음과 같이 캐스팅해주어야 합니다:

  fwrite((char *)&somestruct, sizeof somestruct, 1, fp);
여기서 중요한 것은, fwrite가 구조체에 대한 포인터가 아니라, 바이트를 가리키는 포인터를 받는다는 것입니다.

위와 같이 쓰여진 데이터 화일은 (특히 구조체가 포인터나 실수(floating point)를 포함하고 있을 때) 이식성이 없습니다. (질문 [*]2.12와 [*]20.5를 참고하기 바랍니다). 구조체가 저장되는 메모리 이미지는 컴퓨터와 컴파일러에 매우 의존적입니다. 각각 다른 컴파일러는 각각 다른 크기의 padding을 사용할 수 있으며, 바이트 크기와 순서(endian)가 다를 수 있습니다. 한 시스템에서 구조체를 어떤 파일에 쓰고(write), 다른 시스템에서 이 파일의 내용을 올바르게 읽는 것이 중요하다면, 질문 [*]2.12와 [*]20.5를 참고하기 바랍니다.

만약 구조체가 포인터를 (char * 타입의 문자열이나 다른 구조체를 가리키고 있는 포인터) 포함하고 있었다면, 단지 포인터 값만 기록되기 때문에, 화일에서 이 데이터를 읽을 경우, 의미없는 값이 됩니다. 또한 데이터 파일의 이식성을 높이기 위해서는 fopen()을 호출할 때, “b” flag을 써야 합니다; 질문 [*]12.38을 참고하시기 바랍니다.

좀 더 이식성이 높은 방법은, 구조체를 필드 단위로 읽고 쓰는 함수를 만들어 쓰는 것입니다.

References
[H&S] § 15.13 p. 381

2.3 Structure Padding



Q 2.12
제 컴파일러는 구조체 안에 `hole'을 만들어 넣어서 공간을 낭비하고 외부 데이터 화일에 “binary” I/O를 불가능하게 합니다. 이 `padding' 기능을 끄거나 구조체 필드의 alignment를 조정할 수 있을까요?

Answer
대부분의 컴퓨터는 데이터들이 적절하게 align되어 있을 때, 메모리를 가장 효과적으로 읽고 쓸 수 있습니다. 예를 들어 byte-address machine에서 short int나 크기가 2인 데이터는 짝수로 된 주소에 위치해 있는 것이 가장 효과적이며, long int나 크기가 4인 데이터는 4의 배수로 된 주소에 위치하는 것이 좋습니다. 어떤 머신에서는, 위와 달리 제대로 위치해 있지 않은 데이터는 아예 읽고 쓸 수 없을 수 있습니다.

다음과 같은 구조체가 있다고 가정해 봅시다:

  struct {
    char c;
    int i;
  };
대부분의 컴파일러에서 위와 같은 구조체를 만들 때, charint 사이에 어떤 `hole'을 만들어서 int 값이 제대로 align될 수 있도록 해 줍니다. (이렇게 두번째 필드를 첫번째 필드를 기준으로 하여, 증가하는 방식의 정렬을 (incremental alignment) 쓰는 것은, 이 구조체 자체가 올바르게 정렬되어 있다는 것을 가정한 것입니다. 결국 컴파일러는 malloc이 하는 것처럼, 구조체를 할당할 때 올바른 align을 보장해 주어야 합니다.)

아마도 컴파일러에서 이런 (`padding'이나 `hole'을 어떻게 쓰는지) 제어를 할 수 있는 방법을 제공할 것입니다. (#pragma를 써서 할 수 있습니다; 질문 [*]11.20을 참고하기 바랍니다), 그러나 이런 제어를 위한 표준 방법은 없다는 것을 아셔야 합니다.

이러한 padding으로 발생하는, 낭비되는 공간이 염려된다면, 구조체의 멤버를, 큰 크기에서 작은 크기 순으로 정의하면, 낭비되는 공간을 줄일 수 있습니다. bit field를 사용하면 더욱 많은 공간을 절약할 수 있으나, bit field 나름대로의 단점도 존재합니다. (질문 [*]2.26을 참고하기 바랍니다.)

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

References
[K&R2] § 6.4 p. 138
[H&S] § 5.6.4 p. 135



Q 2.13
구조체 타입에 sizeof 연산자를 썼더니, 제가 예상한 것보다 훨씬 큰 값을 리턴합니다. 왜 그러죠?
Answer
구조체는 필요한 경우 이러한 `padding' 공간을 포함할 수 있습니다. 이는 구조체가 배열로 만들어질 때, 정렬(alignment) 속성이 보존되도록 하기 위한 것입니다. 또 배열로 쓰이지 않을 경우에도 이러한 여분의 `padding'이 남아 있을 수 있습니다. 이는 sizeof가 일관된 크기를 리턴하게 하기 위한 것입니다. 질문 [*]2.12를 참고하기 바랍니다.
References
[H&S] § 5.6.7 pp. 139-40

2.4 Accessing Members



Q 2.14
구조체 안에서 각각의 필드에 대한 byte offset을 얻을 수 있나요?
Answer
ANSI C는 offsetof() 매크로를 정의합니다. <stddef.h>를 보시기 바랍니다. 만약에 이 매크로가 없다면 다음과 같이 만들 수 있습니다:

  #define offsetof(type, mem) ((size_t) \
    ((char *)&((type *)0)->mem - (char *)(type *)0))

이 방법은 100% 이식성이 뛰어난 것이 아닙니다. 어떤 컴파일러에서는 이 방법을 쓸 수 없을 수 있습니다.

(복잡하기 때문에 조금 더 설명하면, 잘 캐스팅된 널 포인터를 빼는 것은 널 포인터가 내부적으로, 0이 아닌 다른 값이더라도 offset 값을 얻을 수 있게 보장해 줍니다. (char *)로 캐스팅하는 것은 offset이 byte offset 단위로 결과를 얻을 수 있게 하기 위한 것입니다. 호환성이 없는 부분이 하나 있는데, 주소 계산을 할 때, type 오브젝트가 주소 0번지에 있다고 속이는 부분에 있습니다만, 실제로 참조되지 않기 때문에 access violation이 일어날 가능성은 없습니다.) 쓰는 방법을 알기 위해, 질문 [*]2.15를 참고하기 바랍니다.

References
[ANSI] § 4.1.5
[C89] § 7.1.6
[ANSI Rationale] § 3.5.4.2
[H&S] § 11.1 pp. 292-3



Q 2.15
실행 시간(run-time)에 구조체 필드를 이름으로 access할 수 있습니까?
Answer
각각의 필드 이름과 offset을 offsetof() 매크로를 써서 테이블에 저장해 두면 됩니다. struct a에서 필드 `b'의 오프셋은 다음과 같이 얻을 수 있습니다:

  offsetb = offsetof(struct a, b)

만약 이 구조체 변수를 가리키는 포인터 structp가 있고, 필드 `b'가 int일때, 위에서 계산한 offsetb를 쓰면 b의 값을 다음과 같이 설정할 수 있습니다:

  *(int *)((char *)structp + offsetb) = value;



Q 2.16
C 언어에, Pascal의 with 문장과 같은 기능이 있습니까?
Answer
질문 [*]20.23을 보기 바랍니다.

2.5 Miscellaneous Structure Questions



Q 2.17
배열의 이름이, 배열의 첫번째 요소를 가리키는 포인터처럼 동작한다면, 왜 구조체 이름은 비슷한 방식으로 동작하지 않을까요?
Answer
The rule (see question [*]6.3) that causes array references to “decay” into pointers is a special case that applies only to arrays and that reflects their “second-class” status in C. (An analogous rule applies to functions.) Structures, however, are first-class objects: When you mention a structure, you get the entire structure.



Q 2.18
이 프로그램은 정상적으로 동작하지만 프로그램이 끝났을 때, core 파일을 만들어 냅니다. 왜 그런가요?

  struct list {
    char *item;
    struct list *next;
  }

    /* Here is the main program.  */

    main(argc, argv)
    { ...  }

Answer
구조체를 정의할 때 세미콜론(`;')을 빠뜨렸기 때문에 main()이 구조체를 리턴하는 함수로 정의되어 버렸습니다. (중간에 들어간 주석(comment) 때문에 이 버그를 찾아내기가 더욱 힘들 것입니다) 대개 구조체를 리턴하는 함수들은 숨겨진 리턴 포인터(hidden return pointer)를 추가하는 식으로 구현되기 때문에, main()이 세개의 인자를 받는 것처럼 만들어집니다. 원래 C start-up code는 main()이 두 개의 인자를 받는 것으로 짜여져 있으므로, 이 경우 정상적으로 동작할 수 없습니다. 질문 [*]10.9와 [*]16.4를 참고하기 바랍니다.

References
[CT&P] § 2.3 pp. 21-2

2.6 Unions



Q 2.19
structure와 union은 어떻게 서로 다른가요?
Answer
union은 모든 필드가 서로 겹쳐서 존재한다는 것을 제외하고는, structure와 같습니다; 따라서 union에서는 한 번에 하나의 필드만을 쓸 수 있습니다. (물론 하나의 필드에 쓰고(write), 다른 필드에서 읽어서, 어떤 타입의 비트 패턴을 조사하거나 다른 식으로 해석하기 위해서 쓸 수도 있지만, 이는 상당히 machine dependent한 것입니다.) union의 크기는, 각각의 멤버들 중 가장 큰 멤버의 사이즈가 됩니다. 반면에 structure의 크기는 각각의 멤버들의 크기를 다 더한 값입니다. (두 가지 경우 모두, 적절한 padding 값이 더해질 수 있습니다; 질문 [*]2.12와 [*]2.13을 참고하기 바랍니다.)
References
[ANSI] § 3.5.2.1
[C89] § 6.5.2.1
[H&S] § 5.7 pp. 140-5 esp. § 5.7.4



Q 2.20
union을 초기화(initialization)할 수 있습니까?
Answer
현재 C 표준은 union 안에, 이름이 있는 첫번째 멤버의 초기화만 인정합니다. (ANSI 이전의 컴파일러에서는 union을 초기화할 수 있는 방법이 전혀 없습니다.)

union을 초기화하기 위해, 여러 제안이 있었지만 아직 채택된 것은 없습니다. (GNU C 컴파일러는 어떤 멤버라도 초기화할 수 있는 확장 기능을 제공하며, 곧 이 기능이 표준으로 채택될 가능성이 높습니다.) If you're really desperate, you can sometimes define several variant copies of a union, with the members in different orders, so that you can declare and initialize the one having the appropriate first member. (These variants are guaranteed to be implemented compatibly, so it's okay to “pun” them by initializing one and then using the other.)

[C9X]는 “designated initializer”를 소개하고 있으며, 어떠한 멤버의 초기값도 쓸 수 있도록 하고 있습니다.

Note
[C9X]에 소개된 예는 다음과 같습니다:
  union { /* ... */ } u = { .any_member = 42 };
즉, 위의 예는 union u의 멤버인 “any_member”를 42로 초기화하고 있습니다.
References
[K&R2] § 6.8 pp. 148-9
[C89] § 6.5.7
[C9X] § 6.7.8
[H&S] § 4.6.7 p. 100



Q 2.21
union의 어떤 필드가 현재 쓰여지고 있는지 알 수 있는 방법이 있을까요?
Answer
없습니다. 여러분이 직접 어떤 필드가 쓰여지고 있는지 기록해 두어야 합니다.
  struct taggedunion {
    enum { UNKNOWN, INT, LONG, DOUBLE, POINTER } code;
    union {
      int i;
      long l;
      double d;
      void *p;
    } u;
  };
위와 같이 만들고, union에 값을 쓸(write) 때마다 code를 올바른 값으로 설정하면 됩니다; 컴파일러가 자동으로 이런 것을 해 주지는 않습니다. (C 언어에서 union은 Pascal의 variant record와는 다릅니다.)
References
[H&S] § 5.7.3 pp. 143

2.7 Enumerations



Q 2.22
enumeration(열거형)을 쓰는 것과 #define으로 정의한 매크로를 쓰는 것과 차이가 있습니까?
Answer
현재 이 둘에는 약간의 차이가 있습니다. C 표준은 `enumeration이 에러없이 정수형 타입으로 쓰일 수 있다'고 정의할 수 있습니다. (달리 표현하면, 이렇게 명백하게 캐스트를 하지 않고 섞어쓸 수 없었다면 현명하게 enumeration을 썼을 때에만 프로그래밍 에러를 잡아 줄 수 있었을 겁니다. If such intermixing were disallowed without explicit casts, judicious use of enumerations could catch certain programming errors.)

enumeration을 썼을 때에 좋은 점은, 수치 값이 자동적으로 대입되기 때문에, 디버거(debugger)가 열거형 변수를 검사할 때 심볼(symbol) 값으로 보여줄 수 있다는 점입니다. (enumeration과 정수형을 섞어쓰는 것이 오류는 아니지만, 좋은 스타일이 아니기 때문에 어떤 컴파일러는 가벼운 경고를 출력하기도 합니다.) enumeration을 쓸 때의 단점은 이러한 사소한 경고를 프로그래머가 처리해 줘야 한다는 것입니다; 어떤 프로그래머들은 enumeration 변수의 크기를 제어할 수 없다는 것에 불평하기도 합니다.

References
[K&R2] § 2.3 p. 39, § A4.2 p. 196
[ANSI] § 3.1.2.5, § 3.5.2, § 3.5.2.2, Appendix E
[C89] § 6.1.2.5, § 6.5.2, § 6.5.2.2, Annex F
[H&S] § 5.5 pp. 127-9, § 5.11.2 p. 153
Note
최신의 GCC에 적절한 디버깅 옵션을 주면, 디버거 GDB에서, 매크로도 값이 아닌, 이름(symbol 값)으로 보여줍니다. 따라서 이 경우, enum이 가지는 장점 하나는 없어진다고 말할 수 있습니다.



Q 2.23
enumeration은 이식성이 있나요?
Answer
Enumeration은 그리 오래지 않아 C 언어에 포함되었습니다. (K&R1에는 없었습니다.) 그렇지만 확실하게 C 언어의 일부가 되었으며, 따라서 모든 현대 컴파일러들은 이를 지원합니다. They're quite portable, although historical uncertainty about their precise definition led to their specification in the Standard being rather weak. (질문 [*]2.22를 참고하기 바랍니다.)



Q 2.24
열거형 값을 심볼로 출력할 수 있는 간단한 방법이 있을까요?
Answer
없습니다. 열거형 값을 문자열로 매핑(mapping)시켜주는 함수를 직접 만들어야 합니다. (디버깅 목적으로, 성능 좋은 디버거들은 자동으로 열거형 값을 심볼로 보여주기도 합니다.)

2.8 Bitfields



Q 2.25
아래와 같이 구조체 선언에 콜론(:)을 쓰는게 뭐죠?
  struct record {
    char *name;
    int refcount : 4;
    unsigned dirty : 1;
  };
Answer
비트 필드(bitfield)라고 합니다; 콜론(:) 다음에 오는 것은, 콜론 앞 필드의 정확한 크기를 비트 단위로 나타냅니다. (C 언어를 자세히 다루는 여러 책에서 잘 다루고 있습니다.) 여러 비트 단위의 flag이나 작은 값들을 저장하는 데 bitfield를 사용하면 공간을 절약할 수 있습니다. 어떤, 알려진 저장 방식에 정확히 일치하는 구조체를 만드는데 쓰기도 합니다. (Their success at the latter task is mitigated by the fact that bitfields are assigned left to right on some machines and right to left on others.)

콜론을 써서 비트 필드의 크기를 정하는 것은 구조체나 union에서만 쓸 수 있습니다. 다른 변수 타입의 크기를 직접 정하려고 쓸 수 없습니다. (질문 [*]1.2와 [*]1.3 참고)

References
[K&R1] § 6.7 pp. 136-8
[K&R2] § 6.9 pp. 149-50
[ANSI] § 3.5.2.1
[C89] § 6.5.2.1
[H&S] § 5.6.5 pp. 136-8



Q 2.26
Why do people use explicit masks and bit-twiddling code so much instead of declaring bitfields?
Answer
Bitfields are thought to be nonportable, although they are no less portable than other parts of the language. You don't know how large they can be, but that's equally true for values of type int. You don't know by default whether they're signed, but that's equally true of type char. You don't know whether they're laid out from left to right or right to left in memory, but that's equally true of the bytes of all types and matters only if you're trying to conform to externally imposed storage layout. (Doing so is always nonportable; see also question [*]2.12 and [*]20.5.)

Bitfields are inconvenient when you also want to be able to manipulate some collection of bits as a whole (perhaps to copy a set of flags). You can't have arrays of bitfields; see also question [*]20.8. Many programmers suspect that the compiler won't generate good code for bitfields; historically, this was sometimes true.

Straightforward code using bitfields is certainly clearer than the equivalent explicit masking instructions; it's too bad that bitfields can't be used more often.

Seong-Kook Shin
2018-05-28