Skip to content

48. 구조체와 공용체


1. 키워드

  • 전방 선언(Forward Declaration)


2. 구조체와 공용체 활용하기

  • 지금까지 구조체와 공용체를 단독으로 사용했다.
  • 하지만 구조체 안에 구조체와 공용체가 들어갈 수 있고, 반대로 공용체 안에 구조체와 공용체가 들어갈 수도 있다.


3. 구조체 안에서 구조체 멤버 사용하기

  • 다음과 같이 구조체는 구조체를 멤버로 가질 수 있다.


#include <stdio.h>

struct Phone // 휴대전화 구조체
{
    int areacode;              // 국가번호
    unsigned long long number; // 휴대전화 번호
};

struct Person // 사람 구조체
{
    char name[20];      // 이름
    int age;            // 나이
    struct Phone phone; // 휴대전화 구조체를 멤버로 가짐
};

int main()
{
    struct Person p1;

    p1.phone.areacode = 82;       // 변수.멤버.멤버 순으로 접근하여 값 할당
    p1.phone.number = 3045671234; // 변수.멤버.멤버 순으로 접근하여 값 할당

    printf("%d %llu\n", p1.phone.areacode, p1.phone.number);

    return 0;
}

// 82 3045671234


  • 구조체를 멤버로 가지려면 다음과 같이 구조체 안에서 구조체 변수를 선언하면 된다.
  • 여기서는 사람 구조체가 휴대전화 구조체를 멤버로 가지고 있다.


struct Person // 사람 구조체
{
    char name[20];      // 이름
    int age;            // 나이
    struct Phone phone; // 휴대전화 구조체를 멤버로 가짐
};


  • 멤버에 접근할 때는 .(점)을 사용하며 가지고 있는 구조체에 계층적으로 접근하면 된다.
  • 다음은 .을 두 번 사용하여 휴대전화 구조체의 areacodenumber 멤버에 접근하여 값을 할당하고, 값을 가져온다.


변수.멤버.멤버
p1.phone.areacode = 82;       // 변수.멤버.멤버 순으로 접근하여 값 할당
p1.phone.number = 3045671234; // 변수.멤버.멤버 순으로 접근하여 값 할당

printf("%d %llu\n", p1.phone.areacode, p1.phone.number);


구조체 정의 순서

  • 구조체를 PhonePerson 순서가 아니라 PersonPhone 순서로 바꿔서 컴파일해 보면 정의되지 않는 구조체를 사용한다는 식으로 에러 메시지가 나온다.
  • 구조체 안에 구조체가 변수로 들어갈 때는 안에 들어가는 구조체를 먼저 선언해 준다.


  • 만약 Phone 구조체를 다른 곳에서는 쓰지 않고, 특정 구조체 안에서만 쓴다면 구조체 안에 구조체를 정의하는 게 더 편리하다.
  • 단, 이때는 구조체를 정의한 뒤 반드시 변수를 선언해야 한다.


struct Person // 사람 구조체
{
    char name[20]; // 이름
    int age;       // 나이
    struct Phone   // 휴대전화 구조체를 사람 구조체 안에 정의
    {
        int areacode;              // 국가번호
        unsigned long long number; // 휴대전화 번호
    } phone;                       // 구조체를 정의하는 동시에 변수 선언
};


구조체 변수를 선언하는 동시에 초기화하기

  • 구조체 변수를 선언하면서 안에 들어있는 구조체까지 초기화하려면 {}(중괄호) 안에 {}를 사용한다.


struct 구조체이름 변수이름 = {값1, 값2, {값3, 값4}};


  • 다음과 같이 구조체 안에 들어있는 구조체 멤버 위치에 {}를 사용하여 값을 묶어주면 된다.


#include <stdio.h>

struct Phone
{
    int areacode;
    unsigned long long number;
};

struct Person
{
    char name[20];
    int age;
    struct Phone phone;
};

int main()
{
    // phone의 멤버 areacode에는 82, number에는 3045671234
    struct Person p1 = {.name = "Andrew", .age = 25, {.areacode = 82, .number = 3045671234}};

    printf("%d %llu\n", p1.phone.areacode, p1.phone.number);

    // phone의 멤버 areacode에는 82, number에는 3087654321
    struct Person p2 = {"Maria", 19, {82, 3087654321}};

    printf("%d %llu\n", p2.phone.areacode, p2.phone.number);

    return 0;
}

// 82 3045671234
// 82 3087654321


4. 구조체 안의 구조체 멤버에 메모리 할당하기

  • 이번에는 구조체 안의 구조체 멤버에 메모리를 할당해 보자.
  • 먼저 다음은 구조체 안에 구조체 멤버가 변수로 있는 상태에서 메모리를 할당하여 사용하는 방법이다.


#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일

struct Phone // 휴대전화 구조체
{
    int areacode;              // 국가번호
    unsigned long long number; // 휴대전화 번호
};

struct Person // 사람 구조체
{
    char name[20];      // 이름
    int age;            // 나이
    struct Phone phone; // 휴대전화 구조체를 멤버로 가짐
};

int main()
{
    struct Person *p1 = malloc(sizeof(struct Person)); // 사람 구조체 포인터에 메모리 할당

    p1->phone.areacode = 82;       // 포인터->멤버.멤버 순으로 접근하여 값 할당
    p1->phone.number = 3045671234; // 포인터->멤버.멤버 순으로 접근하여 값 할당

    printf("%d %llu\n", p1->phone.areacode, p1->phone.number);

    free(p1); // 동적 메모리 해제

    return 0;
}

// 82 3045671234


  • struct Person *p1 = malloc(sizeof(struct Person));과 같이 사람 구조체 포인터에 메모리를 할당한다.
  • p1은 포인터이므로 ->(화살표 연산자)를 사용하여 멤버에 접근한다.
  • 하지만 phone은 포인터가 아닌 일반 변수이므로 .을 사용하여 멤버에 접근한다.
  • 즉, 포인터->멤버.멤버 모양이다.


포인터->멤버.멤버
p1->phone.areacode = 82;       // 포인터->멤버.멤버 순으로 접근하여 값 할당
p1->phone.number = 3045671234; // 포인터->멤버.멤버 순으로 접근하여 값 할당

printf("%d %llu\n", p1->phone.areacode, p1->phone.number);


  • .이냐 ->냐 구분할 때는 현재 구조체가 선언된 상태를 확인하면 된다.
  • 변수면 ., 포인터면 ->이다.
  • 구조체 포인터의 사용이 끝났다면 반드시 free 함수로 동적 메모리를 해제한다.


  • 이번에는 구조체의 포인터를 멤버로 가지는 상황을 알아보자.


#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일

struct Phone // 휴대전화 구조체
{
    int areacode;              // 국가번호
    unsigned long long number; // 휴대전화 번호
};

struct Person // 사람 구조체
{
    char name[20];       // 이름
    int age;             // 나이
    struct Phone *phone; // 휴대전화 구조체 포인터 선언
};

int main()
{
    struct Person *p1 = malloc(sizeof(struct Person)); // 바깥 구조체의 포인터에 메모리 할당
    p1->phone = malloc(sizeof(struct Phone));          // 멤버 포인터에 메모리 할당

    p1->phone->areacode = 82;       // 포인터->포인터->멤버 순으로 접근하여 값 할당
    p1->phone->number = 3045671234; // 포인터->포인터->멤버 순으로 접근하여 값 할당

    printf("%d %llu\n", p1->phone->areacode, p1->phone->number);

    free(p1->phone); // 구조체 멤버의 메모리를 먼저 해제
    free(p1);        // 구조체 메모리 해제

    return 0;
}

// 82 3045671234


  • 사람 구조체를 살펴보면 struct Phone *phone;과 같이 구조체 포인터를 멤버로 가지고 있다.


struct Person // 사람 구조체
{
    char name[20];       // 이름
    int age;             // 나이
    struct Phone *phone; // 휴대전화 구조체 포인터 선언
};


  • 구조체 포인터는 선언만 해서는 사용을 할 수가 없다.
  • 일단 바깥 구조체의 포인터에 메모리를 할당한 뒤 멤버로 있는 구조체 포인터에 메모리를 할당한다.


struct Person *p1 = malloc(sizeof(struct Person)); // 바깥 구조체의 포인터에 메모리 할당
p1->phone = malloc(sizeof(struct Phone));          // 멤버 포인터에 메모리 할당


001


  • p1은 포인터이므로 ->를 사용하면 되고, 구조체 멤버 phone도 메모리를 할당한 포인터이므로 똑같이 ->를 사용하면 된다.
  • 즉, 포인터->포인터->멤버 모양이다.


포인터->포인터->멤버
p1->phone->areacode = 82;       // 포인터->포인터->멤버 순으로 접근하여 값 할당
p1->phone->number = 3045671234; // 포인터->포인터->멤버 순으로 접근하여 값 할당

printf("%d %llu\n", p1->phone->areacode, p1->phone->number);


  • 구조체 포인터 사용이 끝났으면 메모리를 해제해야 하는데 순서가 중요하다.
  • 반드시 안쪽에 있는 멤버부터 메모리를 해제한다.


free(p1->phone); // 구조체 멤버의 메모리를 먼저 해제
free(p1);        // 구조체 메모리 해제


002


  • 바깥에 있는 구조체 포인터를 먼저 해제하면 데이터가 사라지므로 안에 있는 멤버에 더이상 접근할 수 없게 된다.
  • 그래서 멤버 포인터에 저장된 주소도 알 수 없으므로 해제도 할 수 없게 된다.


  • 다음과 같이 멤버 포인터의 주소를 미리 다른 곳에 저장해 두었다면 바깥에 있는 구조체 포인터를 먼저 해제해도 된다.


struct Phone *phone = p1->phone;

free(p1);    // 바깥의 구조체 메모리를 먼저 해제
free(phone); // 미리 저장해 둔 포인터를 이용해서 멤버 포인터 해제


구조체 정의 순서

  • 구조체를 PhonePerson 순서가 아니라 PersonPhone 순서로 바꿔서 컴파일해 보면 정의되지 않은 구조체를 사용한다는 식으로 에러 메시지가 나온다.
  • 구조체 안에 구조체가 포인터로 들어갈 때는 전방 선언을 사용하면 정의되지 않은 구조체를 먼저 사용할 수 있다.


struct 구조체이름;
struct Phone; // Phone 구조체 전방 선언

struct Person
{
    char name[20];
    int age;
    struct Phone *phone; // 아직 정의되지 않은 구조체를 포인터로 선언
};

struct Phone // Phone 구조체 정의
{
    int areacode;
    unsigned long long number;
};


  • 전방 선언은 struct Phone;과 같이 struct 키워드에 구조체 이름을 지정해 주면 된다.
  • 단, 전방 선언을 했다면 반드시 구조체를 따로 정의해 주어야 한다.


5. 익명 구조체와 익명 공용체 활용하기

  • 이번에는 익명 구조체와 익명 공용체를 활용하는 방법을 알아보자.
  • 다음은 3차원 벡터 좌표를 구조체와 공용체로 나타낸 예제이다.


#include <stdio.h>

struct Vector3 // 3차원 벡터 좌표
{
    union // 익명 공용체
    {
        struct // 익명 구조체
        {
            float x; // x 좌표
            float y; // y 좌표
            float z; // z 좌표
        };
        float v[3]; // 좌표를 배열로 저장
    };
};

int main()
{
    struct Vector3 pos;

    for (int i = 0; i < 3; i++) // 3번 반복
    {
        pos.v[i] = 1.0f; // v는 배열이므로 인덱스로 접근할 수 있음
    }

    printf("%f %f %f\n", pos.x, pos.y, pos.z); // x, y, z로 접근

    return 0;
}

// 1.000000 1.000000 1.000000


  • x, y, z 좌표를 저장하는 Vector3 구조체를 정의했다.
  • 먼저 맨 안쪽 x, y, zfloat 타입으로 선언되어 있으며 익명 구조체이다.
  • 이때 x, y, z는 각각의 값을 독립적인 공간에 저장하기 위해 구조체로 정의한다.
  • 또한, pos.x처럼 멤버에 바로 접근하기 위해 익명 구조체로 정의한다.
  • 그리고 익명 공용체가 x, y, z 익명 구조체와 배열 v를 감싸고 있다.
  • pos.v처럼 멤버에 바로 접근하기 위해 익명 공용체로 정의한다.


struct Vector3 // 3차원 벡터 좌표
{
    union // 익명 공용체
    {
        struct // 익명 구조체
        {
            float x; // x 좌표
            float y; // y 좌표
            float z; // z 좌표
        };
        float v[3]; // 좌표를 배열로 저장
    };
};


  • float 타입 x, y, z는 변수 3개이고, float v[3]은 배열의 요소가 3개이다.
  • 자료형도 같고 개수도 같으므로 결국 같은 공간을 차지한다.
  • 따라서 공용체로 묶어주면 x, y, zv[3]은 같은 공간을 공유하게 된다.


003


  • 다음과 같이 v는 배열이므로 인덱스로 접근하여 값을 할당할 수 있다.
  • 여기서는 3번 반복하면서 v[0], v[1], v[2]1.0을 할당했다.


for (int i = 0; i < 3; i++) // 3번 반복
{
    pos.v[i] = 1.0f; // v는 배열이므로 인덱스로 접근할 수 있음
}


  • 공용체에 값을 넣었으므로 값은 같은 공간에 있다.
  • 따라서 x, y, z 멤버로도 접근할 수도 있다.


printf("%f %f %f\n", pos.x, pos.y, pos.z); // x, y, z로 접근


  • 이처럼 익명 구조체와 익명 공용체를 사용하면 같은 값이지만 이름과 형태를 달리하여 접근할 수 있다.

References