본문 바로가기
C++/강좌

[C++ 강좌] 상수 선언 방법 #define, const, enum, enum class

by EZC 2021. 1. 20.
반응형

 

C/C++ 공부를 하다 보면 다양한 방식의 상수 선언 법을 알게 됩니다. 그리고 대체 어떤 방법을 사용해야 할지, 어떤 차이점이 있는지 궁금해지곤 하죠. 이번 강좌에서는 #define, const, enum, enum class 네가지 상수 선언 방식과 Best Practice는 무엇인지 알아보도록 하겠습니다.

 

목차 (클릭 시 이동)

 

     

    1. #define 상수 (매크로)

    소스코드를 빌드한다는 것은 전처리(Preprocess) → 컴파일(Compile) → 어셈블(Assemble) → 링크(Link)라는 일련의 과정을 거쳐 실행파일(.exe)을 만드는 것입니다. 첫 과정인 전처리는 전처리기(Proprocessor)가 소스코드를 쭉 읽으며 전처리 지시자(Proprocess Directives)로 처리된 부분을 해석하여 소스코드를 변형시켜주는 것을 말합니다.

     

    전처리 지시자는 #define, #include, #endif, #pragma등과 같이 같이 #으로 시작하는 문구를 말하는데, 여기서 #define매크로라고도 하며 이를 이용해 상수를 정의할 수 있습니다.

    #define X 3
    #define Y 3
    
    int arr[X][Y];

    위와 같이 매크로 상수를 정의하고 컴파일을 하면 컴파일 전에 전처리기에 의해 소스코드에서 X라고 작성된 모든 부분이 3으로 치환 됩니다.(X의 모든 부분이 3으로 Ctrl-C, Ctrl-V 된다고 생각하시면 됩니다. #include로 선언하는 헤더 파일도 #include 부분에 해당 헤더파일의 전체 내용이 Ctrl-C, Ctrl-V 되는 것입니다.)

     

    따라서 컴파일을 해도 기호 테이블(Symbol Table)에는 X가 없는 것이고 이로 인해 만약 컴파일 중에 X 값에 관련하여 에러가 발생하면 컴파일러는 X가 아니라 3이라는 값에 대해 에러 메시지를 뿜게 됩니다. 그렇게 되면 만약 코드가 굉장히 길고 X 뿐만 아니라 Y와 같은 다른 매크로 상수도 3으로 정의되어 있다면 디버깅 작업이 굉장히 어려워지는 것이죠.

     

    기호 테이블(Symbol Table)이란 컴파일러가 컴파일을 하기 위해 변수, 함수, 객체, 클래스, 인터페이스들을 따로 보관하여 관리하는 것을 말합니다.

     

    그리고 소스코드에 있는 X의 수 만큼 치환을 위해 많은 사본이 생기며, 매크로 상수는 클래스가 아니라 헤더 파일에 종속되어 있기 때문에 캡슐화(Encapsulation)가 되지 않습니다. 즉, C++에서 추구하는 객체지향 프로그래밍에도 어긋나게 되는 것이죠.

     

    2. const 상수

    위 매크로 상수의 문제점은 일단 const 상수로 해결이 됩니다.

    const int x = 3;
    const int y = 5;
    
    int arr[x][y];

    x, y컴파일 시 기호 테이블에 존재하게 되니 컴파일 에러 시 변수 이름으로 추적이 가능하고, 한 번만 선언하여 사용하면 되므로 많은 사본이 생기지도 않으며, 클래스 내부에 상수 선언을 하게 되면 객체지향 프로그래밍이 가능하게 됩니다.

     

    좋습니다!..만 문제점이 한 가지 있네요. 우리는 그저 3, 5라는 값을 매직 넘버(Magic Number)로 사용하고 싶은 것뿐인데 이런 식으로 상수를 선언해버리면 메모리에 int 만큼의 공간을 차지하면서 메모리 낭비가 됩니다.

     

    하지만 물론 해결책은 있죠.

     

    3. enum 상수 (enum hack)

    enum eCoor
    {
    	X = 3,
    	Y = 5
    };
    
    int arr[X][Y];

    enum은 컴파일 타임에 컴파일러에게 X, Y가 무엇인지 알려주는 매직 넘버 역할을 하기 때문에 메모리에 할당되지 않습니다. 따라서 메모리 낭비도 없고 컴파일 타임에 존재하기 때문에 컴파일 에러 시 추적도 가능하며 class 내부에 선언이 가능하기 때문에 객체지향 프로그래밍에도 적합합니다. 그리고 이와 같은 방식을 enum hack이라고도 합니다.

     

    다만 enum 또한 몇가지 단점이 있습니다.

     

    정수형만 가능하기 때문에 PI = 3.14 같은 상수의 선언은 불가능합니다.

    enum eMath { PI = 3.14 }; // 컴파일 에러

     

    컴파일 타임에 기본 형식인 int 형으로 암시적 형변환이 됩니다. (Week Type)

    enum eCoor
    {
    	X = 3,
    	Y = 5
    };
    
    int x = X; // 가능
    
    if (X)     // 가능
    {
    }

    사실 암시적 형 변환은 어찌 보면 개발자에게 편리한 기능이니 오롯이 단점이라고 말하기는 애매하기도 하지만, 다른 type과 enum의 확실한 구분점이 모호해지고 개발자에게 의도치 않은 실수를 가져다줄 수 있는 요지이기도 합니다.

     

    또한 다른 열거형과 이름 중복이 불가능합니다.

    enum eDogCoor
    {
    	X = 3,
    	Y = 5
    };
    
    enum eCatCoor
    {
    	X = 3, // X 재정의로 컴파일 에러
    	Y = 5  // 열거형은 매크로 상수와 달리 컴파일 에러 시에도 X로 추적이 가능
    };

    이럴 경우 namespace로 구분지어 해결하기도 했죠.

    namespace dogcoor
    {
    	enum eDogCoor
    	{
    		X = 3,
    		Y = 5
    	};
    }
    
    namespace catcoor
    {
    	enum eCatCoor
    	{
    		X = 3,
    		Y = 5
    	};
    }

     

    이와 같이 enum은 매크로 상수와 const 상수의 단점들을 커버하기는 하지만 여전히 enum 자체의 불편한 문제점들이 있습니다.

     

    그리고 이러한 단점들을 커버하기 위한 enum class가 C++11에서 등장합니다.(다만 정수형만 가능한 것은 변하지 않습니다.)

     

    4. enum class

    enum class eCoor
    {
    	X = 3,
    	Y = 5
    };
    
    int x = X;                          // 컴파일 에러
    int x = static_cast<int>(X);        // 컴파일 에러
    int y = static_cast<int>(eCoor::X); // 가능
    
    if (eCoor::X)                       // 컴파일 에러
    {
    }

    enum class의 기본 형식은 int가 아니라 enum 그 자체이기 때문에 암시적 형 변환이 불가능하며 명시적 형 변환만을 허용합니다. 또한 기존의 enum은 선언 값들이 enum {}; 안에 국한되지 않기 때문에 별도의 스코프 없이 그냥 X 자체로 사용이 가능했지만, enum class는 선언 값들이 enum class {}; 안에 국한되기 때문에 eCoor::X와 같이 별도의 스코프를 사용해야 합니다.

     

    그렇기 때문에 다른 열거형과의 이름 중복도 이제는 가능해진 것이죠.

    enum class eDogCoor
    {
    	X = 3,
    	Y = 5
    };
    
    enum class eCatCoor
    {
    	X = 3, // 가능
    	Y = 5  // 가능
    };

     

    5. 결론 (Best Practice)

    결론은 C++에서 매직 넘버로써 (정수형) 상수를 사용하고자 하면 enum class를 쓰는 것이 좋습니다. 객체지향적 프로그래밍이 가능하고, 메모리 낭비도 없으며, 컴파일 타임에는 존재하여 디버깅이 용이합니다. 또한 기존 enum의 단점들까지 보완하게 됐죠. 다만 정수형 상수만 가능하다는 것도 잊지 않으시면 될 것 같습니다.

     

    그럼 틀리거나 궁금한 점이 있다면 댓글 부탁드리며 이번 강좌는 여기에서 마치도록 하겠습니다. 감사합니다! :)

    반응형

    댓글