제7장제 1 부  C++의 기본: C 언어
7  구조체, 공용체, 나열형 및 사용자 정의형

 

 

구조체, 공용체, 나열형, 및
사용자 정의형
C 언어에서는 사용자가 자료형을 정의할 수 있도록 하는 다섯 가지 방법을 허용하고 있다. 먼저, 첫 번째가 하나의 이름으로 변수들을 그룹화하는 혼합된(compound) 자료형이라고 하는 구조체(structure)이다(여기서는 집합체(aggregate) 또는 집단(conglomerate)이라는 용어도 보통 사용된다). 두 번째 사용자 정의형은 구조체의 변형으로 각 개개의 비트들에 접근할 수 있는 비트-필드(bit field)이다. 세 번째 방법은 2개 이상의 서로 다른 형의 변수들을 같은 메모리에 정의할 수 있도록 하는 공용체(union)이다. 네 번째 사용자 정의 자료형은 이름을 가진 정수 상수들의 리스트인 나열형(enumeration)이다. 마지막으로, typedef를 사용하여 현재 형에 새로운 이름을 정의하는 방법이 있다.

 

7.1  구조체

구조체는 하나의 이름으로 참조되는 변수들의 집합으로 관련된 정보를 함께 유지하는데 편리한 수단을 제공한다. 구조체 선언(structure declaration)은 구조체를 생성하기 위해서 사용될 수 있는 골격(template)을 구성한다. 구조체를 구성하는 변수들을 구조체의 멤버(member)라고 한다. (구조체 멤버는 또한 요소(element) 또는 필드(filed)라고도 한다.)
일반적으로, 구조체의 모든 멤버들은 논리적으로 관계있는 것들이다. 예를 들어, 우편목록에서 이름과 주소 정보는 보통 특정 구조체로 표현될 수 있다. 다음 코드는 이름과 주소 필드를 정의하는 구조체 선언을 보인 것이다. 키워드 struct는 컴파일러에게 구조체가 선언되었음을 알리는 것이다.


struct addr
{
  char name[30];
  char street[40];
  char city[20];
  char state[3];
  unsigned long int zip;
};


이 선언에서 세미콜론(;)으로 끝나고 있음을 주의하자. 이것은 구조체 선언도 문장이기 때문이다. 또한, 구조체 태그(tag) addr은 이 구조체만의 특별한 자료구조의 식별자이면서, 이것의 형 지정자이다.
구조체 선언 시점에, 이것이 실제로 생성되는 것은 아니다. 단지 특정 자료의 형식만이 정의된 것이다. addr형의 변수를 선언하기 위해서는 다음과 같이 한다.


struct addr addr_info;


이것은 addr_info라는 struct addr 형의 변수를 선언한다. 구조체를 정의할 때는 특정 변수가 아니라, 혼합된 변수형을 정의하는 것이다. 그 형에 대한 특정 변수를 선언해야만 비로소 하나의 구조체가 실제로 존재하게 되는 것이다.

주의
C++에서는, 일단 특정 구조체가 선언되고 나면, 키워드 struct를 앞에 붙이지 않고 이것의 태그 이름만으로 그 형의 변수를 선언할 수 있다. 예를 들어, C++에서, addr 형의 구조체 변수를 선언하기 위해서 다음과 같이 쓰면 된다.

  addr addr_info;

C와 C++에서 이러한 차이가 나는 이유는 C에서는, 구조체 태그(tag) 이름은 완전한 형이름이 아닌 반면에 C++에서 이것은 완전한 형이름이다. 그러나, C++ 프로그램에서 (키워드 struct를 사용해야 하는) C-스타일의 선언도 확실히 사용할 수 있다는 것을 명심하자.


(addr_info처럼) 구조체 변수가 선언될 때, C/C++ 컴파일러는 이것의 모든 멤버들을 처리하기 위해서 충분한 메모리를 자동적으로 할당한다. 그림 7-1은 1바이트 크기의 문자와 long형 정수를 4바이트로 가정할 때, addr_info가 메모리에서 어떻게 구성되는지를 보인 것이다.


그림 7-1  메모리 내의 addr_info 구조체


또한 구조체를 선언할 때 하나 이상의 구조체 변수들을 함께 선언할 수도 있다. 예를 들어, 다음을 생각해 보자.


struct addr {
  char name[30];
  char street[40];
  char city[20];
  char state[3];
  unsigned long int zip;
} addr_info, binfo, cinfo;


이것은 addr이라는 구조체형을 정의하며, 그 형에 대한 변수들 addr_info, binfo, 및 cinfo 등을 함께 선언한다.
만일 하나의 구조체 변수만이 필요하면, 구조체 태그는 필요하지 않다. 즉, 다음은 해당 구조체로 정의된 하나의 변수인 addr_info를 선언한 것이다.


struct {
  char name[30];
  char street[40];
  char city[20];
  char state[3];
  unsigned long int zip;
} addr_info;

다음은 구조체 선언의 일반 형식이다.

  struct tag {
    type member_name;
    type member_name;
    type member_name;
     .
     .
     .
  } structure_variables;

여기서, tag 또는 structure_variables 중 하나는 생략될 수 있지만, 이들 모두를 생략할 수는 없다.

7.1.1  구조체 멤버 접근
구조체의 각 멤버들은 도트(.) 연산자를 이용하여 접근할 수 있다. 예를 들어, 다음 코드는 위에서 이미 선언된 구조체 변수 addr_info의 zip 필드에 ZIP 코드 12345를 할당한 것이다.


addr_info.zip = 12345;


구조체 변수 이름 다음에 점과 멤버 이름은 그 구조체 내의 각각의 멤버이다. 구조체의 멤버에 접근하기 위한 일반 형식은 다음과 같다.

  structure_name.member_name

그러므로, 화면에 ZIP 코드를 출력하려면, 다음과 같이 작성한다.


printf("%d", addr_info.zip);


이것은 구조체 변수 addr_info의 zip 멤버에 포함된 ZIP 코드를 출력한다.
같은 방법으로, 문자배열 addr_info.name은 다음과 같이 gets()에서 사용할 수 있다.


gets(addr_info.name);

이것은 배열 name의 첫 번째 요소에 대한 문자 포인터를 전달한다.
name은 문자 배열이기 때문에, name의 첨자를 이용하여 addr_info.name 각각의 문자들에 접근할 수 있다. 예를 들어, 다음 코드를 이용하면 한 번에 하나의 문자씩 addr_info.name의 내용을 출력할 수 있다.


register int t;

for(t=0; addr_info.name[t]; ++t)
  putchar(addr_info.name[t]);


7.1.2  구조체 치환
하나의 구조체에 포함된 정보는 치환문을 이용하여 같은 형의 또 다른 구조체에 치환될 수 있다. 즉, 이를 위해서, 각 멤버들의 값들을 분리해서 치환할 필요가 없다는 것이다. 다음 프로그램은 구조체 치환을 설명하고 있다.


#include <stdio.h>

void main(void)
{
  struct {
    int a;
    int b;
  } x, y;

  x.a = 10;

  y = x;  /* 하나의 구조체를 다른 것에 치환 */

  printf("%d", y.a);
}


치환 이후에, y.a는 값 10을 포함한다.

7.2  구조체의 배열

아마도 구조체에 대해서 가장 많이 사용하는 경우가 구조체의 배열일 것이다. 구조체의 배열을 선언하기 위해서, 먼저 해당 구조체를 정의하고 다음으로 그 형에 대한 배열변수를 선언해야 한다. 예를 들어, addr 형의 구조체를 100개 갖는 배열을 선언하기 위해서, 다음과 같이 작성한다.


struct addr addr_info[100];


이것은 구조체 addr을 요소로 하는 100의 변수들을 생성한다.
특정한 구조체에 접근하기 위해서, 배열 이름의 첨자 인덱스를 이용한다. 예를 들어, 세 번째 구조체의 ZIP 코드를 출력하기 위해서, 다음과 같이 작성한다.


printf("%d", addr_info[2].zip);


모든 배열 변수들과 마찬가지로, 구조체의 배열도 0부터 시작된다.

 

7.3  구조체를 함수로 전달하기

이 절은 구조체와 이것의 멤버들을 함수로 전달하는 방법에 대해서 다룬다.

7.3.1  구조체 멤버들을 함수로 전달하기
구조체의 멤버들을 함수로 전달할 때, 그 멤버의 값이 실제로 함수로 전달된다. 그러므로, (물론, 문자들의 배열처럼, 해당 멤버가 혼합된 것이 아니라면) 간단한 변수를 전달한다. 예를 들어, 다음 구조체를 생각해 보자.


struct fred
{


  char x;
  int y;
  float z;
  char s[10];
} mike;


다음은 함수에 전달된 각 멤버의 예이다.


func(mike.x);  /* x의 문자값 전달 */
func2(mike.y); /* y의 정수값 전달 */
func3(mike.z); /* z의 실수값 전달 */
func4(mike.s); /* 문자열 s의 주소 전달 */
func(mike.s[2]); /* s[2]의 문자값 전달 */


만일 각 개개의 구조체 멤버의 주소를 전달하고자 한다면, 구조체 이름 앞에 &를 붙인다. 예를 들어, 구조체 mike의 멤버들의 주소를 전달하기 위해서, 다음과 같이 작성하면 된다.


func(&mike.x);    /* 문자 x의 주소 전달 */
func2(&mike.y);   /* 정수 y의 주소 전달 */
func3(&mike.z);   /* 실수 z의 주소 전달 */
func4(mike.s);    /* 문자열 s의 주소 전달 */
func(&mike.s[2]); /* 문자 s[2]의 주소 전달 */


연산자 &는 각각의 멤버 이름이 아니라, 구조체 이름 앞에 붙인다는 것을 명심하자. 또한 s는 이미 주소를 나타내므로, &를 붙일 필요가 없다.

7.3.2  전체 구조체를 함수로 전달하기
구조체가 함수에 대한 인자로 사용될 때, 전체 구조체는 표준의 값에 의한 전달(call-by-value) 방법으로 전달된다. 즉 이것은 전달받은 구조체의 내용이 특정 함수 내에서 변경되더라도, 인자로 사용된 구조체에 어떠한 영향도 주지 않음을 의미하는 것이다.
구조체가 매개변수로 사용될 때, 인자의 형은 매개변수의 형과 일치해야 한다는 것을 명심하자. 예를 들어, 다음 프로그램에서, 인자 arg와 매개변수 parm 모두는 같은 형의 구조체로 선언된다.


#include <stdio.h>

/* 구조체형 정의하기 */
struct struct_type {
  int a, b;
  char ch;
};

void f1(struct struct_type parm);

void main(void)
{
  struct struct_type arg;

  arg.a = 1000;

  f1(arg);
}

void f1(struct struct_type parm)
{
  printf("%d", parm.a);
}


이 프로그램에서 알 수 있듯이, 구조체를 매개변수로 선언하려면, 프로그램의 모든 부분에서 이것을 사용할 수 있도록 구조체형을 전역적으로 선언해야 한다. 예를 들어, struct_type가 main() 내에 선언되었다면, f1()에서는 사용할 수 없는 것이다.
이미 언급했듯이, 구조체를 전달할 때, 인자의 형(type)은 매개변수의 형과 일치해야 한다. 이것은 이들이 물리적으로 비슷하다는 것만으로는 충분치 않다는 의미이다. 즉, 이들의 형이 일치해야 한다는 것이다. 예를 들어, 위의 프로그램을 다음과 같이 수정하면 f1()을 호출할 때 사용한 인자의 형이름과 이것의 매개변수의 형이름이 일치하지 않기 때문에, 부정확한 것이며 컴파일되지 않는다.


/* 이 프로그램은 부정확하며 컴파일되지 않는다. */
#include <stdio.h>

/* 구조체형 정의 */
struct struct_type {
  int a, b;
  char ch;
};

/* struct_type과 비슷한 구조체이지만, 서로 다른 이름으로 정의 */


struct struct_type2 {
  int a, b;
  char ch;
};

void f1(struct struct_type2 parm);

void main(void)
{
  struct struct_type arg;

  arg.a = 1000;

  f1(arg);  /* 형 불일치 */
}

void f1(struct struct_type2 parm)
{
  printf("%d", parm.a);
}

 


7.4  구조체 포인터

C에서는 어떤 형의 변수에 대한 포인터를 허용하는 것처럼, 구조체에 대한 포인터도 허용한다. 그러나, 꼭 알아 두어야 할 구조체 포인터에 대한 특별한 사항들이 있다.

7.4.1  구조체 포인터 선언
다른 포인터와 마찬가지로, 구조체 포인터는 구조체 변수의 이름 앞에 *를 붙여서 선언한다. 예를 들어, 앞에서 정의한 addr을 생각할 때, 다음은 addr_pointer를 그 형의 자료에 대한 포인터로 선언한 것이다.


struct addr *addr_pointer;


C++에서는 이러한 선언시 키워드 struct를 붙여 선언할 필요가 없음을 명심하자.
7.4.2  구조체 포인터 사용
구조체 포인터는 기본적으로 두 가지 부분에서 사용된다. 즉, 참조에 의한 전달(call by reference)로 함수에 매개변수를 전달할 때, 그리고 C의 동적할당 시스템을 이용하여 링크드 리스트와 어떤 다른 동적 자료구조를 만들 때 사용된다.
간단한 구조체라도 이것을 함수에 전달하는데는 하나의 중요한 결점이 있다. 즉, 함수호출이 실행될 때 그 구조체를 저장하기 위해서 스택으로 삽입해야 하는 부담이 존재한다. (함수 호출시 인수들은 스택을 이용하여 함수로 전달된다는 것을 상기하자.) 거의 맴버가 없는 간단한 구조체의 경우에, 이러한 부담은 그리 크지 않다. 그러나, 만일 구조체가 많은 멤버들을 포함하고 있거나 또는 이것의 몇몇 멤버들이 배열이라면, 실행 성능은 받아들일 수 없을 정도로 떨어질 수 있다. 이러한 문제에 대한 해결방법은 함수에 포인터만을 전달하는 것이다.
구조체에 대한 포인터가 함수로 전달될 때, 구조체에 대한 주소만이 스택에 삽입된다. 이것은 함수 호출을 매우 빠르게 할 수 있도록 한다. 또한 어떤 경우에, 함수가 인자로 사용된 실제 구조체를 복사하는 것이 아니라 단지 참조할때 또 다른 장점을 제공할 수 있다. 포인터를 전달함으로써, 함수는 호출시 사용된 구조체의 내용을 수정할 수 있다.
구조체 변수의 주소를 위해서, 구조체의 이름 앞에 연산자 &를 붙인다. 예를 들어, 다음 코드를 생각해 보자.


struct bal {
  float balance;
  char name[80];
} person;

struct bal *p;  /* 구조체 포인터 선언 */


이때, 다음 문장은 구조체 person의 주소를 포인터 p에 넣는다.


p = &person;


여기서, 그 구조체에 대한 포인터를 사용한 구조체 멤버들의 접근은 -> 연산자를 사용해야 한다. 예를 들어, 다음은 balance 필드를 참조한다.


p->balance

-> 연산자는 보통 화살표 연산자라고 하며 이것은 마이너스 기호(-) 다음에  부등기호(>)로 구성된다. 화살표 연산자는 구조체에 대한 포인터로 구조체 멤버에 접근할 때 앞에서 논의한 도트 연산자 위치에서 사용된다.
구조체 포인터가 어떻게 사용되는지를 보기 위해서, 소프트웨어 타이머를 사용하여 화면에 시, 분, 및 초를 출력하는 다음의 간단한 프로그램을 살펴보자.


/* 소프트웨어 타이머 출력하기 */
#include <stdio.h>

#define DELAY 128000

struct my_time {
  int hours;
  int minutes;
  int seconds;
};

void display(struct my_time *t);
void update(struct my_time *t);
void delay(void);

void main(void)
{
  struct my_time systime;

  systime.hours = 0;
  systime.minutes = 0;
  systime.seconds = 0;

  for(;;) {
    update(&systime);
    display(&systime);
  }
}

void update(struct my_time *t)
{
  t->seconds++;
  if(t->seconds==60) {
    t->seconds = 0;
    t->minutes++;
  }

  if(t->minutes==60) {
    t->minutes = 0;


    t->hours++;
  }

  if(t->hours==24) t->hours = 0;
  delay();
}

void display(struct my_time *t)
{
  printf("%02d:", t->hours);
  printf("%02d:", t->minutes);
  printf("%02d\n", t->seconds);
}

void delay(void)
{
  long int t;

  /* 필요한 만큼 수정 */
  for(t=1; t<DELAY; ++t) ;
}


이 프로그램의 시간은 DELAY 정의를 수정하여 조정 가능하다.
여기서 볼 수 있듯이, 전역의 구조체 my_time이 정의되었다. main() 내에서, 구조체 변수 systime이 선언되며 00:00:00으로 초기화된다. 이것은 systime이 main() 함수에서만 직접 접근될 수 있음을 의미한다.
시간을 수정하는 함수 update()와 시간을 출력하는 display()에는 systime의 주소가 전달된다. 이 두 함수에서, 이들의 인자는 my_time 구조체에 대한 포인터로 선언된다.
update()와 display() 내에서, systime의 각 멤버는 포인터로 접근된다. update()가 systime 구조체에 대한 포인터를 받기 때문에, 이것의 값을 수정할 수 있다. 예를 들어, 시간이 24:00:00에 도달할 때 시간들을 다시 0으로 설정하기 위해서, update()는 다음 코드를 포함한다.


if(t->hours = =24) t->hours=0;


이것은 hours를 0으로 설정하기 위해서, main()의 systime을 지적하고 있는 t에 포함된 주소를 사용하도록 컴파일러에게 알려준다.
기억할것
구조체 자체에 대한 연산에서, 구조체 요소들에 접근하기 위해서는 도트 연산자를 사용한다. 그러나, 구조체에 대한 포인터를 가지고 있을 때는 화살표 연산자를 사용한다.

 


7.5  구조체 내에 포함된 배열과 구조체

구조체의 멤버들은 간단한 내장형 또는 혼합형이 될 수 있다. 간단한 멤버는 정수형 또는 문자형 같은 어떤 내장된 자료형 중에 하나이다. addr에서 사용된 문자배열 처럼, 혼합된 요소의 형은 이미 보았다. 또 다른 혼합된 자료형으로는 다른 자료형 및 구조체의 1차원 및 다차원 배열이 있다.
구조체의 멤버가 배열일 때도, 이것은 앞의 예에서 보았던 것처럼 다루어진다. 예를 들어, 다음 구조체를 생각해 보자.


struct x {
  int a[10][10];  /* 10×10의 정수 배열 */
  float b;
} y;


구조체 y의 a에 3행 7열 정수를 참조하기 위해서, 다음과 같이 사용한다.


y.a[3][7]


특정 구조체가 또 다른 구조체의 멤버가 될때, 이 구조체를 중첩된 구조체(nested structure)라고 한다. 예를 들어, 다음 예에서, 구조체 address는 emp 내에 중첩되어 있다.


struct emp {
  struct addr address;  /* 중첩된 구조체 */
  float wage;
} worker;


여기서, 구조체 emp는 2개의 멤버를 가진 것으로 정의되었다. 첫 번째 것은 고용인의 주소를 포함하는 addr형의 구조체이며, 두 번째 것은 고용인의 임금을 포함하는 wage이다. 다음 코드 부분은 address의 zip 요소에 93456을 저장한다.


worker.address.zip = 93456;


여기서 볼 수 있듯이, 각 구조체의 멤버들은 가장 바깥쪽에서 안쪽으로 참조된다. ANSI C 표준은 구조체들을 15개까지 중첩할 수 있다고 명시하고 있다. 제안된 ANSI C++ 표준은 256개까지 중첩을 허용하도록 제안하고 있다.

 

7.6  비트 필드

대부분의 다른 컴퓨터 언어와는 달리, C는 하나의 비트에 접근할 수 있도록 하는 비트 필드(bit-filed)라는 내장된 기능을 가지고 있다. 비트 필드는 다음과 같은 이유 때문에 유용하다.

? 만일 기억장소가 부족하다면, 한 바이트로 여러 개의 2진 변수(boolean: 참/거짓)들을 저장할 수 있다.
? 어떤 장치는 정보를 비트로 부호화(encode)하여 전송할 수 있다.
? 암호화(encryption)루틴은 특정 바이트 내의 비트들에 접근할 필요가 있다.

이러한 작업들은 비트 연산자를 이용하여 수행할 수 있지만, 비트 필드는 프로그램상에 더 많은 구조(및 효율성)을 추가할 수 있다.
비트들에 접근하기 위해서, C는 구조체를 기본으로한 방법을 사용한다. 사실, 비트 필드는 실제로 어떤 길이의 필드를 비트로 정의하는 구조체 멤버의 특수한 형일 뿐이다. 다음은 비트 필드 정의의 일반 형식이다.

  struct tag {
    type name 1: length;
    type name 2: length;
    type name 3: length;
     .
     .
     .
    type name N: length;
  } variable_list;

여기서, type는 int, unsigned, 또는 signed 중 하나인 비트 필드의 형을 지정한다. 길이 1의 비트 필드들은 하나의 비트는 부호가 올 수 없으므로, unsigned로 선언되어야 한다. (어떤 컴파일러들은 단지 unsigned 비트 필드만을 제공한다.) 비트 필드의 비트 수는 length로 지정한다.
비트 필드는 하드웨어 장치로부터의 입력을 분석할 때 자주 사용된다. 예를 들어, 직렬 통신 어댑터의 상태 포트는 다음과 같이 구성된 상태 바이트를 리턴할 수 있다.

    비트  의미
     0  clear-to-send라인에 변경
     1  data-set-ready에 변경
     2  발견된 trailing edge
     3  수신 라인에 변화
     4  clear-to-send
     5  data-set-ready
     6  전화 벨울리기
     7  수신된 신호

다음의 비트 필드를 사용하여 이러한 상태 바이트 정보를 표현할 수 있다.


struct status_type {
  unsigned delta_cts: 1;
  unsigned delta_dsr: 1;
  unsigned tr_edge;   1;
  unsigned delta_rec: 1;
  unsigned cts:       1;
  unsigned dsr;       1;
  unsigned ring:      1;
  unsigned rec_line:  1;
} status;


이때, 프로그램에서, 자료를 송신 또는 수신할 수 있는 시기를 결정하기 위해서 다음과 비슷한 루틴을 사용할 수 있다.


status = get_port_status();
if(status.cts) printf("clear to send");
if(status.dsr) printf("data ready");


비트 필드에 값을 할당하기 위해서, 구조체의 어떤 다른 형 요소들을 사용하는 것과 같은 형식을 사용한다. 예를 들어, 다음 코드는 ring 필드를 초기화한다.


status.ring = 0;


이 예에서 알 수 있듯이, 각 비트 필드는 도트 연산자로 접근된다. 그러나, 만일 구조체가 포인터로 참조된다면, 화살표 연산자(->)를 사용해야 한다.
각 비트 필드에 반드시 이름이 있어야 하는 것은 아니다. 즉, 사용하지 않는 것들은 그냥 지나치고 원하는 특정 비트를 쉽게 참조한다. 예를 들어, 만일 cts와 dsr 비트만이 관심 있다면, status_type 구조체를 다음과 같이 선언할 수 있다.


struct status_type {
  unsinged:     4;
  unsigned cts: 1;
  unsigned dsr: 1;
} status;


또한, dsr 비트 다음의 비트들은 사용하지 않는 것이라면 어떤 지정도 필요 없음을 주의하자.
비트 필드와 보통의 구조체멤버들을 함께 쓸 수도 있다. 예를 들어, 다음은 고용인의 상태, 고용인이 봉급을 받았는지, 그리고 공제 항목수 등, 세가지 종류의 정보를 저장하는데 1바이트만을 이용하는 고용인 레코드를 정의한다. 비트 필드가 없다면, 이 정보는 3바이트가 필요할 것이다.


struct emp {
  struct addr address;
  float pay;
  unsigned lay_off:    1;  /* 고용인 상태 */
  unsigned hourly:     1;  /* 봉급 여부 */
  unsigned deductions: 3;  /* 공제 */
};


비트 필드 변수에도 제약이 있다. 즉, 비트 필드변수에는 주소를 저장할 수 없으며, 또한 배열도 될 수 없다. 비트 필드가 오른쪽에서 왼쪽으로 또는 왼쪽에서 오른쪽으로 실행되는지는 기계에 따라 다르므로 알 수 없다. 즉, 비트 필드를 사용하는 코드는 어느 정도는 기계에 의존적인 것이다.

 

7.7  공용체

공용체(union)는 서로 다른 시점에 2개 이상의 서로 다른 (일반적으로 서로 다른 형의) 변수들에 의해서 공유된 메모리 장소이다. union 선언은 구조체 선언과 비슷하다. 다음은 이것의 일반 형식이다.

  union tag {
    type variable_name;
    type variable_name;
    type variable_name;
    .
    .
    .
  } union_variables;

다음은 이것의 예이다.


union u_type {
  int i;
  char ch;
};


이 선언은 어떤 공용체 변수도 생성하지 않는다. 공용체 변수는 선언의 끝에 이름을 붙여, 또는 또 다른 분리된 선언문을 사용하여 선언될 수 있다. 위의 선언 u_type 형의 공용체 변수 cnvt를 선언하기 위해서, 다음과 같이 할 수 있다.


union u_type cnvt;


cnvt에서, 정수 i와 문자 ch는 같은 메모리 장소를 공유한다. (물론, i는 2바이트이고 ch는 1바이트를 차지한다.) 그림 7-2는 i와 ch가 어떻게 같은 주소를 공유하는지를 보여 준다. 이렇게 하면, 프로그램에서 언제든지 정수 또는 문자로 cnvt에 저장된 자료를 참조할 수 있다.
공용체 변수가 선언될 때, 컴파일러는 공용체에서 가장 큰 멤버를 저장하는데 충분한 기억장소를 자동적으로 할당한다. 예를 들어, (정수는 2바이트라고 가정할 때) cnvt는 비록 ch가 1바이트만을 요구한다고 하더라도, i를 저장할 수 있도록 2바이트 길이이다.
공용체의 멤버에 접근하는 데는 구조체와 같은 문법을 사용한다. 즉, 도트 및 화살표 연산자를 사용한다. 만일 공용체에서 직접 작업한다면, 도트 연산자를 사용하고, 포인터를 통해서 공용체에 접근한다면, 화살표 연산자를 사용한다. 예를 들어, cnvt의 i요소에 정수 10을 할당하기 위해서, 다음과 같이 쓴다.


cnvt.i = 10;


다음의 예에서, cnvt에 대한 포인터가 함수로 전달된다.


void func1(union u_type *un)
{
  un->i = 10;  /* 함수를 사용하여 10을 cnvt에 할당 */
}

 

그림 7-2  (2바이트 정수라고 할때) i와 ch가 공용체 cnvt를 이용하는 방법


공용체를 이용하면 기계에 의존하지 않는 (이식성 있는) 코드를 만드는데 도움이 될 수 있다. 컴파일러는 공용체 멤버들의 실제 크기를 유지하고 있기 때문에, 어떠한 기계에도 의존하지 않는다. 즉, int, long, float 또는 어떤 다른 것 등의 크기에 대해서는 걱정할 필요가 없는 것이다.
기본적으로 서로 다른 방법으로 union에 저장된 자료를 참조할 수 있기 때문에, 특별한 형변환이 필요할 때 공용체가 자주 사용된다. 예를 들어, 정밀도를 변경하기 위해서, double로 구성된 바이트들을 처리하는데 공용체을 사용할 수 있다.
비표준의 형변환이 필요할 경우 공용체를 유용하게 사용하는 예를 보기 위해서, 정수를 디스크 파일에 출력하는 문제를 생각해 보자. C/C++ 표준 라이브러리에서, 정수를 파일에 출력하기 위해서 특별하게 정의되는 함수는 없다. 비록 fwrite()를 이용하여 (정수를 포함하는) 어떤 형의 자료도 출력할 수 있지만, fwrite()를 이용하면 어떤 간단한 연산조차 허용되지 않는다. 그러나, 공용체를 사용하면, 정수의 2진 표현을 한번에 한 바이트씩 파일에 출력하는 putw()라는 함수를 쉽게 만들 수 있다. 이를 위해서, 먼저 하나의 정수와 2바이트 문자 배열로 구성되는 다음과 같은 union을 생성한다.


union pw {
  int i;
  char ch[2];
};


다음 프로그램은 putw()를 만드는데 공용체 pw를 사용하고 있다.


#include <stdio.h>
union pw {
  int i;
  char ch[2];
};

putw(int num, FILE *fp);

void main(void)
{
  FILE *fp;

  fp = fopen("test.tmp", "w+");

  putw(1000, fp);  /* 정수로 1000 값을 출력 */
  fclose(fp);
}

putw(int num, FILE *fp)
{
  union pw word;

  word.i = num;


  putc(word.ch[0], fp);  /* 처음 반 출력 */
  return putc(word.ch[1], fp);  /* 두 번째 반 출력 */
}


비록 putw()가 정수로 호출되더라도, 한 번에 한 바이트씩 디스크 파일에 정수의 각 바이트를 출력하기 위해서 표준 함수 putc()를 여전히 사용할 수 있다.

주의
C++는 익명의 공용체(anonymous union)라는 특별한 형태의 공용체를 제공하고 있다. 이것은 제 2부에서 다룬다.

 


7.8  나열형

나열형(enumeration)은 특정 변수가 가질 수 있는 모든 합법적인 값들을 지정하는 이름을 가진 정수형 상수들의 집합이다. 나열형은 우리의 일상 생활에서도 많이 찾아 볼 수 있다. 예를 들어, 미국에서 사용하고 있는 동전의 나열은 다음과 같다.

 penny, nickel, dime, quarter, half-dollar, dollar

나열형은 구조체와 매우 유사하게 정의된다. 즉, 키워드 enum은 나열형의 시작을 나타낸다. 다음은 나열형의 일반형식이다.

 enum tag { enumeration list } variable_list;

여기서, 태그(tag)와 variable_list는 있어도 되고 없어도 되는 선택사항이다. 구조체에서처럼, 나열형 태그이름은 이 형의 변수를 선언하는데 사용된다. 다음 코드는 coin이라는 나열형을 정의하며 그 형의 money라는 변수를 선언한다.


enum coin { penny, nickel, dime, quarter, half-dollar, dollar };
enum coin money;

이렇게 선언될 때, 다음 문장은 옳은 것이다.


money = dime;
if (money==quarter) printf("Money is a quarter.\n");


나열형에서 중요한 점은 각 요소들이 정수값을 내포하고 있다는 것이다. 즉, 이들은 정수가 사용될 수 있는 어떤 곳에서도, 정수로 사용될 수 있다. 각 요소는 바로 앞에 나열된 요소의 값보다 하나 큰 정수값을 갖는다. 이때 가장 처음에 나열된 요소의 값은 0이다. 그러므로, 다음은 화면에 0 2를 출력한다.


printf("%d %d", penny, dime);


또한 초기화를 이용하여 하나 이상의 요소들의 값을 지정할 수 있다. 이를 위해서 해당 요소 다음에 등호(=)를 붙이고 정수값을 사용한다. 이때 초기화된 요소의 다음 요소에는 바로 이전의 초기값보다 하나 큰 값이 할당된다. 예를 들어, 다음 코드는 quarter에 100 값을 할당한다.


enum coin {penny, nickel, dime, quarter = 100, half-dollar, dollar};


이때, 각 요소들의 정수 값은 다음과 같이 된다.

 penny  0
 nickel  1
 dime  2
 quarter  100
 half-dollar 101
 dollar  102

나열형에서 오류가 발생할 수 있는 경우는 해당 요소들 자체 심볼이 직접 입력 및 출력될 수 없다는 것이다. 예를 들어, 다음 코드는 기대한 대로 수행되지 않는다.


/* 이것은 동작하지 않는다 */
money = dollar;
printf("%s", money);

여기서, dollar는 특정 정수에 대한 이름일 뿐이라는 것을 명심하자. 즉, 이것은 문자열이 아니다. 같은 이유 때문에, 다음 코드도 기대한 결과를 얻을 수 없다.


/* 이것은 동작하지 않는다 */
strcpy(money, "dime");


즉, 특정 요소의 이름을 포함하는 문자열은 그 요소에 대해서 자동적으로 변환되는 것이 아니다.
실제로, (만일 자체의 정수값이 설정되지 않는다면) 나열형 요소들을 입력하고 출력하는 코드를 만드는 것은 매우 지루한 것이다. 예를 들어, money가 포함하는 동전의 이름을 출력하기 위해서 다음의 코드가 필요하다.


switch (money) {
  case penny: printf("penny");
    break;
  case nickel: printf("nickel");
    break;
  case dime: printf("dime");
    break;
  case quarter: printf("quarter");
    break;
  case half_dollar: printf("half_dollar");
    break;
  case dollar: printf("dollar");
}


또 다른 방법으로, 문자열의 배열을 선언하여 나열형의 값을 대응하는 문자열에 대한 인덱스로 사용할 수 있다. 예를 들어, 다음 코드는 적절한 문자열을 출력한다.


char name[][12] = {
  “penny",
  "nickel",
  "dime",
  "quarter",
  "half_dollar",
  "dollar"
};

printf("%s", name[money]);


물론, 이것은 문자열 배열의 인덱스가 0부터 시작하기 때문에, 어떠한 요소도 초기화되지 않을 때만 가능하다.
입출력 연산을 위해 나열형 값이 수동적으로 사람이 읽을 수 있는 문자열 값으로 변환되어야 하기 때문에, 이 값들은 이러한 변환이 필요하지 않은 루틴에서 가장 유용하게 사용될 수 있다. 예를 들어, 나열형은 컴파일러의 심볼 테이블을 정의하는데 자주 사용된다. 나열형은 또한 변수들이 올바른 값들을 할당하고 있음을 확인하는 컴파일시 채크를 제공함으로써 프로그램의 타당성을 증명하는데 사용된다.

 

7.9  이식성을 보장하기 위한 sizeof

구조체와 공용체는 서로 다른 크기의 변수들을 생성하는데 사용될 수 있으며, 이 변수들의 실제 크기는 기계에 따라 다를 수 있음을 이미 알아 보았다. sizeof 연산자는 어떤 변수 또는 형에 대한 크기를 계산하여, 프로그램에서 기계에 의존적인 코드를 제거하는데 도움이 된다. 이 연산자는 구조체와 공용체에서 특히 유용하다.
이제부터, 많은 C/C++ 컴파일러에서 공통적으로 사용하는 다음의 몇몇 형에 대한 크기를 가정해서 설명해 보자.

  형   바이트 크기
  char       1
  int        2
  float       4

그러므로, 다음 코드는 화면에 숫자 1, 2, 및 4를 출력할 것이다.


char ch;
int i;
float f;
printf("%d", sizeof(ch));
printf("%d", sizeof(i));
printf("%d", sizeof(f));


특정 구조체의 크기는 그 구성멤버들의 크기의 합과 같거나 이 보다 크다. 이에 대한 예를 들어보자.


struct s {
  char ch;
  int i;
  float f;
} s_var;


여기서, sizeof(s_var)는 적어도 7(즉, 4+2+1)이다. 그러나, 컴파일러가 단어 또는 단락(paragraph) 할당으로 구조체를 확보하도록 허용하기 때문에, s_var의 크기는 이보다 클 수 있다. (단락은 16바이트이다.) 구조체의 크기는 자신의 멤버들의 크기 합보다 크게 될 수 있기 때문에, 구조체의 크기를 알고자 할 때는 항상 sizeof을 사용해야 한다.
sizeof은 컴파일시의 연산자이기 때문에, 어떤 변수의 크기를 계산하기 위해서 필요한 모든 정보는 컴파일시에 알려진다. 공용체의 크기는 항상 자신의 가장 큰 멤버의 크기와 같기 때문에, 이것은 공용체에서 특히 의미가 있다. 예를 들어, 다음을 생각해 보자.


union u {
  char ch;
  int i;
  float f;
} u_var;


여기서, sizeof(u_var)은 4이다. 실행시에, u_var에 실제로 무엇을 저장하는가는 문제가 아니다. 여기서의 문제는 어떤 union도 자신의 가장 큰 요소의 크기만큼 커야하기 때문에, 자신의 가장 큰 멤버의 크기를 가진다.

 

7.10  typedef

C는 키워드 typedef를 사용하여 새로운 자료형 이름을 명확하게 정의할 수 있도록 한다. 실제로 이것은 새로운 자료형을 생성하는 것이 아니라, 기존의 자료형에 새로운 이름을 부여하는 것이다. 이것은 기계에 의존적인 프로그램을 더욱 이식성이 좋은 것으로 만드는데 도움이 된다. 만일 사용자의 프로그램에서 이용된 기계에 의존적인 각 자료형에 대해서 사용자 자신의 형이름을 정의한다면, 이것을 새로운 환경에서 컴파일할 때 typedef문을 이용한 것만이 변경될 수 있다. typedef는 또한 표준 자료형에 의미 있는 이름을 부여함으로써, 사용자의 코드에 대한 자체 문장화에 도움이 될 수도 있다. 다음은 typedef의 일반 형식이다.

 typedef type newname;

여기서, type은 어떤 기존의 자료형이며, newname은 이 형에 대한 새로운 이름이다. 사용자가 정의한 새로운 이름은 기존의 형이름과 대치되는 것이 아니라, 추가되는 것이다.
예를 들어, 다음과 같이 float에 대한 새로운 이름을 만들 수 있다.


typedef float balance;


이 문장은 float에 대한 또다른 이름이 balance라는 것을 컴파일러에게 알려준다. 다음은 balance를 사용하여 float형의 변수를 생성한 것이다.


balance over_due;


여기서, over_due는 float형의 또 다른 용어인 balance 형의 실수형 변수이다.
또한, 위에서처럼 balance가 이미 정의되었다면, 이것은 또 다른 typedef에서도 사용될 수 있다. 예를 들어, 다음은 overdraft가 float형의 다른 이름인 balance에 대한 또 다른 이름이라는 것을 컴파일러에게 알려준다.


typedef balance overdraft;


typedef을 사용하면 사용자의 프로그램을 읽기도 쉽고, 새로운 기계에 이식하기도 쉬워진다. 그러나, 이것으로 어떤 새로운 자료형을 생성하는 것은 아님을 명심하기 바란다.


Posted by 영웅기삼
,