<CPP>/BASIC

16. 사용자 정의 자료형

CodeGrimie 2021. 1. 19. 15:04

C언어는 기본적으로 최소한의 자료형만을 지원한다.

이는 C언어의 특징 중 하나인 가벼움을 유지하기 위한 고육지책이다.

 

우리가 살아가는 이 세상에는 수많은 데이터가 있고 그 데이터마다 또 구조가 다 다르다.

이런 경우의 수를 모두 변수형으로 만들어두면 C언어의 크기가 한계 없이 커질 것이 분명했다.

 

대신 C에서는 기본 자료형을 확장해서 사용자 정의 자료형을 만들 수 있게 하였다.

CPP 역시 C 언어를 계승하면서 이 기능을 가져왔다.

(약간의 수정은 되었다.)

구조체(Structure)

구조체(Structure)라는 단어만으로 어떤 일을 하는 건지 쉽게 한 번에 이해되지 않을 것이다.

우리나라의 말로 번역되면서 조금(많이) 압축 된 느낌이 있다.

 

영어가 가지는 뜻을 생각해보면 쉽다.

한정된 공간에 정해진 물체(값)을 채워 넣은 것 -> 설계, 구조로 주로 해석됨.

이제는 어른들의 장난감으로 더 많이 팔리는 레고(Lego) 역시 구조체 완구의 일종이다.

설계도대로 만들기도 하지만 제작자 마음대로 제작할 수도 있다.

 

그렇기 때문에 같은 재료를 줘도 결과가 다 다를 가능성이 높다.

 

▼ 레고가 구조체 완구의 일종이다.

구조체의 기본 모습

그렇기 때문에 구조체의 기본적인 모습도 레고를 생각하면 이해하기 편하다.

 

▼ 구조체의 형태

struct Point2D
{
    // 구조체 멤버 변수
    float _x;
    float _y;
};

▼ 코드의 도식화

float(4 Byte) 두 개를 가지는 (4 + 4)Byte의 크기를 가지는 Point2D는 새로운 자료형이 만들어졌다.

이제 이 새로운 자료형을 어떻게 사용하는지 알아보자.

구조체 사용

▼ 구조체 값을 초기화한다.

#include <cstdio>

struct Point2D
{
    // 구조체 멤버 변수
    float _x;
    float _y;
};

int main()
{
    struct Point2D  p1;

    p1._x = 0.0f;
    p1._y = 0.0f;
    
    printf("P1_X : %d\n", p1._x);
    printf("P1_Y : %d\n", p1._y);
    
    return (0);
}

단순히 p1이라는 Point 2D0.0f0.0f를 넣어 초기화해주는 아주 간단한 코드다.

여기서 집중해서 봐야 하는 부분은 오히려 선언 부분이다.

 

struct Point2D p1;에서 구조체(Struct)를 사용한 사용자 정의 자료형이라고 명시해주고 있다.

컴파일러는 이걸 보고 Point 2D라는 자료형이 C에 없더라도 사용자 정의 자료형이라고 생각하고 넘어간다.

 

우리의 예제가 같은 변수형을 가지는 구조체라서 더 그렇지만 왠지 모르게 배열이랑 비슷하다고 생각되지 않는가?

아래의 배열로 같은 경우를 만들었을 때 코드를 살펴보자.

 

▼ 어딘가 비슷한 냄새가 난다

#include <cstdio>

float Point2D[2]
{
    0.0f,
    0.0f
};

int main()
{
    Point2D[0] = 0.0f;
    Point2D[1] = 0.0f;
    
    printf("Point2D[0] : %d\n", Point2D[0]);
    printf("Point2D[1] : %d\n", Point2D[1]);
    
    return (0);
}

구조체는 배열과 비슷하다

구조체는 결과적으로 보면 다양한 값이 들어가는 배열이라 생각하면 편하다.

구조체 역시 내부 변숫값들의 주소가 연속적으로 할당된다.

 

그리고 몇몇 배열 문법을 사용할 수도 있을 뿐 아니라마찬가지로 첫 번째 값의 주소가 시작 지점이다.

 

▼ 배열과 같이 초기화할 수도 있다.

struct Point2D
{
    // 구조체 멤버 변수
    float _x;
    float _y;
};

int main()
{
    struct Point2D  p1 = {0.0f, 0.0f};
    
    return (0);
}

배열과 비교하면 구조체가 가지는 장점이 훨씬 두드러진다.

 

구조체는 배열과 달리 하나의 변수형으로 받아들이기 때문에 구조체의 배열을 만들어서 관리 할 수 있다.

 

▼ 구조체의 배열

struct Point2D
{
    // 구조체 멤버 변수
    float _x;
    float _y;
};

int main()
{
    struct P2DArr[3];
    
    struct MyPoint2D = {0.0f, 0.0f};
    struct YourPoint2D = {1.0f, 1.0f};
    struct TheirPoint2D = {2.0f, 2.0f};
    
    P2DArr[0] = MyPoint2D;
    P2DArr[1] = YourPoint2D;
    P2DArr[2] = TheirPoint2D;
    
    return (0);
}

그리고 선언을 할 때 몇 개의 인자가 들어가는지 명시하지 않아도 된다.

 

그럼 모든 경우에 구조체를 사용하지 왜 배열도 사용하는가 궁금해진다.

그 이유는 구조체의 메모리 할당 방식에 답이 있다.

 

구조체 안에는 다양한 크기의 변수가 들어가기 때문에 배열처럼 같은 바이트로 주소 값을 이동할 수 없다.

그렇기 때문에 가장 큰 변수형을 기준으로 메모리를 할당하는 데 그러다 보면 사이사이에 빈 공간이 생기게 된다.

구조체 메워넣기(Structure Padding)

▼ 필요 메모리(5Byte)보다 큰 실사용 메모리(8Byte)

struct DataType {
    char _byte_1;
    int  _byte_4;
};

int main()
{
    struct DataType dt = {'A', 4};
    printf("DataType size : %lld\n", sizeof(dt));
    
    return (0);
}

구조체 DataType에 소속되어있는 char형은 1Byte, int형은 4Byte 메모리를 필요로 한다.

그래서 둘을 개별로 선언해서 사용하면 5Byte의 메모리를 사용한다.

 

그런데 구조체를 만들어서 사용하니 8Byte가 나온다.

이건 구조체를 배열처럼 사용할 수 있는 문법을 위해 발생하는 구조체 메워넣기 현상이다.

 

▼ 메모리의 크기가 달라서 다음 메모리 주소를 찾을 기준이 없다.

구조체 메워 넣기가 필요한 이유는 구조체가 다양한 자료형을 가질 수 있기 때문이다.

 

 

▼ 빈 공간을 사용하지 않아도 채워 넣어서 4Byte로 만들었다.

구조체는 아예 가장 큰 메모리를 필요로 하는 변수형을 기준을 잡는다. 

 

만약 3Byte 내에 들어갈 수 있는 또 다른 변수가 없다면 3Byte만큼 가상으로 메워 넣고 넘어가는데

이게 바로 구조체 메워 넣기다.

구조체 내부 변수 선언 순서에 따른 메모리 크기 차이

구조체 메워 넣기 때문에 구조체에서는 내부 변수들의 선언 순서에 따라 구조체의 크기가 바뀌곤 한다.

그래서 구조체 내부 변수들을 선언할 때에는 최대한 Byte를 계산해서 순서를 정하는 게 좋다.

 

▼ 같은 코드라도 내부 변수 선언 순서에 따라서 구조체의 크기가 바뀐다.

// 12 Byte
struct DataType {
    char _byte_1;
    // PADDING 3Byte
    int  _byte_4;
    short _byte_2;
    // PADDING 2Byte
};

// 8Byte
struct DataType {
    char _byte_1;
    short _byte_2;
    // PADDING 1Byte
    int  _byte_4;
};

최근의 컴파일러들은 이러한 경우를 알아서 최적화를 진행하는 경우가 많아서 다른 결과가 나올 수도 있다.

(의도적으로 메모리를 많이 먹게 만드는 경우는 잘 없으니까.)

 

▼ CPP에선 아래의 명령어로 메워 넣는 범위를 조절하여 직접 최적화할 수 있다.

#pragma pack(2)

자료형 재정의(typedef)

▼ 구조체 재정의는 보다 편리한 사용을 가능하게 한다.

#if 0

// 방법 1
struct _Point2D
{
    float _x;
    float _y;
};
typedef struct _Point2D Point2D;

#else

// 방법 2
typedef struct _Point2D {
    float _x;
    float _y;
}Point2D;

#endif

int main()
{
    Point2D p1 = {0.0f, 0.0f};
    return (0);
}

Main 함수에서 struct가 빠지고 일반 내장 변수처럼 사용하고 있다.

매번 struct를 작성하는 귀찮음을 해결해주기 때문에 대부분의 경우 typedef을 통해 구조체를 재정의해서 사용한다.

 

추가로 보통 재정의 후에 원하던 구조체명으로 쓰고 싶기 때문에 typedef 하기 전의

구조체명은 언더바(_)를 넣어서 선언해준다.

C와 CPP의 구조체 차이점

CPP의 창시자 비아르네 스트로우스트로프

CPP에서 구조체는 오로지 C 라이브러리와 하위 호환하기 위해 존재한다고 말했다.

 

CPP에는 클래스가 있지만 여전히 많은 분야에서 사용되는 C는 구조체 밖에 없다.

그래서 CPP와 C가 호환하기 위해서는 구조체의 존재가 필수였다.

 

겉보기만 같아 보이고 내부적으로는 다르게 작동한다.

typedef struct _CStruct {
    char c;
}CStruct;

typedef struct _CPP_STRUCT {
    char c;
    void print() { printf("%c", c); };
}CPP_STRUCT;

 

그럼에도 불구하고 CPP에서 구조체는 클래스에 가깝다.

구조체 안에 함수를 작성할 수도 있고 클래스가 사용하는 몇 가지의 기능들도 사용할 수 있다.

 

하지만 도리어 이게 C에서는 허용되지 않는 문법이기 때문에

CPP 표준 위원회에서는 구조체는 사용해야 한다면 C와 동일한 문법을 지키기를 권장한다.

존재 목적 자체가 C와 문제없이 호환하기 위함이기 때문이다.

 

C와 호환이 필요 없는 경우라면 클래스를 사용하는 것이 CPP 언어 의도에 알맞다.

공용체(Union)

메모리를 공용으로 하나만 할당하는 사용자 정의 자료형이다.

 

공용체의 의의는 아주 특정한 경우에는 메모리를 절약할 수 있다는 것이다.

특정 변수들이 동시에 사용되지 않고 하나씩만 사용한다는 것이 확실할 때 공용체를 사용하여

하나의 메모리만 사용해 메모리를 절약한다.

공용체 사용

구조체와 형태는 비슷하다.

 

structunion의 차이일 뿐이다.

union Case
{
    char    isChar;
    int     isInt;
};

공용으로 쓰기 때문에 아무리 많은 멤버 자료형이 있더라도 제일 큰 자료형의 메모리 하나를 가진다.

 

 

▼ 레고 돌기는 Byte와 상관이 없다.

메모리가 풍족하지 못 한 임베디드 환경이 아닌 이상 거의 보기 힘들다.

지금은 아 공용체라는 것만 알고 넘어가도 된다.