C++/C++ CS 개념

매크로(#define) VS 상수

CE : 하랑 2023. 10. 26. 19:27

 

 

ex)

 

#define TEST 1000

 

문제점

TEST -> 기호식 이름(symbolic name)으로 보이지만 컴파일러에겐 전혀 보이지 않는다.

 

 

이유는?

소스 코드가 어떻게든 컴파일러에게 넘어가기 전에 선행 처리자가 밀어버리고 숫자 상수로 바꾸어 버리기 때문이다.

 

 


상수 :  수식에서 변하지 않는 값

컴파일러 : 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램


 

결과

test라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않는다.

 

 

 

해결방법은 ?

매크로 대신 상수 사용

ex)

const int test = 1000;

 

(1) test는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 당연히 컴파일러의 눈에도 보이며 기호 테이블에도 당연히 들어간다.

 

(2) 상수가 부동소수점 실수 타입일 경우에는 컴파일을 거친 최종 코드의 크기가 #define을 썼을 때보다 작게 나올 수 있다.

 

 

 

#define을 상수로 교체할 때 주의해야할 2가지 경우

1. 상수 포인터(constant pointer)를 정의하는 경우

(1) 상수 정의는 대개 헤더 파일에 넣는 것이 상례이므로 포인터는 꼭 const로 선언 -> 포인터가 가리키는 대상까지 const 선언

ex) const char* const name = "Hong"; -> string 객체 : const std::string name("Hong");

 


 

 

(1) 포인터

- 일반 데이터를 저장하는 변수가 아닌 메모리의 주소 값을 저장하는 변수

 

 

(2) 포인터 연산자

- 주소 연산자(&) : 변수 이름 앞에서 사용하여 해당 변수의 메모리 주소 값을 반환

- 참조 연산자(*) : 포인터의 이름이나 주소 앞에 사용하며 해당 주소를 참조하여 주소에 저장되어 있는 값을 반환

 

 


 

 

2. 클래스 맴버로 상수를 정의하는 경우 -> 클래스 상수를 정의하는 경우

(1) 어떤 상수의 유효범위를 클래스로 한정하고자 할 때는 그 상수를 멤버로 만들어야 하는데, 그 상수의 사본 개수가 한 개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.

ex)

class test{

private:

  static const int num = 5;  // 상수 선언

}

 

(2) num은 선언(declaration)

- C++에서는 사용하고자 하는 것에 대해 정의가 마련되어 있어야 하는 게 보통이지만, 정적 멤버로 만들어지는 정수류(각종 정수 타입, char, bool 등) 타입의 클래스 내부 상수는 예외

 

- 단, 클래스 상수의 주소를 구한다든지, 주소를 구하지 않는데도 컴파일러가 잘못 만들어진 관계로 정의를 달라고 떼쓰는 경우에는 별도의 정의를 제공 -> const int test :: num;

 

(3) 클래스 상수의 정의는 구현 파일에 둡니다. 헤더 파일은 두지 않습니다.

(4) 정의에는 상수의 초기값이 있으면 안 되는 이유 -> 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다. -> num은 선언될 당시에 바로 초기화된다는 것

 


 

정적 멤버 변수 

- 정적 멤버란 클래스에 속하지만, 객체 별로 할당되지 않고 클래스의 모든 객체가 공유하는 멤버를 의미

- 멤버 변수가 정적(static)으로 선언되면, 해당 클래스의 모든 객체에 대해 하나의 데이터만이 유지 관리된다.

- 클래스 영역에서 선언되지만, 정의는 파일 영역에서 수행

- 정적 멤버 변수를 외부에서도 접근할 수 있게 하고 싶으면, 정적 멤버 변수를 public 영역에 선언

 


 

 

TIP!

 

1. 매크로는 일단 정의되면 컴파일이 끝날 때까지 유효

-> #define은 클래스 상수를 정의하는 데 쓸 수도 없을 뿐 아니라 어떤 형태의 캡슐화 혜택도 받을 수 없습니다.

 

2. 조금 오래된 컴파일러는 위의 문법을 받아들이지 않는 경우 종종 있음

이유 ?

- 정적 클래스 멤버가 선언된 시점에 초기값을 주는 것이 대개 맞지 않다고 판단

- 클래스 내부 초기화를 허용하는 경우가 정수 타입의 상수에 대해서만 국한

 

해결방법

- 초기값을 상수 정의 시점에 주도록 한다.

 

 

3. 나열자 둔갑술 (enum hack)

- 나열자 타입의 값은 int가 놓일 곳에도 쓸 수 있다는 C++의 진실을 적극 활용

ex)

 

class test{

private:

  enum{ num = 5 }; // 나열자 둔갑술 num을 5에 대한 기호식 이름으로 만듬

 

  int scores[num];

};

 

 

(1) 나열자 둔갑술은 동작 방식이 const보다는 #define에 더 가까움 

-> enum은 #define처럼 어떤 형태의 쓸데없는 메모리 할당도 절대 저지르지 않는다.

 

(2) 실용적 -> 템플릿 메타프로그래밍의 핵심 기법

 


 

템플릿 메타프로그래밍

- 템플릿에 어떠한 타입이 있는 값을 사용할 수 있고, 이를 통해서 컴파일 타임에 구성되는 어떠한 로직을 구성할 수 있다.


 

4. #define 지시자의 또 다른 오용 사례 -> 매크로 함수

ex)

#define MAX(a,b) f((a) > (b) ? (a) : (b))

- 이런 매크로를 작성할 때는 매크로 본문에 들어 있는 인자마다 반드시 괄호를 씌워 줘야한다. 

 

기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작방식 및 타입 안정성까지 완벽히 취할 수 있는 방법

-> 인라인 함수에 대한 템플릿 준비

ex)

template<typename T>

inline void Max(const T& a, const T& b)

{

  f(a > b ? a: b);

}

 

- 이 함수는 템플릿이기 때문에 동일 계열 함수군을 만들어낸다.

- 동일한 타입의 객체 두 개를 인자로 받고 둘 중 큰 것을 f에 넘겨서 호출하는 구조

- 함수 본문에 괄호로 분칠을 해 댈 필요도 없고, 인자를 여러 번 평가할지도 모른다는 걱정도 없어진다.

- 함수이기 때문에 유효범위 및 접근 규칙을 그대로 따라간다.

 

 

5. 단순한 상수를 쓸 때는, #define보다 const 객체 혹은 enum을 우선 생각

6. 함수처럼 쓰이는 매크로를 만들려면, #define보다 인라인 함수를 우선 생각

 

 

 

 

 

출처 : 스콧 마이어스, [Effective C++ 제 3판], 곽용재, 프로텍미디어(2015), p 54-58

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'C++ > C++ CS 개념' 카테고리의 다른 글

생성자, 소멸자, 대입 연산자  (0) 2023.11.08
객체지향 (Object-Oriented)  (0) 2023.11.08
아키텍처 패턴  (3) 2023.11.08
소프트웨어 생명 주기 (Software Life Cycle)  (0) 2023.11.07
const  (0) 2023.10.30