세가지 새로운 타입 소개: structures, unions, enumerations
structure: collection of values(members), possibly of different types.
union: similar to a structure, except that tis members share the same storage -> able to store one member at a time, not all members simultaneously
enumeration: integer type whose values are named by the programmer
structure 가장 중요함. 1,2,3절은 이에 대해 다룸
16.1 how to declare structure variables and perform basic operations on them
16.2 how to define structure types, write functions accept structure args or return structures
16.3 how arrays and structures can be nested
16.4 unions
16.5 enumeration
16.1 Structure Variables
지금까지 다로운 유일한 데이타 구조는 배열. 배열의 중요한 두 가지 특징: 첫째, 모든 원소들이 같은 type. 둘째, 배열의 원소를 선택하려면, 그 포지션을 특정해 주어야 함.
structure(구조체)의 특징은 배열과 꽤나 다름. 구조체의 원소들(C용어로는 members)은 같은 타입이 아니어도 된다. 또 구조체들의 멤버들은 이름을 가지고 있다. 그들을 선택할 때 포지션으로 하는 것이 아니라 이름으로 한다.
다른 언어들에서도 비슷한 개념을 가지고 있는데, 구조체는 records, 그 멤버들은 fields라고 하는 언어들이 있음.
Declaring Structure Variables
관련되어 있는 데이터 아이템들의 콜렉션을 저장하고 싶을 때 구조체를 사용한다. 예를 들어 창고에 있는 부품들을 기록할 때.
각각의 부품에 대해, 부품의 번호(an integer), 부품의 이름(a string of characters), 부품의 재고(an integer)의 정보를 저장해야 함.
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1, part2;
각각의 구조체 변수는 세개의 멤버를 가지고 있다: number(부품번호), name(부품의 이름), on_hand(부품의 재고)
part1, part2는 해당 타입의 변수이다.
구조체의 멤버가 메모리에 저장되는 방식은 선언된 순서대로이다. 다음과 같이 가정한다면
(1) part1이 주소 2000에 저장됨
(2) 정수는 4바이트를 차지
(3) NAME_LEN의 값은 25
(4) 멤버들 사이에 gap이 없음
part1의 모습은 다음과 같다.
각각의 구조체는 새로운 자기만의 scope를 갖는다; 그 scope 안에서 선언된 모든 이름은 프로그램 내의 다른 이름과 충돌하지 않는다.(C용어로는, 각각의 스트럭쳐는 그 멤버들을 위한 독립된 name space를 갖는다)
Initializing Structure Variables
배열과 마찬가지로, 구조체 변수는 선언과 동시에 초기화될 수 있다. 구조체를 초기화하기 위해서는 구조체에 저장될 값들의 리스트를 중괄호 안에 넣는다.
struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} part1 = {528, "Disk drive", 10},
part2 = {914, "Printer cable", 5};
initializer안에 있는 변수는 구조체의 멤버와 같은 순서대로 써야 한다. part1의 경우, 그 number 멤버는 528, name 멤버는 "Disk drive", on_hand 멤버는 10이다.
Structure initializer의 규칙은 array initializer의 규칙과 비슷하다. in'zer 안의 expression은 모두 상수여야 한다. (C99에서는 이 제한이 풀어졌다. 18.5 참고) in'zer의 멤버는 구조체 선언에 있는 것보다 적어도 된다; 배열과 마찬가지로, "남는" 멤버들의 값은 0으로 주어진다. 남는 문자 배열의 바이트들의 값은 0이 되고, 빈 문자열이 된다.
Designated Initializers(C99)
initializer의 형태를 다르게 지정할 수 있다.
{528, "Disk drive", 10}
{.number = 528, .name = "Disk drive", .on_hand = 10} // 동일
마침표와 멤버 이름이 결합된 것을 designator라고 한다(배열의 원소를 지정하는 designator와 형태가 다르다).
이 방법의 장점: 읽기 쉽다, 바꾸기 쉽다, 구조체 선언시와 순서가 바뀌어도 된다. 또 모든 값 앞에 designator가 오지 않아도 된다.
Operations on Structures
배열 연산중 가장 흔하게 쓰이는 것은 subscripting - 위치로 원소를 선택하는 것 - 이다. 구조체에서 가장 흔하게 하는 연산도 그 멤버를 선택하는 것이다. 구조체에선 포지션이 아닌 이름으로 멤버에 접근한다.
구조체이름.멤버이름
printf("Part number: %d\n", part1.number);
printf("Part name: %s\n", part1.name);
printf("Quantity on hand: %d\n", part1.on_hand);
구조체의 멤버는 lvalue이다. 따라서 assignment의 왼쪽에 올 수 있고 increment, decrement operator를 사용할 수 있다.
멤버의 이름 앞에 오는 구둣점(.)은 배열의 subscribtion시 쓰는 [] 처럼 연산자이다. postfix ++, -- 연산자와 우선순위가 같으며, 거의 모든 연산자들보다 앞선다.
scanf("%d", &part1.on_hand); // part1의 on_hand 멤버의 값을 입력받음
part2 = part1;
이 assignment operation은 part1.number를 복사해서 part2.number에 넣고, part1.name을 복사해서 part2.name에 넣는 식이다.
배열이 이렇게 간단하게 복사가 되지 않는데 비해 구조체는 이렇게 쉽게 복사할 수 있는데, 심지어 구조체 내에 있는 배열도 이렇게 복사할 수 있다. 이 특성을 이용해 "더미" 구조체를 만들어 배열을 간단히 복사할 수 있다.
struct { int a[10]; } a1, a2;
a1 = a2; // legal, seince a1 and a2 are structures
= 연산자는 compatible type인 구조체들 사이에서만 사용 가능하다. 동시에 선언된 두개의 구조체는 호환 가능하다. 같은 "structure tag"를 이용해 선언되었거나, 같은 타입 이름을 가진 구조체들도 호환 가능하다. ->수정할것
assignment를 제외하면 구조체 전체에 대해 쓸 수 있는 연산자는 없다. == 나 != 연산자로 구조체 전체가 같은지 다른지 비교할 수 없다.
16.2 Structure Types
naming structure types.
만약 변수를 프로그램의 다른 지점에서 선언해야 한다면? 동일한 구조체 정보를 가진 선언을 앞, 뒤로 쓸 것인가? 이것은 코드를 뻥튀기시킬 뿐 아니라, 그렇게 선언된 두 구조체 변수는 compatible하지 않고, 둘 간에 assign연산자도 사용할 수 없다. 또 두 변수의 type에 대한 이름이 없기 때문에 함수 호출시 인자로 사용할 수도 없다.
이러한 어려움을 피하기 위해, 어떤 구조체의 type을 나타낼 수 있는 이름을 정의해야 한다 - 특정 구조체 함수의 이름이 아닌.
구조체의 이름을 짓는 데에 두가지 방법이 있다. "structure tag"를 선언하거나, typedef으로 타입의 이름을 정의할 수 있다.
Declaring a Structure Tag
structure tag는, 특정한 종류의 구조체를 식별하기 위한 이름이다. 아래 예제는 part 라는 이름의 structure tag를 선언한다.
struct part {
int number;
char name[NAME_LEN+1];
int on_hand;
};
닫는 중괄호 뒤에 세미콜론이 있어야 선언이 끝난다.
part 태그를 만들고 나면, 그것을 변수 선언에 사용할 수 있다.
struct part part1, part2;
part는 type 이름이 아니기에, struct를 생략할 수 없다.
구조체 태그는 struct라는 단어가 앞에 오지 않으면 인식이 안 되기 때문에, 프로그램 내 다른 이름들과 충돌하지 않는다. 따라서 part라는 이름의 변수를 만드는 것은 좀 혼란을 가중시키기는 하겠지만 아무런 문제가 없다. 구조체 태그의 선언은 구조체 변수의 선언과 동시에 이루어질 수 있다. struct part로 선언된 모든 구조체는 서로 compatible하다.
Defining a Structure Type
structure tag를 선언하는 것의 대안으로, typedef를 써서 genuine type name을 정의할 수 있다. 다음 예는 Part 라는 이름을 가진 type을 정의한다.
typedef struct {
int number;
char name[NAME_LEN+1];
int on_hand;
} Part;
이 경우에는 type의 이름인 Part가 struct 뒤에 오는것이 아니라 가장 뒤에 온다.
기존 type 변수를 선언하는 것과 동일한 방식으로 변수를 선언한다.
Part part1, part2;
Part가 typedef의 이름이므로, struct Part 라는 표현은 쓸 수 없다. 모든 Part 변수는 어디에서 선언되었든간에 compatible하다.
구조체가 linked list 안에서 쓰이기 위해서는 structure tag 방식으로 선언되어야 한다. 이 책에선 대부분의 경우 typedef보다 structure tags 방법을 사용한다.
Structure as Arguments and Return Values
함수는 구조체를 인자나 리턴 값으로 가질 수 있다.
void print_part(struct part p)
{
printf("Part number: %d\n", p.number);
printf("Part name: %s\n", p.name);
printf("Quantity on hand: %d\n", p.on_hand);
}
part 구조체를 인자로 받아, 그 구조체의 멤버를 출력하는 함수
호출: print_part(part1);
struct part build_part(int number, const char *name, int on_hand)
{
struct part p;
p.number = number;
strcpy(p.name, name);
p.on_hand = on_hand;
return p;
}
인자들로부터 part 구조체를 구성해서 반환하는 함수
part1 = build_part(528, "Disk drive", 10);
함수로 구조체를 전달하는 것, 또 함수로부터 구조체를 반환하는 것 모두 구조체 안의 모든 멤버의 복사본을 만들어야 한다. 그 결과 이러한 작업은 상당한 양의 부하를 일으키며, 특히 구조체가 크다면 더 그렇다. 이러한 오버헤드를 피하기 위해, 구조체 자체를 pass하는것보다 pointer를 pass하는것이 추천되는 때가 있다. 비슷하게, 함수로부터 구조체 자체를 리턴받는것이 아니라 함수가 구조체를 가리키는 포인터를 리턴하도록 할 수 있다. -> 17.5에서 자세히 다룸
효율성 외에도 구조 복사를 피하는 다른 이유가 있습니다. 예를 들어, <stdio.h> 헤더는 일반적으로 구조 인 FILE 형식을 정의합니다. 각 FILE 구조는 열린 파일의 상태에 대한 정보를 저장하므로 프로그램 내에서 고유해야합니다. 파일을 여는 <stdio.h>의 모든 함수는 FILE 구조체에 대한 포인터를 반환하고 열린 파일에서 작업을 수행하는 모든 함수는 FILE 포인터를 인수로 필요로합니다.
경우에 따라 함수 내에서 구조체 변수를 초기화하여 함수에 대한 매개 변수로 제공되는 다른 구조체와 일치시킬 수 있습니다. 다음 예제에서 part2의 이니셜 라이저는 f 함수에 전달 된 매개 변수입니다. (구글버녁)
void f(struct part part1)
{
struct part part2 = part1;
...
}
이런 초기화도 허용된다. 단 초기화하는 구조체(이 경우에는 part2)가 automatic storage duration(local to function, not has been declared static)이어야 한다.
Compound Literals(C99)
9.3에서 C99의 compound literal 기능을 소개했다. 이름이 없는 배열을 만들기 위해, 주로 함수에 배열을 pass할 목적으로 사용되었다. compound literal은 "on the fly"한 구조체를 만들기 위해서도 사용될 수 있다. 매개변수로 pass되거나, 함수로부터 리턴되거나, 변수에 할당될 수 있다.
1. 함수에 전달될 구조체를 만들기
print_part((struct part) {528, "Disk drive", 10});
compound literal(굵은 부분)은 part 구조체를 만든다. 그리고 함수의 인자로 전달한다.
2. 변수로 할당
part1 = (struct part) {528, "Disk drive", 10};
in'zer가 담겨진 선언과 비슷해 보이지만, 같지 않다. in'zer는 선언할 때에만 사용할 수 있고, 이런 문장에서는 사용할 수 없다.
일반적으로 compound literal은 괄호 안의 타입 이름, 중괄호로 감싼 값들의 집합이 온다. type name은 structure tag가 될수도 있고 typedef의 이름이 될 수도 있다. designated initializer처럼 designator가 올 수도 있다.
print_part((sturct part) {.on_hand = 10, .name = "Disk drive", .number = 528});
16.3 Nested Arrays and Structures
구조체와 배열은 제한 없이 혼합될 수 있다. 배열은 그 원소로 구조체를 가질수 있고, 구조체는 배열이나 구조체를 멤버로 가질수 있다. 이미 구조체 내에 배열이 nested 된 것을 보았다(part 구조체의 name 멤버). 이번엔 멤버가 구조체인 구조체와, 멤버가 구조체인 배열을 살펴보자.
Nested Structures
구조체 안에 구조체를 넣기.
Arrays of Structures
원소가 구조체인 배열. 배열과 구조체의 가장 흔한 콤비네이션중 하나이다. 이런 배열은 간단한 데이터베이스가 된다. 예를 들어, 이 part의 배열은 100가지 부품에 대한 정보를 저장할 수 있다.
struct part inventory[100];
배열 안에 있는 부품 중 하나에 접근하기 위해서, subscripting을 이용한다. i번째에 위치한 부품을 출력하기 위해선
print_part(inventory[i])
inventory[i].number = 883; // i번째 부품의 번호를 수정
inventory[i].name = '\0'; // i번째 부품의 이름을 empty string으로 수정
Initializing an Array of Structures
구조체의 배열을 초기화하는 것은 다차원 배열을 초기화하는 것과 비슷하다. 각각의 구조체는 중괄호로 묶여진 in'zer가 있다; 배열의 in'zer는 구조체의 in'zer들을 중괄호로 묶는다.
배열의 구조체를 초기화하는 이유 중 하나는 프로그램 실행 도중 바뀌지 않는 데이터베이스를 사용하기 위해서이다. 국제전화 국가 코드에 접근하는 프로그램을 만든다고 생각해 보자. 먼저 국가의 이름과 코드가 담긴 구조체를 선언한다.
struct dialing_code {
char *country;
int code;
};
country가 문자의 배열이 아닌 포인터임에 주목하자. 이는 만약 dialing_code 구조체를 변수로 사용하면 문제가 된다. 하지만 dialing_code 구조체를 초기화하면 country는 string literal을 가리키는 포인터가 되기 때문에 문제가 없다.
그 다음엔 이 구조체의 배열을 선언하면서 각각의 구조체에 나라들의 이름과 코드를 담도록 초기화한다.
const struct dialing_code country_codes[] =
{{"Argentina", 54}, {"Bangladesh", 880},
{"Brazil", 55}, {"Egypt", 20}};
안의 중괄호는 생략이 가능하다. 스타일상 저자는 생략 안함.
C99에서는 한 아이템에 대해 두개 이상의 designator를 사용할 수 있다.
struct part inventory[100] = { [0].number = 528,[0].on_hand = 10, [0].name[0] = 'b' };
굵은 부분: inventory의 첫번째 아이템의 이름의 첫번째 글자가 'b'
16.4 Unions
union(공용체)은 구조체와 비슷하게 하나 이상의 멤버(타입이 같지 않아도 되는)로 구성된다. 하지만 컴파일러는 가장 큰 멤버를 저장할 수 있을 만큼만 메모리를 할당한다. 그 공간 안에서 서로를 덮어쓰게 된다.
union {
int i;
double d;
} u;
struct {
int i;
double d;
} s;
두 개의 멤버를 가진 union u. union의 선언은 structure와 닮았다.
int가 4바이트, double이 8바이트를 차지한다고 가정하면 s와 u는 메모리에 위와 같이 저장된다.
s 구조체에선, i와 d는 다른 메모리 주소를 차지한다; s의 전체 크기는 12바이트이다.
u 공용체에선, i와 d가 겹친다(i는 d의 첫번째 4바이트를 차지한다); u의 전체 크기는 8바이트밖에 안 된다. 또한 i와 d의 주소는 같다.
공용체의 멤버에 접근하는 방법은 구조체의 그것과 동일하다.
컴파일러가 공용체의 멤버들의 저장공간을 서로 덮기 때문에, 멤버 중 하나의 값을 바꾸면 나머지 멤버들의 값도 바뀌게 된다. 따라서 만약 u.d에 값을 저장하면, u.i의 값을 잃는다(만약 u.i의 값을 조사하면 의미없어보이는 값이 나온다). 이 성질 때문에, u는 i와 d중 하나의 값만 저장할수 있는 장소라고 생각할 수 있다.
공용체의 성질은 구조체의 성질과 거의 동일하다. union tag, union type을 구조체와 같은 방법으로 사용 가능하고, = 연산자로 복사할 수 있고, 함수로 전달되거나 함수가 리턴할 수 있다. designated initializer를 사용 가능하다.
Using Unions to Save Space
구조체의 공간을 절약하기 위한 방법으로 공용체를 사용할 수 있다. 카탈로그로 판매된 아이템에 대한 정보를 담은 구조체를 디자인한다고 생각해 보자. 카탈로그에는 세 가지 상품만 들어 있다: 책, 머그, 셔츠. 각각의 아이템은 재고 번호와 가격, 또 각각의 아이템에 따라 다른 추가 정보가 있다
Using Unions to Build Mixed Data Structures
공용체는 서로 다른 타입이 섞여 있는 데이터 구조를 만드는데 유용하다. 예를 들어 int와 double 값이 섞인 배열을 만들고 싶을 때 공용체를 이용할 수 있다. 먼저, 공용체 타입을 정의하는데 그 멤버들은 배열에 들어갈 수 있는 타입의 종류들로 한다. 그리고 Number를 원소로 갖는 배열을 만든다.
typedef union {
int i;
double d;
} Number;
Number number_array[100];
number_array의 각각의 원소들은 Number union이다. Number union에는 int, double중 하나의 값만 저장할 수 있으므로, int와 double을 혼합해서 number_array에 넣을 수 있다.
Adding a "Tag Field" to a Union
공용체의 큰 문제점은 공용체 중 어떤 멤버가 마지막으로 바뀌었고, 유의마한 값을 지니고 있는지 구별할 방법이 없다는 것이다. 이 문제를 해결하기 위해 구조체 안에 다른 멤버 - "tag field" 또는 "discriminant" - 와 함께 공용체를 집어넣는 방법이 있다. 그 멤버의 목적은 공용체 안에 어떤 값이 저장되어 있는지 알려주는 것이다.
#define INT_KIND 0
#define DOUBLE_KIND 1
typedef struct {
int kind; /* tag field */
union {
int i;
double d;
} u;
} Number;
Number의 멤버는 kind와 u가 있다. kind의 값은 INT_KIND, 또는 DOUBLE_KIND 둘 중 하나이다. 만약 n이 Number 변수이고 멤버 i에 변수를 할당한다면
n.kind = INT_KIND;
n.u.i = 82;
Number variable로부터 값을 꺼낼 때는, kind값을 통해 어떤 멤버의 값이 마지막으로 저장된지 알수 있다.
void print_number(Number n)
{
if (n.kind == INT_KIND)
printf("%d", n.u.i);
else
printf("%g", n.u.d);
}
tag의 값을 바꾸는 것은 물론 프로그래머의 몫이다.
16.5 Enumerations
많은 프로그램에서는, 의미있는 값의 범위가 작은 변수가 필요하다. 대표적으로 boolean 변수가 있다: "true" 또는 "false"만 필요.
트럼프 카드의 무늬를 저장하는 변수는 4개의 값만 저장된다: "clubs", "diamonds", "hearts", "spades". 이러한 변수를 다루는 방법은 그 변수를 정수로 선언하고, 그것을 변수의 사용 가능한 값을 나타내게 하는 것이다.
int s; /* s will store a suit */
...
s = 2; /* 2 represents "hearts" */
이 방법도 통하지만, 코드를 읽는 사람이 s가 네가지의 값만 갖는 변수라는 것을 알 수 없고, 2의 의미도 분명하지 않다.
좀더 발전된 방법은 매크로를 사용해서 suit "type"과 각각의 무늬에 대한 이름을 정의하는 것이다.
#define SUIT int
#define CLUBS 0
#define DIAMONDS 1
#define HEARTS 2
#define SPADES 3
이제 바로 위 표현을 아래같이 나타내서 좀더 읽기 쉽다
SUIT s;
s = HEARTS;
이 방법은 발전되었지만 최선의 답은 아니다. 프로그램을 읽는 사람은 각각의 매크로가 같은 "type"의 값을 나타낸다는 것을 알 수 없다. 또 가능한 값이 꽤 많다면, 일일이 매크로로 정의하는 것도 지루하다. 게다가 위에서 정의한 이름들은 전처리기에서 다 바꿔버리므로, 디버깅할 때 사용할 수 없다.
C에서 가능한 값이 적을 때 사용하기 위해서 만든 특별한 타입이 enumerated type(열거형)이다. 이 타입의 값들은 프로그래머에 의해서 열거되며(enumerated = listed) 각각의 값들은 이름(enumeration constant)이 있어야 한다. 아래는 s1과 s2에 할당될 수 있는 값들(클럽, 다이아, 하트, 스페이드)을 열거한다.
enum {CLUBS, DIAMONDS, HEARTS, SPADES} s1, s2;
열거형은 구조체나 공용체와 공통점이 거의 없지만, 선언 방법은 비슷하다. 그러나 enumeration constat들의 이름은 선언하고 있는 scope 내에 있는 것들과 겹쳐서는 안된다.
enumeration constant는 #define 지시문으로 만들어진 상수들과 비슷하지만, 같지는 않다. 예를 들어, E.C는 C의 범위 규칙을 따른다. 만약 enum이 함수 내에서 선언되었다면, 그 상수들은 함수 밖에서는 visible하지 않다.
Enumeration Tags and Type Names
구조체나 공용체와 비슷한 이유로 enumeration의 이름을 종종 만들게 된다. 앞서 배운 둘과 마찬가지로, tag를 선언하거나 typedef를 통해 새로운 type 이름을 정의할 수 있다.
enum suit {CLUBS, DIAMONDS, HEARTS, SPADES};
enum suit s1, s2;
// tag 이용
typedef enum {CLUBS, DIAMONDS, HEARTS, SPADES} Suit;
Suit s1, s2;
// typedef 이용
C89에선 typedef enum이 Boolean type을 정의하는 훌륭한 방법이었다.
typedef enum {FALSE, TRUE} Bool;
Enumerations as Integers
C는 enumeration의 값과 상수를 정수로 취급한다. 기본값으로 컴파일러는 특정 열거형 내 상수에 0, 1, 2..를 할당한다. 할당되는 정수의 값을 지정할 수 있다.
enum suit {CLUBS = 1, DIAMONDS = 2, HEARTS = 3, SPADES = 4};
순서 없이 임의의 정수를 입력해도 된다.
enum dept {RESEARCH = 20, PRODUCTION = 10, SALES = 25};
여러개의 상수가 같은 값을 가지는 것도 허용된다. 또 상수에 특정 값이 할당되지 않으면, 바로 직전의 상수보다 1 큰 값이 된다.
Using Enumerations to Declare "Tag Fields"
typedef struct {
enum {INT_KIND, DOUBLE_KIND} kind;
union {
int i;
double d;
} u;
} Number;
16.4에서 접한 문제 - 공용체에 마지막으로 할당된 값을 구별하는 것 - 를 완벽하게 해결할 수 있다. kind 멤버를 정수 대신 enum으로 만들자. 이 새로운 구조체는 예전 것과 정확히 같다. 이 방법의 장점: INT_KIND, DOUBLE_KIND 는 매크로가 아닌 enumeration constant이다. 또 kind의 의미, 둘 중 하나만 사용 가능하단 것이 명확해졌다.