Ch06. 복합데이터 - 05. 구조체

2025. 4. 2. 22:14Programming Language/C++

반응형

1. 구조체(struct)

서로 관련된 여러 개의 변수(데이터)를 하나로 묶는 사용자 정의 자료형으로 예를 들어, 이름, 나이, 키 등의 변수를 학생(Student) 이라는 단위로 묶어놓은 것이 구조체라고 할 수 있다.

클래스(class)와 매우 유사하지만, 기본 접근 지정자가 다르다.

 

2. 구조체의 정의

구조체를 정의하는 기본적인 방법은 아래와 같이

 

struct 구조체명 {
    타입 멤버변수명1;
    타입 멤버변수명2;
    . . . .
};

생성해주면 된다.

 

구조체 정의 생성의 예는 아래와 같고

struct Student {
    std::string name;
    int age;
    double height;
};

 

이 구조체는 Student 라는 구조체로 Student, 학생으로써 가질 수 있는 데이터로 이름, 나이, 키의 정보를 한번에 묶어주는 구조체가 된다.

3. 구조체 변수의 선언과 초기화

위와 같이 정의된 구조체를 사용하기 위해서 선언할 때에는 

구조체명 구조체변수명 ;

과 같이 사용 하며 실 구조체의 사용의 예시는 

Student s1;

과 같이 선언한다.

 

이렇게 선언한 구조체에 값을 초기화 하는 방법은 

s1 = { "Kim", 32, 170.2 };

이렇게 순서에 맞게 name, age, height에 대한 정보를 타입에 맞게 넣어주면 초기화가 된다.

또 원하는 멤버변수에만 값을 넣어주고자 한다면

// 구조체변수명.멤버변수명 = 값;
s1.name = 'Kim';
s1.age = 32;
s1.height = 170.1;

이렇게 값을 넣어줄 수 도 있다.

 

물론 선언과 동시에 값을 초기화 해줄 수 도 있다.

Student s1 = { "Kim", 32, 170.2 };

여기서 C++ 11 이후부터는 =를 생략해서 초기화도 가능하다(선언시점에 초기화하는 경우만).

Student s1{ "Kim", 32, 170.2 };

 

 

4. 구조체 멤버 접근

이제 구조체 멤버에 접근을 해보자.

구조체의 멤버에 접근하는 방법은 위에서 원하는 멤버에만 값을 넣을 때와 비슷하게 사용하면 된다.

구조체변수명.멤버변수명

실 사용의 예시를 보자면

이렇게 멤버 변수에 접근이 가능한 것을 볼 수 있다.

 

5. 구조체의 멤버 변수가 될 수 있는 구조체

구조체의 내부에 구조체를 넣어서 구조체를 만들어 줄 수도 있다.

struct SchoolInfo {
    int grade;
    int classNumber;
};

struct Student {
    std::string name;
    int age;
    float height;
    SchoolInfo schoolInfo;
};

 

내부에 구조체를 초기화 하는 방법은 그냥 기존방법과 동일하게 그 내부에 구조체를 초기화하면 된다.

Student s1 = { "Kim", 32, 170.2f ,{ 3, 10} };
Student s2{ "Park", 32, 187.3f ,{ 3, 9 } };

s1.schoolInfo = { 2, 11 };

 

6. 구조체의 복사

배열의 경우는 그냥 단순하게 

int arr1[3] = {1, 2, 3};
int arr2[3];

arr2 = arr1;

이렇게 복사하는게 불가능 했었는데 구조체의 경우는 

Student s1 = { "Kim", 32, 170.2f ,{ 3, 10 } };
Student s2;

s2 = s1;

이게 가능하다

std::cout << s2.name << std::endl;
std::cout << s2.age << std::endl;
std::cout << s2.height << std::endl;
std::cout << s2.schoolInfo.grade << std::endl;
std::cout << s2.schoolInfo.classNumber << std::endl;

의 값을 출력해보면

이렇게 값을 잘 출력하는 모습까지 볼 수 있다.

 

7. 구조체 배열의 선언

구조체를 배열로도 선언이 가능한데 선언하는 방법은

Student s1[3];

과 같이 선언하면 된다.

 

이 또한 선언과 함께 초기화도 가능하고

Student s1[3] = {
    {"Kim", 31, 160.5},
    {"Park", 29, 175.0},
    {"Lee", 33, 168.0}
};

 

배열의 요소를 선택해서 초기화하는 것도 가능하다

s1[0] = { "Kim", 31, 160.5 };
s1[1] = { "Park", 29, 175.0 };
s1[2] = { "Lee", 33, 168.0 };

 

물론 배열 하나에 있는 구조체의 멤버를 선택해서 각각 값을 넣는것 또한 가능하다.

s1[0].name = "Kang";
s1[0].age = 30;
s1[0].height = 189.2;

 

그리고 구조체 배열의 각 요소는 구조체이기 때문에 배열 요소 하나를 떼어서 구조체에 복사하는 것 또한 가능하다

Student s1[3] = {
    {"Kim", 31, 160.5},
    {"Park", 29, 175.0},
    {"Lee", 33, 168.0}
};

Student s2;

s2 = s1[0];

그리고 이렇게 가져온 s2와 s1[0]은 주소값이 아예 동일한 같은 값을 바라보는게 아니라 그냥 값만 가져온 것이기 때문에 

s2.name = "Kong";

std::cout << s2.name << std::endl;
std::cout << s1[0].name << std::endl;

이렇게 출력되는 값은 동일하지 않다.

c2와 c1[0]의 주소값을 확인해보면

std::cout << &s2 << std::endl;
std::cout << &s1[0] << std::endl;

이렇게 주소값이 다른 것을 볼 수 있다.

 

만약 s2가 아예 s1의 [0]번째 인덱스의 구조체의 주소값을 공유하는 변수로 복사하고 싶다면 추후 함수 파트에서 배울 Call-by-reference를 사용하는 레퍼런스를 사용해서 (*나중에 배울 내용이니 그냥 대충 읽고만 지나가도 좋다*)

Student& s2 = s1[0];
s2.name = "Kong";

std::cout << s2.name << std::endl;
std::cout << s1[0].name << std::endl;

이렇게 사용해주면

이렇게 동일한 값을 바라보게 할 수 있다.

 

주소값을 확인해보면

std::cout << &s2 << std::endl;
std::cout << &s1[0] << std::endl;

s2가 s1[0]의 주소값을 공유하는 것을 알 수 있다.

 

8. 구조체 크기

구조체의 크기는 sizeof 함수를 사용해서 확인이 가능하다.

struct Practice {
    float mem1;    
    float mem2;     
    char mem3[10];  
    short mem4;    
};

Practice p1;

std::cout << sizeof(p1) << std::endl;

이렇게 선언 되어 있는 경우에 예상에는 

    struct Practice {
        float mem1;     // 4
        float mem2;     // 4
        char mem3[10];  // 10
        short mem4;     // 2
    };

    Practice p1;

    std::cout << sizeof(p1) << std::endl;

각 크기에 맞게 20바이트의 크기를 가질 것으로 예상이 되고 

결과도 20바이트가 나온다.

 

근데 여기서 조금 특이한 점이 있는데 

struct Practice {
    float mem1;     // 4
    short mem4;     // 2
    float mem2;     // 4
    char mem3[10];  // 10
};

Practice p1;

std::cout << sizeof(p1) << std::endl;

이렇게 순서가 바뀌면 어쨋든 타입은 동일하기 때문에 20 바이트가 나올것으로 예상하지만 그 결과는

24바이트가 나온다.

 

이게 무슨 일일까?

 

이는 정렬(alignment)과 패딩(padding)이라는 개념에 의해서 발생되는 차이이다.

 

정렬(alignment)과 패딩(padding)

CPU는 특정 자료형(구조체에서 크기가 큰 자료형)을 그 자료형의 크기만큼 정렬시킨 주소에서 읽으면 데이터를 빠르게 읽을 수 있는데 그때 자료형마다 특정 주소 정렬 조건을 가지게 된다.

예를 들어 float은 보통 4바이트 정렬을 요구(주소가 0, 4, 8, ... 이런 위치에 있어야 함)하고 short은 2바이트 정렬을 요구하게 된다.

여기서 만약 정렬 조건에 맞지 않으면 중간에 빈 공간인 패딩을 넣어서 정렬 기준을 강제로 맞추려고 한다.

이 때 구조체를 예를 들어서 구조체에서 정렬의 조건이 되는 것은 그 구조체에서 가장 큰 크기의 데이터의 크기만큼의 정렬 조건을 가진다(int이 가장 큰 크기인 경우 4배수가 되어야만 하고, double이 가장 큰 크기인 경우는 8배수가 되어야만 한다.)

 

그래서 

struct Person {
    float height;      // 4 bytes
    short grade;       // 2 bytes
    float weight;      // 4 bytes
    char name[10];     // 10 bytes
};

이렇게 될것으로 예상되는 구조체가 실제로 메모리에 올라갈때 

float height  => 4바이트  / 0 ~ 3   공간을 할당 
short grade   => 2바이트  / 4 ~ 5   공간을 할당 => 4가 맞춰지지 않기에 추가로 패딩이 옆에 생성
(패딩)        => 2바이트  / 6 ~ 7   공간을 할당 => short 타입으로 인해 발생한 패딩
float weight  => 4바이트  / 8 ~ 11  공간을 할당
char name[10] => 10바이트 / 12 ~ 21 공간을 할당 => 4의 배수로 맞춰지지 않기에 추가로 패딩이 옆에 생성됨
(패딩)        => 2바이트  / 22 ~ 24 공간을 할당 => char[10] 타입으로 인해 발생한 패딩

이런 식으로 패딩이 생성된다.

 

패딩을 생성하는 기준은 구조체의 타입중 가장 큰 크기를 가진 타입을 기준으로 정렬이 발생한다.

그러면 char형 배열이 가장 큰거 아닌가 싶겠지만 배열의 경우는 그냥 메모리상에 연속된 덩어리일 뿐 컴파일러가 읽을때는 배열의 전체가 아니라 배열의 요소를 보고 정렬을 판단하기 때문에 배열이 가장 큰 크기가 되는 것은 아니다.

물론 배열의 타입이 가장 큰 타입으로 되어 있는 배열이라면 그 부분을 신경 써야하겠지만 char형 배열의 경우는 거의 신경 쓰지 않아도 된다.

 

근데 그러면 

struct Practice {
    float mem1;     // 4
    float mem2;     // 4
    char mem3[10];  // 10
    short mem4;     // 2
};

얘도 정렬의 대상이 되어야 하는데 왜 이건 20 바이트냐 하냐면 mem3[10]과 mem4(2)가 10 + 2 = 12바이트로 붙어 있으면서그 전체가 float의 정렬 기준인 4의 배수인 12바이트가 되기 때문이다.

 

물론 이런 부분까지 신경쓸 수준이 되면 초고수가 되어 있지 않을까 싶다...

정리하니까 정리 하지만 이런 하드웨어 수준의 정보를 아는게 정말 도움이 될까 싶다(물론 무조건 필요하고 중요하다고 생각 되지만 내 수준에서 이런 감각까지 깨우칠 수 있을까 의문이 들긴한다.)

 

아무튼 그냥 우리가 구조체를 만들더라도 우리가 원하는 크기의 구조체가 생성되는 것이 아니라 CPU에 의해서 이렇게 크기가 변경될수도 있다는 것을 알고만 있으면 될듯 싶다.

반응형