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에 대해 다룹니다.
struct x1 { ... }; typedef struct { ... } x2;
x2 b;tag를 써서 정의된 구조체는 다음과 같이 선언해야 합니다.2.1
struct x1 a;(두 가지 방식 모두 적용해서 다음과 같이 정의할 수도 있습니다:
typedef struct x3 { ... } x3;조금 혼동스럽기는 하지만, tag 이름과 typedef 이름이 중복되는 것은, 서로 다른 namespace에 속하기 때문에, 전혀 상관없습니다. 질문 1.29를 보기 바랍니다.)
struct x { ... }; x thestruct;
struct x thestruct;원한다면, 구조체를 선언할 때, typedef 이름을 같이 선언하고, 이 typedef 이름으로 실제 구조체 변수 또는 상수를 만들 때 쓸 수 있습니다:
typedef struct { ... } tx; tx thestruct;덧붙여 질문 2.1도 참고하시기 바랍니다.
->
연산자나
sizeof 연산자가 쓰이지 않는다면 -- C 언어로 완전하지 않은,
incomplete type에 대한 포인터를 쓸 수 있습니다. 단지, 이 구조체를
실제 다루는 함수가 들어있는 파일안에서 완전한 정의를 제공하면 됩니다.
덧붙여 질문
11.5도 참고하시기 바랍니다.
extern f(struct x *p);
struct name { int namelen; char namestr[1]; };그리고나서 namestr에 공간을 할당하고 (allocation), 배열 namestr이 여러 element를 가진 것처럼 쓰는 것을 봤습니다. 이게 안전한 방법인가요?
#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”라는 개념을 소개하고 있고, 이는 배열이 구조체의 마지막 멤버로써 쓰일 때에는 배열의 크기 지정을 생략할 수 있도록 해 줍니다.
(구조체가 복사, 전달, 리턴되는 경우, 복사 작업은 통채로(monolithically) 이루어지기 때문에 구조체 안의 포인터 멤버 필드가 가리키는 데이터는 복사되지 않는다는 것에 주의하시기 바랍니다.)
==
나 !=
를 써서 구조체를 비교할 수 없나요?
==
로 비교하는 것보다 strcmp로 비교하는 것이 더
바람직합니다. (질문
8.2 참고)
여러분이 두 개의 구조체를 비교하길 원한다면, 필드 단위로 구조체를 비교하는 함수를 직접 만들어야 합니다.
함수의 리턴 타입으로 구조체가 쓰이면, 대부분은 따로 컴파일러가 마련한, 보이지 않은 곳, 일반적으로 함수의 인자 형태로 저장됩니다. 어떤 오래된 컴파일러는 구조체를 리턴할 때 쓸 목적으로 특별한, static 공간을 마련합니다. 이런 경우, 구조체를 리턴하는 함수가 다시 재진입(reentrant)할 수 없기 때문에, ANSI C에 부합하지 않습니다.
드디어, [C9X] 표준은 “compound literal”이라는 개념을 소개합니다; `compound literal'의 한가지 형태는 구조체 상수를 쓸 수 있게 해 줍니다. 예를 들어, struct point 타입의 인자를 받는 plotpoint()에 상수 구조체를 전달하려면 다음과 같이 할 수 있습니다:
plotpoint((struct point){1, 2});
(또다른 [C9X] 표준인) “designated initializer”라는 개념을 함께 쓰면, 각각의 멤버 이름을 지정할 수도 있습니다:
plotpoint((struct point){.x = 1, .y = 2});
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을 참고하시기 바랍니다.
좀 더 이식성이 높은 방법은, 구조체를 필드 단위로 읽고 쓰는 함수를 만들어 쓰는 것입니다.
다음과 같은 구조체가 있다고 가정해 봅시다:
struct { char c; int i; };대부분의 컴파일러에서 위와 같은 구조체를 만들 때, char와 int 사이에 어떤 `hole'을 만들어서 int 값이 제대로 align될 수 있도록 해 줍니다. (이렇게 두번째 필드를 첫번째 필드를 기준으로 하여, 증가하는 방식의 정렬을 (incremental alignment) 쓰는 것은, 이 구조체 자체가 올바르게 정렬되어 있다는 것을 가정한 것입니다. 결국 컴파일러는 malloc이 하는 것처럼, 구조체를 할당할 때 올바른 align을 보장해 주어야 합니다.)
아마도 컴파일러에서 이런 (`padding'이나 `hole'을 어떻게 쓰는지) 제어를 할 수 있는
방법을 제공할 것입니다.
(#pragma
를 써서 할 수 있습니다; 질문
11.20을 참고하기
바랍니다), 그러나 이런 제어를 위한 표준 방법은 없다는 것을 아셔야
합니다.
이러한 padding으로 발생하는, 낭비되는 공간이 염려된다면, 구조체의 멤버를, 큰 크기에서 작은 크기 순으로 정의하면, 낭비되는 공간을 줄일 수 있습니다. bit field를 사용하면 더욱 많은 공간을 절약할 수 있으나, bit field 나름대로의 단점도 존재합니다. (질문 2.26을 참고하기 바랍니다.)
<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를 참고하기 바랍니다.
offsetb = offsetof(struct a, b)
만약 이 구조체 변수를 가리키는 포인터 structp가 있고, 필드 `b'가 int일때, 위에서 계산한 offsetb를 쓰면 b의 값을 다음과 같이 설정할 수 있습니다:
*(int *)((char *)structp + offsetb) = value;
struct list { char *item; struct list *next; } /* Here is the main program. */ main(argc, argv) { ... }
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”를 소개하고 있으며, 어떠한 멤버의 초기값도 쓸 수 있도록 하고 있습니다.
union { /* ... */ } u = { .any_member = 42 };즉, 위의 예는 union u의 멤버인 “
any_member
”를
42로 초기화하고 있습니다.
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와는 다릅니다.)
#define
으로 정의한
매크로를 쓰는 것과 차이가 있습니까?
enumeration을 썼을 때에 좋은 점은, 수치 값이 자동적으로 대입되기 때문에, 디버거(debugger)가 열거형 변수를 검사할 때 심볼(symbol) 값으로 보여줄 수 있다는 점입니다. (enumeration과 정수형을 섞어쓰는 것이 오류는 아니지만, 좋은 스타일이 아니기 때문에 어떤 컴파일러는 가벼운 경고를 출력하기도 합니다.) enumeration을 쓸 때의 단점은 이러한 사소한 경고를 프로그래머가 처리해 줘야 한다는 것입니다; 어떤 프로그래머들은 enumeration 변수의 크기를 제어할 수 없다는 것에 불평하기도 합니다.
struct record { char *name; int refcount : 4; unsigned dirty : 1; };
콜론을 써서 비트 필드의 크기를 정하는 것은 구조체나 union에서만 쓸 수 있습니다. 다른 변수 타입의 크기를 직접 정하려고 쓸 수 없습니다. (질문 1.2와 1.3 참고)
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