Programming/C DataStructure

Struct - 구조체의 특징과 팁

양디 2015. 12. 26. 10:42

Struct : 구조체

(코드 상 struct이지만, 구조체는 structure일까 struct일까..?)


구조체의 특징

1. 다양한 타입의 변수 !

2. 연속된 메모리 공간 !



1번 특징은 배열과 정 반대이다. 하나의 구조체는 같거나, 서로 다른 타입의 변수들로 구성이 가능하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person {
    char name[20];
    int age;
    char sex;
};
 
void main() {
 
    struct Person yangd = { "Yangd"25'M' };
 
    printf("이름 : %s    \n나이 : %d    \n성별 : %c\n", yangd.name, yangd.age, yangd.sex);
 
}
cs

위의 코드는 구조체를 선언과 사용하는 방식들에 대한 내용이다.

아주 간단한 내용들로 구성되어 있기에 이해하는 데에 어려움이 없을 것이다.


그러나 실제로 사용할때마다 struct Person를 타이핑하기는 (귀찮다!)

따라서 typedef라는 타입을 새로 만드는 전처리 기능을 이용해서 구조체를 설정하곤 한다.


1
2
3
4
5
struct _Person {
    char name[20];
    int age;
    char sex;
}Person, *pPerson;
cs

위와 같이 사용하곤 한다. 이후 코드에서는 struct _Person이 아니라 Person으로 바로 구조체를 사용할 수 있다.

또한 뒤에 있는 *pPerson의 경우에는, 포인터를 간단하게 사용하기 위해 선언해주는 것이다.

이후에 포인터를 선언할때에 *Person을 선언하는 것이 아니라, pPerson을 선언하면 구조체 포인터를 선언할 수 있다.

처음에는 굳이 다른 이름으로 사용하는 것이 어색하고 불편할 수 있지만, 익숙해지면 가독성 등에서 더 편하다는 것을 느낄 수 있다.


2번 특징은 배열과 같다. 하나의 구조체는 연속된 메모리 공간을 차지한다.

그러나 주의해야할 점이 하나 있다. 

구조체는 구조체 내부의 가장 큰 변수 타입에 맞춰서 메모리를 할당한다.

(일반적으로 Default 패딩 사이즈는 CPU와 OS의 bit 수를 기준으로 한다. 32bit의 경우에는 4byte, 64bit의 경우에는 8byte...)

위의 예시를 재활용해보자.


1
2
3
4
5
6
7
8
9
10
11
12
struct Person {
    char name[20];
    int age;
    char sex;
};
 
void main() {
    
    printf("char[20]의 사이즈 + int 사이즈 + char 사이즈 = \n    %d + %d + %d",
        sizeof(char* 20sizeof(int), sizeof(char));
    printf("구조체 크기 : %d"sizeof(struct Person));
}
cs


위의 코드를 보면, char[20]의 사이즈는 20, int의 사이즈는 4, char의 사이즈는 1이므로 일반적으로 우리는

구조체의 크기가 25라고 생각할 것이다. 그렇지만 실제 결과는 그렇지 못하다.



위의 결과를 보면 25가 아닌 28이 나온 것을 알 수 있다.

그럼 왜 이런 결과가 나온 것일까 ?


위에서 말했다시피 가장 큰 사이즈에 맞춰서 메모리가 할당되기 때문이다.

코드에서 char의 사이즈는 1, int의 사이즈는 4이므로 가장 큰 타입은 int가 된다. (배열은 1개 변수의 사이즈를 기준으로 한다.)

따라서 구조체는 int 사이즈 4의 배수로 설정이 된다.


char[20]의 경우에는 이미 4의 배수인 20이기 때문에 20이 되었고,

int의 경우에는 기준이 되기 때문에 4의 크기를 그대로 갖고 있고,

char의 경우에는 사이즈가 1이기 때문에 기준보다 작으므로 4의 메모리를 사용한다.


따라서 20 + 4 + 4가 되어서 28이 되는 것이다.

메모리 공간은 다음과 같이 될 것이다.


20byte (char[20])

 4byte(int)

1byte(char) 

3byte(padding) 


위의 padding은 사용하지 않는 메모리로, 프로그래밍이나 네트워크 등 공부를 하다 보면 자주 마주치게 될 것이다.


이것을 int에서 long long(8byte)로 바꾼다면 몇이 될까 ?


이제는 어느정도 예측할 수 있을 것이다 !

8byte가 기준이 되므로, char[20]은 가장 가까운 8의 배수인 24byte가 할당될 것이고, long long에 8byte, char는 기준에 맞춰서 8byte가 되어서

총 40바이트가 될 것이다. 확인해보실까 ?




정확하다 ! 29가 아니라 40byte를 사용하고 있다는 것을 알 수 있다.


그렇다면 여기에서 질문이 생긴다. 위의 경우에는 11byte가 낭비되고 있는 상황인데, 이걸 고칠 수는 없을까?


정답은 역시 고칠 수 있다! 이다.


메모리누수를 최소화 하는 방법은 2가지가 있다. 



첫번째,

위의 마지막 예제처럼, 24 + 8 + 8으로 char[20], double, char가 선언되었다고 생각을 해보자.

그렇다면 메모리의 모습은 다음과 같을 것이다.


 20byte(char[20])

4byte

(padding)

8byte

(double)

1byte

(char)

7byte

(padding)


표의 모습처럼, 40바이트를 사용하고 있고, 11byte가 padding으로 낭비되고 있는 상황이다.

이렇게 시각적으로 보게 되면, 한가지 의문점이 들지 않는가 ?

char의 1바이트를 왼쪽의 4byte 공간에 집어 넣을 수 있지 않을까 ?

그렇다 정답이다!

아주 단순하게 변수의 순서를 바꾸는 것만으로도 데이터 누수를 최소화할 수 있다 !


 20byte

(char[20])

1byte
(char)

3byte

(padding) 

8byte

(double) 




1
2
3
4
5
struct Person {
    char name[20];
    double age;
    char sex;    
};
cs


1
2
3
4
5
struct Person {
    char name[20];
    char sex;
    double age;    
};
cs


위의 두 구조체는 같은 멤버를 갖고 있지만, 하나는 40byte, 하나는 32byte를 갖고 있다.

이런 식으로 메모리를 절약할 수 있다 


2번째,

이는 #pragma pack(n) 을 통해서 사용하는데, 


1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(1)
struct Person {
    char name[20];
    int age;
    char sex;
};
 
void main() {
    
    printf("char[20]의 사이즈 + int 사이즈 + char 사이즈 = \n    %d + %d + %d",
        sizeof(char* 20sizeof(int), sizeof(char));
    printf("구조체 크기 : %d"sizeof(struct Person));
}
cs


위처럼 pack하는 바이트 단위를 수정할 수 있다. 위의 경우에는 기준을 4byte -> 1byte로 수정하라는 것인데, 위의 코드를 실행해 보면



28이 아닌 25가 나오는 것을 볼 수 있다.


또한 패딩 사이즈를 기존으로 돌려줘야 하므로, 

일반적으로 pragma를 사용할때에는 push와 pop을 통한 stack에 패딩 사이즈를 저장하는 방식을 쓴다.


1
2
3
4
5
6
7
#pragma pack(push, 1)
struct Person {
    char name[20];
    char sex;
    double age;    
};
#pragma pack(pop)
cs


이렇게 하면 Person은 padding 사이즈가 1이고, 그 이후에 접근하는 것은 다시 기존의 padding 사이즈 기준을 따르게 된다.

pack이 필요할 때에는 보통 이 방식을 사용한다.


이런 방식으로 메모리 누수를 최소화 할 수 있지만, 메모리가 매우 제한된 상황이나 비트단위 연산을 하는 특별한 상황이 아니라면 

2번 방식은사용하지 않는 것을 추천한다. 




구조체를 padding을 넣고 메모리를 낭비하면서까지 사용하는데에는 그만한 이유가 있기 때문이다.

CPU가 레지스터의 값을 읽을 때에, CPU의 bit 수에 따라서 변수를 읽게 되는데 레지스터 블록의 경계를 구분하여

블록별로 CPU가 바로 바로 연산할 수 있도록 하기 위함이다.

(컴퓨터 구조를 공부하도록 하자.)


따라서, 메모리 누수를 줄이기 위해선 1번 방법을 사용하는 것을 추천한다.


<질문> char[10], int, char 로 이루어진 구조체의 기본 메모리 크기는 ?




댓글