C, struct, typedef, union


구조체


구조체는 새로운 자료형입니다.

우리가 지금까지 살펴보면 자료형을 보면

int형 변수에는 정수 하나만 저장이 가능했고,

char형 변수에는 문자 하나만 저장이 가능했습니다.

물론 배열이 있긴 한데, 배열은 같은 종류의 데이터만 다룰 수 있습니다.


그럼 만약에 한 변수에 char형 데이터와 int형 데이터를 모두 저장하고 싶다면?

이럴 때 쓰는게 구조체 입니다.

구조체를 선언하고 정의한 예제 코드를 살펴보겠습니다.

#include <stdio.h>

// 구조체 선언
struct USERDATA
{
    int age;
    char name[30];
    char phone[30];
};

void main(void)
{
    // USERDATA라는 구조체 변수 user 선언 및 정의
    struct USERDATA user = {0, "", ""};
    
    // 구조체 멤버에 접근 및 값 대입
    user.age = 30;
    strcpy(user.name, "onsil");
    strcpy(user.phone, "010-1234-2345");
    
    // 구조체 멤버에 접근 및 출력
    printf("%d살, %s, %s\n\n", user.age, user.name, user.phone);
}

/*
30살, onsil, 010-1234-2345
*/

구조체라는 것은 개발자 마음대로 자료형을 새로 만드는 것입니다.

여기서는 USERDATA라는 구조체를 만들었고,

그 구조체의 멤버는 3개가 있고, 각각은

int형 변수인 age와 char형 배열인 name, phone이 있습니다.


main()함수에는 구조체 변수 선언과

각 멤버에 접근하는 방법이 설명되어 있습니다.


typedef를 이용한 형 재선언


그런데 구조체 변수를 선언할 때 보면 struct라고 명시를 해줘야합니다.

이런 불편함을 줄여주는 방안으로 typedef를 이용한 형 재선언 방법이 있습니다.

위와 똑같은 예제를 typedef를 이용하여 선언해보겠습니다.

#include <stdio.h>

// 구조체 선언
typedef struct USERDATA
{
    int age;
    char name[30];
    char phone[30];
}UD;

void main(void)
{
    // USERDATA라는 구조체 변수 user 선언 및 정의
    UD user = {0, "", ""};
    
    // 구조체 멤버에 접근 및 값 대입
    user.age = 30;
    strcpy(user.name, "onsil");
    strcpy(user.phone, "010-1234-2345");
    
    // 구조체 멤버에 접근 및 출력
    printf("%d살, %s, %s\n\n", user.age, user.name, user.phone);
}

/*
 30살, onsil, 010-1234-2345
 */

이번에 구조체 선언 부분을 보면

typedef라는 예약어를 사용하였고,

스코프 끝에 UD란 이름으로 재정의를 해주었습니다.

이제 이 USERDATA라는 구조체는 UD라는 이름으로 사용 가능하는 뜻입니다.

그래서 main() 함수에서 구조체 변수 user를 선언한 부분을 보면

UD 자료형으로 선언된 것을 볼 수 있습니다.


구조체 배열


구조체는 int나 long과 같은 자료형과 똑같이 사용할 수 있습니다.

즉, 구조체의 배열도 존재합니다. 사용방법도 다른 자료형과 동일합니다.

// 구조체 배열

#include <stdio.h>

typedef struct USERDATA
{
    int age;
    char name[30];
    char phone[30];
} USERDATA;

void main(void)
{
    USERDATA user[4] = {
        {5, "teemo", "1234"},
        {15, "jinx", "2345"},
        {30, "jax", "3456"},
        {10, "twitch", "4567"}
    };
    
    printf("sizeof(user): %d\n", sizeof(user));
    printf("sizeof(USERDATA): %d\n\n", sizeof(USERDATA));
    
    for(int i=0; i<4; i++){
        printf("%d번째 유저 이름: %s\t 나이: %d\t 폰번호: %s\n", i+1, user[i].name, user[i].age, user[i].phone);
    }
    
    putchar('\n');
}

/*
 sizeof(user): 256
 sizeof(USERDATA): 64
 
 1번째 유저 이름: teemo     나이: 5     폰번호: 1234
 2번째 유저 이름: jinx     나이: 15     폰번호: 2345
 3번째 유저 이름: jax     나이: 30     폰번호: 3456
 4번째 유저 이름: twitch     나이: 10     폰번호: 4567
*/

sizeof() 함수를 이용하여 알아낸

구조체 변수의 크기와 구조체 자체의 크기에 대해선 뒤에서 살펴보겠습니다.


구조체 동적 할당


구조체를 일반 자료형과 동일하게 처리할 수 있다는 말은

구조체 변수도 동적할당이 가능하다는 뜻입니다.

// 구조체 동적 할당

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

typedef struct USERDATA
{
    int age;
    char name[30];
    char phone[30];
} USERDATA;

int main(void)
{
    // 구조체 포인터 변수 선언 및 동적 할당
    USERDATA *pUser = NULL;
    pUser = (USERDATA*)malloc(sizeof(USERDATA));
    
    // 구조체 포인터 변수를 통해 각 멤버에 접근
    // (*pUser).age = 30; 과 같음
    pUser->age = 30;
    strcpy(pUser->name, "onsil");
    strcpy(pUser->phone, "1234");
    
    printf("%d살\t %s\t %s\n\n", pUser->age, pUser->name, pUser->phone);
    
    // 동적할당된 메모리 해제
    free(pUser);
    
    return 0;
}

/*
30살     onsil     1234
*/

다만 주의해야할 점은 포인터 변수를 통해 멤버에 접근할 때는

-> 연산자를 사용해야 한다는 것입니다.


반환형이나 매개변수로의 구조체


구조체는 다른 자료형처럼 다룰 수 있으므로

이 역시 반환형이나 매개변수로도 사용할 수 있습니다.

// 구조체도 반환형이나 매개변수가 될 수 있다.

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

typedef struct USERDATA
{
    int age;
    char name[30];
    char phone[30];
} USERDATA;

USERDATA MakeUSERDATA(void)
{
    USERDATA user = {0};
    return user;
}

void GetUSERDATA(USERDATA *user, int age, char *name, char *phone)
{
    user->age = age;
    strcpy(user->name, name);
    strcpy(user->phone, phone);
}

void PrintUSERDATA(USERDATA *user)
{
    printf("%d살\t %s\t %s\n\n", user->age, user->name, user->phone);
}


int main(void)
{
    // 구조체 변수 만들기
    USERDATA user = MakeUSERDATA();
    
    // 구조체 데이터 넣기
    GetUSERDATA(&user, 30, "onsil", "1234");
    
    // 구조체 데이터 출력
    PrintUSERDATA(&user);
    
    putchar('\n');
    return 0;
}

/*
30살     onsil     1234
*/


구조체를 멤버로 가지는 구조체


구조체는 멤버로 int, char같은 자료형 뿐 아니라 구조체도 가질 수 있습니다.

// 구조체를 멤버로 가지는 구조체

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

typedef struct BODY
{
    int height;
    int weight;
} BODY;

typedef struct USERDATA
{
    char name[30];
    int age;
    // 구조체 안의 구조체
    BODY body;
} USERDATA;

int main(void)
{
    USERDATA user = {"onsil", 30, {170, 60}};
    printf("이름: %s\n나이: %d\n키: %d\n몸무게: %d\n\n", user.name, user.age, user.body.height, user.body.weight);
    
    return 0;
}

/*
 이름: onsil
 나이: 30
 키: 170
 몸무게: 60
*/

구조체 멤버의 멤버에 접근하려면 .연산자를 두번 써야함을 확인합니다.


위에서는 구조체 안에 멤버로 다른 구조체 변수가 포함되어 있었습니다.

그런데 구조체 안에 멤버로 자기 자신의 변수를 둘 수도 있습니다.

이를 자기 참조 구조체라 부릅니다.

// LinkedList 구현
#include <stdio.h>

typedef struct USERDATA
{
    char name[30];
    int age;
    struct USERDATA *nextUser;
} USERDATA;

int main(void)
{
    USERDATA user[3] = {
        {"teemo", 10, NULL}, {"jinx", 20, NULL}, {"jax", 30, NULL}
    };
    
    USERDATA *pUser = &user[0];
    
    for(int i=0; i<2; i++){
        user[i].nextUser = &user[i+1];
    }
    
    while(pUser != NULL)
    {
        printf("이름: %s, 나이: %d\n", pUser->name, pUser->age);
        pUser = pUser->nextUser;
    }
    
    putchar('\n');
    return 0;
}

/*
 이름: teemo, 나이: 10
 이름: jinx, 나이: 20
 이름: jax, 나이: 30
*/

USERDATA 구조체 선언부를 보면 멤버로

자기자신의 포인트변수가 있는 걸 볼 수 있습니다.

코드상 아직 typedef로 재선언 되기 전이기 때문에

선언할 때 struct를 붙여줘야합니다.


위 코드와 같은 자료구조형을 LinkedList라고 합니다.

여기선 그 중에서 제일 간단한 Single LinkedList 형태입니다.

c_struct_linkedList


구조체의 크기


위에서 잠깐 결과만 봤던 구조체의 크기이야기 입니다.

구조체의 크기에 대해 이해하기 위해 다양한 구조체를 만들어보았습니다.

// 구조체의 크기

#include <stdio.h>

typedef struct S1
{
    char a;
}S1;

typedef struct S2
{
    int a;
}S2;

typedef struct S3
{
    double a;
}S3;

typedef struct S4
{
    char name[20];
}S4;

typedef struct S5
{
    char a;
    int b;
}S5;

typedef struct S6
{
    char a;
    int b;
    double c;
}S6;

typedef struct S7
{
    char a;
    int b;
    double c;
    char name[20];
}S7;

typedef struct S8
{
    char a;
    int b;
    char c;
}S8;

typedef struct S9
{
    char a;
    int b;
    int c;
}S9;

typedef struct S10
{
    char a;
    char str1[30];
    int b;
    char str2[29];
    char c;
}S10;

typedef struct S11
{
    char a;
    double d;
    char str1[30];
    int b;
    int cc;
    char str2[29];
    char c;
}S11;

typedef struct S12
{
    int a;
    int b;
    double c;
}S12;

typedef struct S13
{
    int a;
    char b;
    double c;
}S13;

typedef struct S14
{
    char a;
    short b;
    int c;
    double d;
}S14;

void main(void)
{
    printf("sizeof(S1): %d\n", sizeof(S1));
    printf("sizeof(S2): %d\n", sizeof(S2));
    printf("sizeof(S3): %d\n", sizeof(S3));
    printf("sizeof(S4): %d\n", sizeof(S4));
    printf("sizeof(S5): %d\n", sizeof(S5));
    printf("sizeof(S6): %d\n", sizeof(S6));
    printf("sizeof(S7): %d\n", sizeof(S7));
    printf("sizeof(S8): %d\n", sizeof(S8));
    printf("sizeof(S9): %d\n", sizeof(S9));
    printf("sizeof(S10): %d\n", sizeof(S10));
    printf("sizeof(S11): %d\n", sizeof(S11));
    printf("sizeof(S12): %d\n", sizeof(S12));
    printf("sizeof(S13): %d\n", sizeof(S13));
    printf("sizeof(S14): %d\n\n", sizeof(S14));
    
}

/*
 sizeof(S1): 1
 sizeof(S2): 4
 sizeof(S3): 8
 sizeof(S4): 20
 sizeof(S5): 8
 sizeof(S6): 16
 sizeof(S7): 40
 sizeof(S8): 12
 sizeof(S9): 12
 sizeof(S10): 68
 sizeof(S11): 88
 sizeof(S12): 16
 sizeof(S13): 16
 sizeof(S14): 16
*/

일단 구조체에서 멤버의 데이터가

메모리에 저장되는 규칙을 살펴보겠습니다.

  1. 메모리 저장단위는 가장 큰 자료형의 크기를 기준으로 한다.
  2. 배열의 경우, 배열의 크기가 아니라 요소의 자료형의 크기를 본다.
  3. 기준 크기를 절반으로 나눠서 멤버를 저장할 수 있으면 그렇게 저장한다.


S1은 멤버가 char형 하나뿐이고,

가장 큰 자료형의 크기는 1바이트 입니다.

즉, 1바이트 기준으로 멤버를 저장하고,

1바이트에 a를 저장하여, 구조체의 크기는 1바이트가 나옵니다.


S2, S3도 S1과 같은 원리로 크기가 나옵니다.


S4의 경우 가장 큰 자료형의 크기는 char로 1바이트 입니다.

즉, 1바이트 기준으로 길이 20의 문자열을 저장하므로

구조체의 크기는 20이 됩니다.


S5에서 가장 큰 자료형의 크기는 int로 4바이트 입니다.

즉, 4바이트 기준으로 char a를 저장하고, int b도 저장하므로

각각 4바이트씩, 총 8바이트가 구조체의 크기가 됩니다.

c_struct_S5


S6에서 가장 큰 자료형의 크기는 double로 8바이트 입니다.

즉, 8바이트 기준으로 데이터를 저장하게 되는데,

처음에 저장되는 자료형은 char(1바이트)와 int(4바이트)입니다.

8바이트를 반으로 나누면 4바이트/4바이트가 되고 각각의 공간에

char와 int를 저장할 수 있으므로,

char와 int를 저장하는데 8바이트, double를 저장하는데 8바이트해서

구조체의 크기는 총 16바이트가 됩니다.

c_struct_S6


S7에서 가장 큰 자료형의 크기는 double로 8바이트 입니다.

그럼 앞의 char와 int는 4바이트씩 차지하며 8바이트에 저장됩니다.

그리고 double은 8바이트에 저장이 됩니다.

name같은 경우 길이가 20입니다. 하지만 기본 저장 단위가 8바이트 이므로

24(8x3)바이트에 저장이 됩니다.

즉, 구조체의 크기는 8 + 8 + 24 == 40 바이트입니다.


S8에서 가장 큰 자료형은 int로 4바이트 입니다.

먼저 char a가 4바이트에 저장이 되고,

그 다음 int b가 4바이트에, 마지막 char c가 4바이트에 저장되서

구조체의 크기는 12바이트가 됩니다.

c_structS8


S9는 건너뛰고, S10을 살펴보겠습니다.

여기서 가장 큰 자료형은 int로 4바이트입니다.

그런데 처음에 나오는 자료형이 char와 char[30]입니다.

이 경우, 같은 자료형으로 취급하고 총 char[31]바이트 이기때문에

4바이트 단위로 담기 위해 32(4x8)바이트에 담깁니다.

그리고 int b는 4바이트에 담기고,

뒤에 있는 str2, c도 같은 char로 인식하면 char[30]이므로

32바이트에 담깁니다.

즉, 구조체의 크기는 32 + 4 + 32 == 68 바이트입니다.


S11에서 가장 큰 자료형은 double로 8바이트 입니다.

먼저 char a가 8바이트에 저장이 되고, double d가 8바이트에 저장됩니다.

그리고 나오는 char[30]은 8바이트 단위로 저장하기 위해 32바이트에 저장됩니다.

다음에 int b와 int cc가 나오는데 저장단위인 8바이트를 반으로 나누면

4바이트/4바이트 이고 이는 각각 b와 cc를 담을 수 있으므로

int b와 int cc는 8바이트에 담깁니다.

그리고 뒤에 나오는 str2와 c는 합쳐서 char[30]이므로 32바이트에 저장됩니다.

즉, 구조체의 크기는 8 + 8 + 32 + 8 + 32 == 88 바이트입니다.


마지막으로 S14를 살펴보겠습니다.

여기서 가장 큰 자료형은 double로 8바이트입니다.

8바이트를 반으로 나누면 4바이트/4바이트 이고

앞의 4바이트를 또 반으로 나누면 2바이트/2바이트입니다.

이 두개의 2바이트 공간은 char a와 short b를 저장할 수 있으므로,

그대로 저장됩니다. 그리고 나머지 4바이트에 그 다음 멤버인 int c를 저장합니다.

그리고 다음에 double d를 8바이트에 저장합니다.

즉, 구조체의 크기는 8 + 8 == 16 입니다.

c_struct_S14


이렇게 메모리를 사용할 경우, 빈 메모리 공간이 생기긴 하지만

보통은 이 상태로 사용합니다. 이때 남는 공간을 패딩(padding)이라 부릅니다.

이 상태로 사용하는 이유는 메모리에서 CPU 레지스터로 한번에 읽어보는 작업을 fetch라 하는데

이 fetch하는 데이터의 크기때문입니다.

메모리는 저장공간이니 연산을 위해서는 CPU에서 메모리에 있는 값을 읽어야합니다.

그런데 이때 1바이트씩 읽으면 시간이 오래 걸리겠죠.

그래서 32비트 환경에서는 4바이트, 64비트 환경에서는 8바이트씩 읽습니다.

S8을 예로 들어보겠습니다.

정상대로 패딩이 적용되면 다음과 같겠지만

c_struct_s8_1

패딩이 적용안되면 다음과 같을 것입니다.

c_struct_s8_2

패딩이 안된 상태에서 4바이트씩 읽는 다면

처음에 읽을 때 b자료형이 짤려서 읽히게 됩니다.

b자료형을 읽기위해서 두번째 읽을 때까지 기다렸다가

또 따로 연산처리를 해야 b를 인식할테니 속도가 느릴 것입니다.

이것이 구조체에 패딩이 적용된 이유입니다.


#pragma pack()


하지만 상황에 따라서 패딩없이 꾹꾹 붙여서

만들어야 하는 경우도 있습니다.

(저도 잘 모르지만, 그렇다 합니다)

그런 경우에는 #pragma pack()이라는 전처리기를 사용하면 됩니다.

#include <stdio.h>

#pragma pack(push, 1)
typedef struct USERDATA
{
    char ch;
    int age;
    char name[30];
} USERDATA;

typedef struct MYDATA
{
    char ch;
    int age;
    double du;
} MYDATA;

#pragma pack(pop)

typedef struct MYDATA2
{
    char ch;
    int age;
    double du;
} MYDATA2;

void main(void)
{
    printf("sizeof(USERDATA): %d\n", sizeof(USERDATA));
    printf("sizeof(MYDATA): %d\n", sizeof(MYDATA));
    printf("sizeof(MYDATA2): %d\n\n", sizeof(MYDATA2));
}

/*
 sizeof(USERDATA): 35
 sizeof(MYDATA): 13
 sizeof(MYDATA2): 16
*/

#pragma pack(push, 1)은 데이터 저장 단위를 무조건 1바이트로 하겠다는 것입니다.

그리고 #pragma pack(pop)이 여기까지만 적용하겠다는 의미입니다.

그래서 그 사이에 선언된 USERDATA와 MYDATA는 크기가 딱 멤버들의 크기만큼 나온걸 알 수 있습니다.

USERDATA는 1(char) + 4(int) + char[30](30) == 35

MYDATA는 1(char) + 4(int) + 8(double) == 13

하지만 pragma의 적용을 받지 못한 MYDATA2는 위에서 살펴봤던 것처럼 16이 나옵니다.


비트필드(bit field)


비트필드는 구조체 멤버가 바이트 단위가 아닌

비트 단위 데이터를 다루는 멤버로 선언되는 구조체 입니다.

지금까지 살펴본 자료형 중에 가장 작은 단위는 char로 1바이트 였습니다.

그래서 비트 단위를 다루는게 생소하겠지만

예제를 살펴보면 알 수 있습니다.

// 비트필드
#include <stdio.h>

typedef struct BITFIELD
{
    unsigned char a : 1;
    unsigned char b : 2;
    unsigned char c : 3;
    unsigned char d : 2;
} BF;

int main(void)
{
    BF bf = {1, 3, 7, 5};
    
    printf("bf.a: %d\n", bf.a);
    printf("bf.b: %d\n", bf.b);
    printf("bf.c: %d\n", bf.c);
    printf("bf.d: %d\n\n", bf.d);
    
    printf("bf: %d\n", bf);
    printf("*((unsigned char *)&bf): %d\n", *((unsigned char*)&bf));
    printf("sizeof(bf): %d\n\n", sizeof(bf));
    
    return 0;
}

/*
 bf.a: 1
 bf.b: 3
 bf.c: 7
 bf.d: 1
 
 bf: 127
 *((unsigned char *)&bf): 127
 sizeof(bf): 1
*/

여기서 bf의 메모리 구조를 보면 다음과 같습니다.

c_bitfield1

c_bitfield2

비트는 왼쪽에서부터 채워집니다.

차지하고 있는 비트 개수가 BITFIELD선언부와 일치합니다.


또 하나 살펴볼 것은 bf의 d를 5로 정의했는데

출력할때는 1로 나왔다는 것입니다.

그 이유는 d가 할당받은 비트는 2비트인데,

5는 이진수로 101 으로 세자리입니다.

그래서 앞의 1은 짤리고 01만 인식되어 1로 저장됩니다.


bf자체를 출력하면 01111111인 127이 나옵니다.

그리고 BF 구조체의 사이트는 결국 8비트를 사용했으므로 1바이트 입니다.


여기까지 해보고 문득 이런 궁금증이 들었습니다.

‘비트필드에선 변수명 자체도 127처럼 의미가 있는거 같은데 다른 구조체는 어떨까?’

그래서 시도해본 결과…

#include <stdio.h>

typedef struct ASDF
{
	char a;
	short c;
}ASDF;

void main(void)
{
	printf("sizeof(ASDF): %d\n", sizeof(ASDF));

	printf("@: %d\n", '@');

	ASDF as = { '@', 2 };
	printf("as: %d\n\n", as);

}

c_struct_name_check1

c_struct_name_check2

구조체 변수 as를 출력하니 183360입니다.

이 숫자가 의미하는 바는 결국 메모리에 저장된 비트입니다.

’@’는 아스키코드로 64입니다.

그리고 메모리창을 보면 앞의 2바이트인 40 cc 중 40이 64를 의미합니다.

숫자 하나가 4비트이므로 40은 0100 0000 으로 64입니다.

그리고 뒤의 cc는 패딩된 메모리입니다. 의미없는 쓰레기값이죠.

그래서 40 cc 02 00을 비트로 바꿔서 읽으면 되는데,

스택 메모리는 뒤에서부터 읽어야하니 00 02 cc 40 으로 읽으면 됩니다.

이를 비트로 바꾸어 보면

00000000 00000010 11001100 01000000

이고 이 수를 10진수로 바꿔보면 183360입니다.

이 수를 이루는 비트 중 cc는 의미없는 쓰레기값이니

결국 183360도 의미없는 값임을 알 수 있습니다…

(괜히 책에서 설명 안해준게 아니야…)




공용체


공용체는 구조체와 비슷한 듯하면서도 다릅니다.

공용체는 어떤 한 데이터를 다양한 방법으로 읽을 수 있도록

읽는 방법을 부여하는 문법입니다.

예를 들어 4바이트의 메모리는

int 하나로 해석할 수도 있지만,

short 2개로 해석할 수도 있고, char 4개로 해석할 수도 있습니다.

// 공용체
#include <stdio.h>

typedef union _IP_ADDR
{
    int address; // 해석1, 4바이트
    short shortData[2]; // 해석2, 2바이트 2개
    unsigned char addr[4]; // 해석3, 1바이트 4개
} IP_ADDR;

void main(void)
{
	// 공용체 선언 및 정의
    IP_ADDR data = { 0 };
    // 공용체의 멤버 중 하나에만 값 대입
    data.address = 0x41424344;
    
    // 대입한 하나의 수를 다양하게 해석함
    printf("%x, %x, %x, %x\n", data.addr[0], data.addr[1], data.addr[2], data.addr[3]);
    printf("%d, %d, %d, %d\n", data.addr[0], data.addr[1], data.addr[2], data.addr[3]);
    printf("%c%c%c%c\n\n", data.addr[0], data.addr[1], data.addr[2], data.addr[3]);
    
    printf("%x, %d\n", data.shortData[0], data.shortData[0]);
    printf("%x, %d\n\n", data.shortData[1], data.shortData[1]);
    
    printf("%x\n", data.address);
    printf("%d\n\n", data.address);
}

/*
 44, 43, 42, 41
 68, 67, 66, 65
 DCBA
 
 4344, 17220
 4142, 16706
 
 41424344
 1094861636
*/