본문 바로가기

Programming/C

[ProjectH4C] 코딩도장 Unit 54~55 Write-Up

 

공용체

공용체란, 구조체와 정의 방법은 같지만 멤버를 저장하는 방식에 차이가 있다. 구조체는 멤버들이 각각 공간을 차지하지만 공용체는 하나의 공간에 모든 멤버가 공간을 공유한다. 그림으로 표현하면 다음과 같다.

구조체와 공용체의 메모리 차이. 출처 코딩 54-1

공용체는 멤버로 선언된 자료형 중 가장 큰 자료형의 공간을 공유한다. 위의 그림의 경우 int형이 4바이트로 가장 크기 때문에 short, char 자료형이 int의 공간을 공유한다. 공용체를 정의하는 방법은 다음과 같다.

union 공용체이름{
    자료형 멤버1;
    자료형 멤버2;
    ...
};

각 멤버에 접근하는 방법이나 사용하는 법은 구조체와 동일하며 공용체도 typedef 선언과 익명 공용체 선언이 가능하다.

 

엔디언(Endian)

엔디언이란, 값을 메모리에 저장할 때 연속된 공간에 값을 배열하는 방식을 뜻한다. 엔디언엔 방식에 따라 빅 엔디언과 리틀 엔디언이 있다.

x86(64비트) 계열의 CPU는 값을 메모리에 저장할 때 리틀 엔디언(Little-Endian) 방식으로 저장한다. 리틀 엔디언이란, 저장할 값을 1바이트 단위로 쪼개서 메모리상의 연속된 공간에 낮은 자리수의 수가 앞쪽에 저장되는 방식이다. 예를 들어 0x12345678을 메모리에 저장한다고 가정하면 먼저 값을 1바이트 단위로 쪼개면 12, 34, 56, 78이 된다. 이 때, 가장 낮은 자리수인 78이 가장 앞에, 그 다음 56이, 그 다음엔 34가, 그 다음엔 12가 연속된 메모리 공간에 저장된다. 빅 엔디언은 반대로 높은 자리수의 숫자부터 1바이트씩 앞쪽에 저장된다.

 

리틀 엔디언과 빅 엔디언. 출처: 코딩도장 54-4.

아래는 예시 코드이다.

#include <stdio.h>

union Data {    // 공용체 정의
    char c1;
    short num1;
    int num2;
};

int main()
{
    union Data d1;    // 공용체 변수 선언

    d1.num2 = 0x12345678;    // 리틀 엔디언에서는 메모리에 저장될 때 78 56 34 12로 저장됨

    printf("0x%x\n", d1.num2);    // 0x12345678: 4바이트 전체 값 출력
    printf("0x%x\n", d1.num1);    // 0x5678: 앞의 2바이트 값만 출력
    printf("0x%x\n", d1.c1);      // 0x78: 앞의 1바이트 값만 출력

    printf("%d\n", sizeof(d1));   // 4: 공용체의 전체 크기는 가장 큰 자료형의 크기

    return 0;
}

 

먼저 공용체 변수 d1을 선언한다. 이 때 Data 공용체에서 가장 큰 변수인 num2의 공간 내에 c1, num1이 존재하게 된다. num2에 0x12345678을 저장하면, 이는 리틀 엔디언 방식으로 메모리 상에 78, 56, 34, 12의 순으로 저장된다. 그 후에 num2를 출력할 땐, 메모리 상에서 또 반대로 가져와서 0x12345678이 출력된다. 하지만 num1을 출력하면 0x1234가 아닌 0x5678이 출력된다. num1의 공간은 num2의 앞의 2바이트를 주소로 가지고 있기 때문에 num1의 메모리에는 0x78, 0x56이 순서대로 저장되어 있다. 이를 출력하면 반대로 출력되어서 0x5678이 된다. 같은 이유로 c1을 출력하면 12가 아닌 0x78이 출력된다. 

 

 

공용체 포인터

공용체도 역시 포인터를 선언할 수 있으며, 동적 할당도 가능하다.

union 공용체이름 *포인터이름 = malloc(sizeof(union 공용체이름));

 

메모리를 공유하는 것만 다를 뿐 사용 방식은 구조체와 완전히 같다.

#define _CRT_SECURE_NO_WARNINGS     // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일
#include <string.h>    // strcpy 함수가 선언된 헤더 파일

union Box {    // 공용체 정의
    short candy;
    float snack;
    char doll[8];
};

int main()
{
    union Box *b1 = malloc(sizeof(union Box));    // 공용체 포인터 선언, 메모리 할당

    printf("%d\n", sizeof(union Box));    // 8: 공용체의 전체 크기는 가장 큰 자료형의 크기

    strcpy(b1->doll, "bear");     // doll에 문자열 bear 복사

    printf("%d\n", b1->candy);    // 25954
    printf("%f\n", b1->snack);    // 4464428256607938511036928229376.000000
    printf("%s\n", b1->doll);     // bear

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

    return 0;
}

 

구조체와 공용체 활용

구조체와 공용체는 구조체 또는 공용체를 멤버로 가질 수도 있다. 먼저 구조체를 멤버로 가지는 구조체이다.

#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);    // 82 3045671234

    return 0;
}

 

먼저 국가번호와 전화번호를 멤버로 가지는 Phone 구조체를 선언한 후에 이름, 나이, Phone 구조체를 멤버로 가지는 Person 구조체를 정의했다. main 함수에서는 Person 구조체 변수 p1을 선언해서 사용하였는데, 이 때 Person 구조체의 멤버인 Phone 구조체의 멤버에 접근하는 방법은 p1.phone.areacode와 같이 점을 두 번 사용해서 접근하면 된다. Person 구조체의 멤버인 이름, 나이에 접근할 때에는 평소처럼 p1.name, p1.age와 같이 점을 하나만 사용해서 접근하면 된다. 

구조체를 정의할 때, Phone 구조체보다 Person 구조체를 먼저 정의하면 Person 구조체 내의 Phone 구조체에 대한 정의가 아직 존재하지 않기 때문에 에러가 나기 때문에 구조체를 멤버로 사용할 땐 정의하는 순서를 주의해야 한다. 

 

만약 위의 코드에서 p1 구조체를 동적 할당으로 선언하면, p1 구조체의 멤버에 접근할 때에는 화살표 연산자(->)를 이용해서 접근해야 하지만, 멤버 구조체인 Phone은 동적으로 할당한 게 아니기 때문에 Phone 구조체의 멤버에 접근할 때에는 점(.)을 이용해서

p1->phone.areacode와 같이 접근해야 한다.

 

 

멤버 구조체도 동적으로 할당 가능하다. 이 때에는 구조체 멤버를 struct Phone *phone 으로 정의해주어야 한다.

#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);    // 82 3045671234

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

    return 0;
}

*p1을 동적 할당 해준 후에 그의 멤버 구조체인 p1->phone도 phone 구조체의 크기만큼 동적할당 해주었다. 이 때에는 phone의 멤버에 접근할 때 화살표를 두개 써서 p1->phone->areacode와 같이 접근해야 한다.

 

 

 

 

 

 

 


 

#54.4 퀴즈

공용체의 크기는 멤버 중 가장 큰 자료형의 크기와 같다. 크기가 가장 큰 자료형은 long long int (8바이트)이다.

 

 

공용체 포인터 변수에 동적 할당하는 방법은

union 공용체이름 *포인터이름 = malloc(sizeof(union 공용체이름));

이다. 답은 d.

 

num1은 2바이트이므로 0x1111이 저장되어있다. 

 

 

#54.5 연습문제: 정수 데이터 공용체 정의하기

#include <stdio.h>

①        Data          
_______________________                         
_______________________                        
_______________________                        

int main()
{
    ②_______________ d1;

    ③_______________

    printf("0x%x 0x%x\n", d1.num1, d1.c1);

    return 0;
}

공용체를 정의해야 하는데, 출력 결과가 0x5678 0x78로 각각 2바이트, 1바이트이기 때문에 해당 크기의 자료형으로 변수를 선언한 후에, 2바이트의 변수에 0x5678을 저장하면 리틀 엔디언 방식이 적용되어서 1바이트의 변수엔 0x78이 저장되게 된다.

 

1.

union Data{
    char c1;
    short num1;
};

2. union Data

3. d1.num = 0x5678;

 

 

#54.6 연습문제: 공용체 포인터 사용하기

#include <stdio.h>
#include <stdlib.h>

union Data {
    short num1;
    int num2;
};

int main()
{
    __________________________________________

    d1->num2 = 1;

    printf("%d %d\n", d1->num1, d1->num2);

    free(d1);

    return 0;
}

출력함수를 보면 화살표 연산자(->)를 사용했기 때문에 포인터 변수로 선언하고 동적 할당 해주어야 한다.

union Data *d1 = malloc(sizeof(union Data));

 

 

#54.7 심사문제: 정수 데이터 공용체 정의하기

d1.c1과 sizeof(d1))의 출력 결과가 각각 0x11, 4이다. c1은 1바이트 크기의 변수여야 하고, 유니온 멤버 중 가장 큰 자료형이 4바이트여야 한다.

 

union Data{
    int num1;
    char c1;
};

 

 

 

#54.8 심사문제: 공용체 포인터 사용하기

공용체 포인터를 선언과 동시에 동적 할당 해주고, 다음 줄에 자료형의 크기가 가장 큰 num2에 0x11111111을 저장해주면 num1을 출력했을 때 0x1111이 출력되게 된다.

 

union Data *d1 = malloc(sizeof(union Data));

d1->num2 = 0x11111111;

 

 

 

 

#55.4 퀴즈

두 구조체가 모두 포인터 변수가 아니기 때문에 둘 다 점(.)으로 접근해야 한다. 답은 d.

 

p1이 구조체 포인터 변수이기 때문에 멤버에 접근할 때 화살표 연산자(->)를 사용해야 한다. 답은 c.

 

둘 다 구조체 포인터 변수로 동적 할당을 했기 때문에 멤버에 접근할 때 화살표 연산자(->)를 사용해야 한다. 답은 b.

 

x, y, z는 구조체 멤버이기 때문에 각각 4바이트씩 연속된 공간을 가진다.

x, y, z와 v[3]는 공용체이기 때문에 같은 메모리를 사용하는데 v[0]과 x. v[1]과 y, v[2]와 z가 각각 같은 메모리를 사용하게 된다.

그렇기 때문에 y에 접근하려면 pos.y 또는 pos.v[1]로 접근해야 한다. 답은 d.

 

 

 

#55.5 연습문제: 게임 캐릭터 구조체 만들기

 

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

struct Stats {
    float health;
    float healthRegen;
    unsigned int mana;
    float manaRegen;
    float range;
    float attackDamage;
    float armor;
    float attackSpeed;
    float magicResist;
    unsigned int movementSpeed;
};

struct Champion {
    char name[20];
    ______________________________
    float abilityPower;
};

int main()
{
    struct Champion lux;

    strcpy(lux.name, "Lux");
    lux.stats.health = 477.72f;
    lux.stats.healthRegen = 1.08f;
    lux.stats.mana = 334;
    lux.stats.manaRegen = 1.24f;
    lux.stats.range = 550;
    lux.stats.attackDamage = 55.5f;
    lux.stats.attackSpeed = 0.625f;
    lux.stats.armor = 18.72f;
    lux.stats.magicResist = 30;
    lux.stats.movementSpeed = 330;
    lux.abilityPower = 0;
    
    printf("%u %f\n", lux.stats.mana, lux.stats.manaRegen);

    return 0;
}

멤버 접근 형태와 출력 형태를 보면 lux의 멤버인 stats의 멤버에 접근하고 있다. 이는 Champion 구조체 변수 lux의 멤버에 stats 구조체가 포함되어 있다는 것을 알려준다.

struct Stats stats;

struct Stats stats;

 

 

#55.6 연습문제: 게임 캐릭터 구조체 사용하기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct Stats {
    float health;
    float healthRegen;
    unsigned int mana;
    float manaRegen;
    float range;
    float attackDamage;
    float armor;
    float attackSpeed;
    float magicResist;
    unsigned int movementSpeed;
};

struct Champion {
    char name[20];
    struct Stats stats;
    float abilityPower;
};

int main()
{
    _____________________________________________  

    strcpy(lux->name, "Lux");
    lux->stats.health = 477.72f;
    lux->stats.healthRegen = 1.08f;
    lux->stats.mana = 334;
    lux->stats.manaRegen = 1.24f;
    lux->stats.range = 550;
    lux->stats.attackDamage = 55.5f;
    lux->stats.attackSpeed = 0.625f;
    lux->stats.armor = 18.72f;
    lux->stats.magicResist = 30;
    lux->stats.movementSpeed = 330;
    lux->abilityPower = 0;
    
    printf("%u %f\n", lux->stats.mana, lux->stats.manaRegen);

    free(lux);    

    return 0;
}

lux 변수가 멤버에 화살표 연산자(->)를 이용해서 접근하기 때문에 lux는 Champion 구조체의 포인터이다.

struct Champion lux = malloc(sizeof(struct Champion));

struct Champion *lux = malloc(sizeof(struct Champion));

 

 

#55.7 연습문제: 장치 옵션 구조체 만들기

#include <stdio.h>

struct DeviceOption {
    ____________________________
        unsigned long long option;
        ...
    ____________________________                            
};

int main()
{
    struct DeviceOption opt;

    opt.boot[0] = 0x01;
    opt.boot[1] = 0x02;
    opt.boot[2] = 0x03;
    opt.boot[3] = 0x04;
    opt.interrupt[0] = 0x05;
    opt.interrupt[1] = 0x06;
    opt.bus[0] = 0x07;
    opt.bus[1] = 0x11;

    printf("0x%llx\n", opt.option);

    return 0;
}

출력 결과가 0x1107060504030201이 되어야 한다. main 함수를 보면 boot 배열 4개, interrupt 배열 2개, bus 배열 2개를 사용했다. 또 지정해준 값의 형태와 출력형태를 보면 출력결과가 값을 지정해준 반대로 출력되는 것을 알 수 있다. 이를 통해, DeviceOption 구조체 내에 익명 공용체를 선언해서 익명 구조체와 option 변수를 멤버로 두고, 익명 구조체 안에 boot, interrupt, bus 배열을 각각 4개, 2개, 2개씩 선언하고, 각각의 크기가 1바이트여야 하기 때문에 자료형은 모두 char형으로 선언해준다.

 

struct DeviceOption {
    union {
        struct {
            char boot[4];
            char interrupt[2];
            char bus[2];
        };
        unsigned long long option;
    };               
};

 

 

#55.8 심사문제: 게임 캐릭터 구조체 사용하기

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

struct Stats {
    float health;
    float healthRegen;
    unsigned int mana;
    float manaRegen;
    float range;
    float attackDamage;
    float armor;
    float attackSpeed;
    float magicResist;
    unsigned int movementSpeed;
};
 
struct Champion {
    char name[20];
    struct Stats stats;
    float abilityPower;
};

int main()
{
    ______________________

    ______________________
    ______________________

    swain.stats.healthRegen = 1.48f;
    swain.stats.mana = 290;
    swain.stats.manaRegen = 1.49f;
    swain.stats.range = 500;
    swain.stats.attackDamage = 52.0f;
    swain.stats.attackSpeed = 0.625f;
    swain.stats.armor = 20.0f;
    swain.stats.magicResist = 30;
    swain.stats.movementSpeed = 335;
    swain.abilityPower = 0;

    printf("%s %f\n", swain.name, swain.stats.health);

    return 0;
}

 

실행 결과

Swain 463.000000

 

name 멤버를 가지는 구조체는 Champion이다. 고로 swain 변수는 Champion 구조체 변수임을 알 수 있다.

swain.name과 swain.stats.health를 초기화 해주는 코드가 따로 없기 때문에 해당 멤버들을 초기화하는 코드도 작성해야 한다.

 

struct Champion swain;

strcpy(swain.name, "Swain");
swain.stats.health = 463.000000;

 

 

 

#55.9 심사문제: 게임 캐릭터 구조체 포인터 사용하기

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct Stats {
    float health;
    float healthRegen;
    unsigned int mana;
    float manaRegen;
    float range;
    float attackDamage;
    float armor;
    float attackSpeed;
    float magicResist;
    unsigned int movementSpeed;
};
 
struct Champion {
    char name[20];
    struct Stats *stats;
    float abilityPower;
};

int main()
{
    _____________________________
    _____________________________

    _____________________________
    _____________________________

    swain->stats->healthRegen = 1.48f;
    swain->stats->mana = 290;
    swain->stats->manaRegen = 1.49f;
    swain->stats->range = 500;
    swain->stats->attackDamage = 52.0f;
    swain->stats->attackSpeed = 0.625f;
    swain->stats->armor = 20.0f;
    swain->stats->magicResist = 30;
    swain->stats->movementSpeed = 335;
    swain->abilityPower = 0;

    printf("%s %f\n", swain->name, swain->stats->health);

    free(swain->stats);    
    free(swain);    

    return 0;
}

 

멤버에 접근할 때 화살표 연산자(->)를 사용하고, 멤버 구조체의 멤버에 접근할 때에도 화살표 연산자를 사용하는 걸로 둘 다 포인터 변수이고, 동적할당 해야한다는 것을 알 수 있다. 그 후에 swain->name과 swain->stats->health를 초기화해주는 코드를 작성하면 된다.

 

struct Champion *swain = malloc(sizeof(struct Champion));
swain->stats = malloc(sizeof(struct Stats));

strcpy(swain->name, "Swain");
swain->stats->health = 463.000000;

 

 

#55.10 심사문제: 장치 옵션 구조체 만들기

#include <stdio.h>

____________________________
____________________________
____________________________
____________________________
____________________________
____________________________
____________________________
____________________________
____________________________

int main()
{
    struct DeviceOption opt;

    opt.boot = 0x22;
    opt.interrupt = 0x11;

    printf("0x%x\n", opt.option);

    return 0;
}

 

실행 결과

0x1122

먼저 DeviceOption 구조체를 정의해야 한다. DeviceOption 구조체는 익명 공용체를 하나 가진다. 익명 공용체는 option 변수와 익명 구조체를 멤버로 가지고, 익명 구조체가 boot와 interrupt를 멤버로 가지게 된다. 이렇게 되면 boot와 interrupt가 각각의 메모리 공간을 가지게 되고, 익명 구조체와 option은 공용체 멤버이기 때문에 공간을 공유하게 된다. 이 때, boot와 interrupt에 각각 1바이트 씩 저장되어 있기 때문에 boot, interrupt는 char 형으로, option은 이 둘을 모두 포함하고 있어야 하기 때문에 2바이트인 short 형으로 선언하면 된다.

 

struct DeviceOption{
    union{
        short option;
        struct{
            char boot;
            char interrupt;
        };
    };
};