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 |