10. struct, union, enum, typedef

struct, union, enum, typedef 등은 새로운 자료형을 만들고, 문자화 된 상수 리스트들을 만들고, 변수 타입명을 대치하는 등, C에서 자료를 다루고 접근하는데 있어서 많은 편리한 기능들을 제공합니다. 이번 장에서는 이에 대해서 간단하게 알아 보겠습니다.

10.1 struct (구조체)
C에는 구조체(structure)라고 불리우는 사용자가 정의할 수 있는 자료형이 있습니다. 구조체는 하나 이상의 변수를 포함할 수 있는 집합체라고 보시면 됩니다.

1) 구조체의 필요성

어떤 프로그램이 직원들의 사원번호, 이름, 나이, 성별들을 관리한다고 가정합니다. 아래와 같이 직원 개인의 정보에 관한 변수들이 있습니다.

int employee_id;                    /* 사원번호 */
char employee_name[12];    /* 이름 */
int employee_age;                  /* 나이 */
int employee_sex;                  /* 성별 */

직원에 관련된 부분을 처리할 때, 대부분 이 변수들은 같이 사용될 것 입니다. 아래와 같이 직원정보를 저장하는 함수는 관련된 모든 변수들을 인자로 넘겨야 합니다.

int set_employee(int id, char* name, int age, int set);

또한 정보를 얻을 때도 함수는 반환값이 하나밖에 없으니, 아래와 같이 함수를 만들어 주어야 합니다.

int get_emploeyee_age(int id);
char* get_emploeyee_name(int id);
int get_emploeyee_sex(int id);

같이 사용되는 변수들이기 때문에, 하나로 그룹화를 해 두면 사용이 편할 것 같습니다.


2) 구조체의 선언

이제 구조체에 대해서 알아 보겠습니다. 구조체는 아래와 같이 선언합니다.

struct [구조체 명]
{
    [변수]
    [변수]
       .
       .
       .
};

[구조체명] 역시 변수명이나 함수명 사용 규칙과 동일하며, [변수]는 C에서 사용 가능한 모든 변수들이 올 수 있습니다.

1) 장에서 사용한 employee_ 로 시작하는 직원 변수들을 하나의 직원 구조체로 묶어 보겠습니다. 아래와 같이 구조체를 선언합니다.

struct employee
{
    int id;   
    char name[12];
    int age;
    int sex;
};

구조체의 멤버변수들은 employee라는 구조체 안에 있으므로, 직원에 관련된 변수임을 알리는 employee_를 생략하였습니다. 이제 위의 employee 정보를 저장하고 불러오는 함수들을 구조체를 사용하여 아래와 같이 단일 인자나 반환값으로 사용할 수 있습니다.

int set_employee(struct employee emp);
struct employee get_employee(int id);

구조체는 이와같이 변수의 사용을 명확히 하고, 함수로 전달이나 반환을 용이하게 해줍니다.


3) 구조체의 사용

이제 구조체를 사용하는 간단한 소스를 보겠습니다.

#include <stdio.h>
#include <string.h>

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

void print_employee(struct employee emp);

int main(int argc, char* argv[])
{
    struct employee emp1;
    struct employee emp2;

    emp1.id = 1;
    strcpy(emp1.name, "def");
    emp1.age = 29;
    emp1.sex = 1;

    emp2.id = 2;
    strcpy(emp2.name, "abc");
    emp2.age = 34;
    emp2.sex = 2;

    print_employee(emp1);
    print_employee(emp2);
}

void print_employee(struct employee emp)
{
    char c;
    
    if(emp.sex == 1)
        c = 'M';
    else    
        c = 'W';
        
    printf("%d: name %s, age %d, sex %c\n", emp.id, emp.name, emp.age, emp.sex);
}

struct employee emp1;
구조체는 다음과 같이 사용합니다.

struct [구조체 이름] [변수명];

emp1.id = 1;
구조체의 사용은 아래와 같이 .을 구분자로 하여 사용합니다. (-> 도 있지만, 포인터 설명시 하겠습니다.)

[구조체 명].[구조체 멤버 변수 명]

print_employee(struct employee emp);
위에서 예를 든 것과 같이 emp하나만 인자로 넘겨주면, 직원의 id, age, sex를 프린트 할 수 있습니다.

여기서는 포인터의 사용으로 구조체의 이해를 어렵게 하지 않기위해 포인터를 사용하지 않았지만, 일반적으로 구조체를 함수의 인자로 사용할 때는 포인터를 사용합니다. 포인터로 주소만 넘기면 스텍 메모리를 4바이트를 사용하지만, 구조체를 사용하면 구조체 크기만큼 스텍을 사용하기 때문입니다.

구조체의 크기가 적을 경우에는 문제가 되지 않지만, 클 경우에는 문제의 소지가 있고 값을 변경할 경우에 문제가 있을 수 있습니다. (이는 다시 설명하겠습니다.)

그러므로 구조체를 함수의 인자로 사용할 때에는 포인터를 사용하는 것이 좋습니다. 아래는 포인터로 변경한 소스입니다.

#include <stdio.h>
#include <string.h>

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

void print_employee(struct employee* emp);

int main(int argc, char* argv[])
{
    struct employee emp1;
    struct employee emp2;

    emp1.id = 1;
    strcpy(emp1.name, "abc");
    emp1.age = 29;
    emp1.sex = 1;

    emp2.id = 2;
    strcpy(emp2.name, "def");
    emp2.age = 34;
    emp2.sex = 2;

    print_employee(&emp1);
    print_employee(&emp2);
}

void print_employee(struct employee* emp)
{  
    char c;

    if(emp->sex == 1)
        c = 'M';
    else   
        c = 'W';
       
    printf("%d: name %s, age %d, sex %c\n", emp->id, emp->name, emp->age, emp->sex);
}

void print_employee(struct employee* emp);
함수 선언 시 emp 앞에 *)를 넣어 포인터로 넘기도록 변경합니다.

print_employee(&emp1);
print_employee(&emp2);
구조체 대신에 구조체의 포인터를 넘기도록, 주소연산자(&)를 사용합니다.
 
emp->id, emp->name, emp->age, emp->sex
구조체가 포인터일 경우에는 멤버변수를 가르킬 때, "." 대신, "->"를 사용하여야 합니다.


10.2 union (공용체)
공용체는 struct를 union으로 선언한다는 것 이외에는 구조체와 똑 같이 선언 합니다.

union [공용체 명]
{
    [변수]
    [변수]
       .
       .
       .
};

한 가지 다른 점은 멤버 변수들간에 메모리 영역을 공유한다는 것입니다. 그러므로 공용체의 크기는 멤버 변수 중 가장 큰 변수의 크기를 갖습니다. 아래의 소스로 같은 메모리 영역을 공유 한다는 의미에 대해서 알아 보겠습니다.

#include <stdio.h>

int main()
{
    union my_int 
    {
        unsigned int  i;
        unsigned char c[4];
    };

    int i; 
    union my_int mi;

    mi.i = 100;

    for(i = 0; i < 4; i++)
    {
        printf("%d\n", mi.c[i]);
    }

    printf("%x, %x, %x\n", &mi, mi.c, &(mi.i));

    return 0;
}

1) 메모리 공유의 의미

union my_int 
{
    unsigned int  i;    
    unsigned char c[4];
};
위와 같은 선언은 메모리에서 다음과 같이 자리 잡습니다.
사용자 삽입 이미지
mi는 위와같이 메모리 어디인가에 4바이트의 영역을 차지 하고 있습니다. i와 c의 메모리 시작 주소는 위와 같이 동일합니다.

이는 하단의 다음 코드에서
printf("%x, %x, %x\n", &mi, mi.c, &(mi.i));
세 변수의 메모리 주소를 출력해 보면,
> bffffc7c, bffffc7c, bffffc7c
위와 같이 동일하게 출력되는 것을 확인할 수 있습니다.

위에 mi가 공용체가 아닌 구조체로 선언되었다면, 아래와 같이 메모리에 위치할 것입니다.
사용자 삽입 이미지
보시는 바와 같이 mi.i와 mi.c가 각각의 메모리 블럭에 자리 잡고 있습니다.
 

2) 빅 엔디언, 리틀 엔디언

mi.i = 100;
for(i = 0; i < 4; i++)
{
    printf("%d\n", mi.c[i]);
}

mi.i에 100을 대입하면, 같이 메모리를 사용하는 mi.c 배열의 값에도 변경이 있습니다. 위는 제 맥에서 아래와 같이 출력 됩니다.
> 0
> 0
> 0
> 100
보시는 바와 같이 c[3]에 100이 들어 가 있습니다. 제 테스트 환경은 모토롤라 CPU를 사용하는 PPC 아이맥이기 때문에 c[3]에 먼저 값이 들어 가 있습니다. 인텔 CPU를 사용하는 환경에서는 c[0]에 100이 들어 갑니다.

이 차이는 cpu에 따라 바이트를 저장하는 순서가 다르기 때문입니다. 이 순서의 차이는 빅 엔디안과 리틀 엔디안으로 불리어지며, 빅 엔디안은 큰 쪽이 먼저 저장되고 리틀 엔디안은 반대로 작은 쪽이 먼저 저장됩니다.

4바이트를 가진 mi.i는 16진수로 표현하면, 0x00000064입니다. 0x는 16진수라는 것을 의미하며 2자리 당 1바이트 입니다. 빅 엔디언은 이 값이 [00][00][00][64]와 같이 그대로 메모리에 들어 가기 때문에 mi.c[3]에서 100(16진수로는 0x64)이 출력됩니다.

하지만 리틀 엔디언은 값이 위와 반대로 [64][00][00][00]와 같이 저장되기 때문에 mi.c[0]에서 100이 출력 됩니다.

전통적으로 모토롤라 계열의 CPU들은 빅 엔디언, 인텔 계열의 CPU들은 리틀 엔디언을 사용합니다. 넣고 빼는 방법은 동일하기 때문에 값에는 변화가 없습니다. 그렇기 때문에 빅 엔디언/리틀 엔디언에 관해서 신경 쓸 필요는 거의 없습니다.

하지만 위와 같이 union의 사용과 같이 바이트 열의 제어 시에는 유의해야 합니다.


10.3 enum (열거형 상수)

enum은 열거형 상수입니다. 비슷한 속성을 가진 값들이나 내용을 일련된 정수(int)로된 상수로 나타내어 줍니다. enum은 다음과 같이 선언합니다.

enum [열거형 상수명] { [상수명] = [값], ... } [열거형 변수];

[열거형 상수명]은 구조체나 유니온 처럼 변수 선언시 필요한 이름이며 생략할 수 있습니다.
[상수명]은 정수대신 사용할 상수의 이름 입니다. 이 값은 = [값]으로 초기화 할 수 있으며, 값을 생략하면 첫번째 상수부터 0, 1, 2... 차례대로 설정됩니다.
[열거형 변수]
는 enum 값을 사용할 변수 이며, 선언시에는 생략 가능합니다. 열거형 상수 값을 가지는 변수들은 정수형입니다.

아래의 소스를 보시면 쉽게 열거형 변수에 대해서 이해 할 수 있습니다.

#include <stdio.h>

int main()
{
    enum Usergrade { MASTER=0, LEVEL1, LEVEL2, LEVEL3 } u3;
   
    enum Usergrade u1 = MASTER;
    int u2 = LEVEL1;

    u3 = LEVEL2;

    printf("%d, %d, %d\n", u1, u2, u3);
    printf("%d, %d, %d, %d\n", MASTER, LEVEL1, LEVEL2, LEVEL3);

    return 0;
}

enum Usergrade { MASTER=0, LEVEL1, LEVEL2, LEVEL3 } u3;
Usergrade라는 enum 상수들을 선언 합니다. MASTER=0이며 기본 시작값이 0이기 때문에, =0을 생략하여도 의미는 같습니다. MASTER 뒤로 LEVEL1, LEVEL2, LEVEL3은 차례대로 1, 2, 3의 값이 들어 갑니다. 만약 MASTER=10으로 시작하면 그뒤로 차례대로 11, 12, 13의 값이 들어 갑니다.

마지막에 이 상수를 사용하기 위해 u3을 선언되어 있습니다. 변수는 꼭 enum 선언시 선언될 필요는 없습니다.

enum Usergrade u1 = MASTER;
int u2 = LEVEL1;
u3 = LEVEL2;
enum 변수를 선언(구조체와 선언이 유사합니다.)하고, 정수형에 대입하고, 위에서 enum 선언시 , 선언된 변수에 enum 상수값들을 넣어 봅니다.

enum을 사용하는 이유는 소스 작성시 의미를 명확하게 하고, 상수들을 일괄되고 안정되게 사용하기 위해서 입니다.

예를 들어 아래와 같은 소스에서 전자 보다는 후자가 더욱 명확하고 이해하기 쉽습니다.
if(user_grade == 0)
    printf("You are Master");
---------------------------------------------------------------
if(user_grade == MASTER)
   printf("You are Master");


10.4 typedef

typdef는 변수형을 사용자가 지정한 이름으로 사용할 수 있게 해줍니다. typedef는 적절하게 사용하여야 하며, 의미에 맞게 이름을 지어 주어야 합니다. 그렇지 않고 지나치게 많이 변수형들을 대치하거나, 작성자만 알수 있는 이름을 사용하면 제 3자가 소스를 볼 경우에 이해를 어렵게 할 수 있습니다.

1) typedef 선언

typedef  [기존 변수 형] [새로운 변수 형];

[기존 변수 형]은 int, struct [구조체명], enum [enum 명] 등과 같이 C 변수형 또는 사용자가 지정한 변수 형이 올 수 있습니다.

[새로운 변수형]은 위의 기존 변수형을 대신해서 사용할 사용자 정의 변수타입 이름입니다.

예를 들면 unsigned int는
typedef unsigned int uint;
위와 같이 선언해 놓으면 uint a;와 같이 편한게 unsigned int형의 변수를 선언할 수 있습니다.

2) 구조체와의 사용

typdef는 구조체에서 특히 편리하게 사용할 수 있습니다. 위에서 살펴본 employee 구조체를 다시 한번 보겠습니다.

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

typedef struct employee emp;

struct employee e1;
emp e2;

위에 e1과 e2는 똑같이 employee 구조체 변수를 선언하고 있습니다. e2는 typedef된 구조체를 사용하므로 e1보다 간단하게 선언할 수 있습니다. 구조체에서 typedef는 아래와 같이 선언할 때 같이 사용하면 더욱 편리합니다.

typedef struct
{
    int id;
    char name[12];
    int age;
    int sex;
} employee;

employee e1;

'프로그래밍 강좌 > C 언어 기초' 카테고리의 다른 글

11. define과 디버깅  (1) 2007.07.08
9. 배열 (array)  (0) 2007.06.17
8. 포인터 (pointer)  (4) 2007.06.16
7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14
AND