Struct(구조체)는 사용자 정의 자료형이라고 할 수 있다. 사용자가 정의한대로 관련 있는 여러 가지 데이터를 모아두는 자료형으로서 사용된다. 이번에 구조체에 대해 공부하면서 특이한 점을 알게 되어서 기록해두려고 작성하게 되었다.
구조체는 C에서부터 존재했던 내용으로, C에서 구조체를 정의하고 선언하는 방식과 C++에서 구조체를 정의하고 선언하는 방식이 조금 다르다고 한다. (물론 C의 방법도 C++에서 그대로 사용할 수 있다)
C에서 구조체를 정의하고 사용하는 방법
방법 1. 구조체를 정의하고, 구조체 타입 변수를 선언할 때 앞에 struct 키워드를 붙여야 한다. (C)
방법 2. 임시 구조체를 정의함과 동시에 typedef를 이용해 MyStruct2라는 이름의 자료형으로 선언해서 자료형처럼 사용할 수 있다. 방법 1과 다르게 변수를 선언할 때 struct 키워드가 필요 없다 (C)
// 방법 1.
struct MyStruct1
{
int m_iVal;
};
// 방법 2.
typedef struct
{
int m_iVal;
} MyStruct2;
int main()
{
// 방법 1.
struct MyStruct1 my1;
// 방법 2.
MyStruct2 my2;
return 0;
}
C++에서는 두 방법 모두 사용할 수 있고, 방법 1에서 struct 키워드를 붙이지 않고도 구조체 타입 변수를 선언할 수 있다.
이 때 구조체 안에 선언된 변수를 그 구조체의 member(멤버)라고 한다. 구조체는 다양한 변수를 멤버로 가질 수 있으며, 이들은 메모리 상에서 연속적으로 할당되며 코드에서 선언된 순서에 따라 배치된다. 따라서, 구조체 변수를 초기화할 때, 그 순서를 고려해서 초기화 할 수 있다.
struct MyStruct
{
int m_iVal;
float m_fVal;
double m_dVal;
};
int main()
{
// 구조체 변수의 초기화
MyStruct my = {100,3.14f,31.41592};
return 0;
}
위와 같이 초기화를 하는 경우, 멤버의 순서대로 m_iVal에 100, m_fVal에 3.14f, m_dVal에 31.41592가 할당된다. {} 안에 아무것도 작성하지 않으면 자동으로 0을 값으로 할당해준다. 예를 들어 { }로 초기화 했다면 모든 멤버에 0이 할당되고, {100, }과 같이 초기화 하면 작성하지 않은 나머지 부분에 해당하는 멤버들이 0으로 값이 할당된다.
구조체에 정의된 멤버에 접근할 수 있다. 기본적으로 구조체 변수 뒤에 .을 쓴 뒤 멤버의 이름을 써서 접근할 수 있다. 아래의 my.m_iVal 같은 경우로, my 변수안의 m_iVal에 접근하는 것이다. 또한, 구조체는 자료형과 같이 포인터를 이용해서 접근할 수도 있다. 구조체의 주소를 포인터 변수에 할당하면 구조체의 시작 주소를 알게 되고, *를 이용해 역참조 한 뒤 .을 이용해 멤버에도 접근할 수 있다 (방법 1). 이 때 방법 1처럼 작성하는 것이 번거롭다면 방법 2처럼 ->을 통해 접근할 수도 있다. 두 표현은 동치이다.
struct MyStruct
{
int m_iVal;
float m_fVal;
double m_dVal;
};
int main()
{
MyStruct my = {100,3.14f,31.41592};
my.m_iVal = 200;
MyStruct* pMy = &my;
(*my).m_iVal = 300; // 방법 1
my->m_iVal = 400; // 방법 2
return 0;
}
구조체 타입은 멤버들의 순서에 따라 메모리에 연속적으로 할당된 것으로 인식한다. 따라서 구조체 타입의 크기는 멤버들의 크기에 따라 결정된다. 이 때 직관적으로 생각하면 멤버들의 크기를 다 더하면 구조체의 크기가 될 것으로 예상되지만 실제로는 그렇지 않다. 구조체의 크기는 가장 크기가 큰 멤버의 크기를 한 단위로 하여 구성된다. 즉, 위의 경우 int, float, double 형의 멤버를 순서대로 가지므로 4/4/8 byte의 메모리를 가지게 되는데, 실제로는 가장 큰 8byte 단위로 메모리가 구성되어 [4/4][8] byte와 같이 구성된다. 다른 예시로 char, int, long long을 멤버로 가지는 경우 [1/4/나머지 3][8] byte로 메모리가 구성되는 것이다. 이를 Byte Padding이라고 한다.
Byte Padding이 일어나는 이유는 CPU가 명령어를 처리하는 단위와 관련이 있다. CPU가 한 번에 명령어를 처리하는 단위를 word라고 한다. x86 CPU는 32bit (4byte), x64 CPU는 64bit (8byte)가 1word에 해당한다. 64비트 운영체제라 가정했을 때, byte padding이 적용되지 않은 경우 아래와 같이 메모리가 할당된다. 이 때 long long 타입의 멤버에 접근하고 싶으면 CPU가 총 2회 메모리에 접근해야 된다. 반면 위 그림처럼 padding이 적용된 경우 1회만으로 long long 타입의 멤버를 읽을 수 있게 된다.
하지만 위와 같이 사용자의 환경에 따라 알아서 byte padding이 일어나는 경우 네트워크 통신 과정에서 패킷 사이즈가 달라 값을 제대로 주고 받을 수 없는 문제가 발생할 수 있다고 한다. 이러한 경우 패딩 사이즈를 통일해줘야 하는데, C++에서 직접 padding 단위를 지정할 수 있다.
#pragma pack(push,1)
struct MyStruct1
{
char m_cVal;
int m_iVal;
long long m_lVal;
};
#pragma pack (pop)
struct MyStruct2
{
char m_cVal;
int m_iVal;
long long m_lVal;
};
int main()
{
int a = sizeof(MyStruct1); // 13byte
int b = sizeof(MyStruct2); // 16byte
return 0;
}
#pragma pack(push,1)을 하면 앞으로 정의하는 구조체들에 대해서 padding 사이즈를 1로 지정한다는 의미이다. 따라서 우리가 처음에 직관적으로 생각하던 것처럼 모든 멤버의 사이즈를 더한 것이 곧 구조체의 사이즈가 된다. #pragma pack(pop)을 하면 우리가 설정했던 1사이즈의 패딩을 취소하고 원래대로 돌아가는 것이다. 이렇게 특정 구조체만 padding 사이즈를 지정해 줄 것이 아니라면 처음부터 #pragma(1)만 작성해도 된다.
네트워크 통신에 대한 내용은 잘 몰라서 더 자세히 작성할 수 없지만 나중에 공부하게 된다면 더 자세하게 알아보고 싶다.
내용 참고 : https://www.youtube.com/watch?v=aROgtACPjjg&ab_channel=NesoAcademy (Byte Padding)
[C++] 함수 객체 (Functor) (0) | 2024.08.08 |
---|---|
[C++] 함수 포인터 (0) | 2024.03.19 |
[C++] 포인터 (0) | 2024.03.19 |
[C++] 변수의 종류와 메모리 영역 (0) | 2024.03.01 |
[C++] 자료형 (Data Type) (0) | 2024.02.29 |