1) #define ASPECT_RATIO 1.653
2) const double AspectRatio = 1.653;
위의 두 소스는 비슷한 목적을 가지고 만들어졌지만 그 차이는 무엇일까?
1) #define의 경우에는, 전처리기에 의해서 컴파일러에게 코드가 넘어가기 전에 'ASPECT_RATIO' 를 전부 1.653 으로 바꿔버린다.
이것은 컴파일러 입장에서 해당 기호를 전혀 알 수 없다는 말이고, 에러가 발생하더라도 컴파일러가 전혀 도움을 줄 수 없다는 의미이다.
( 남이 작성한 경우라면 더더욱 해당 에러를 찾기가 어려워진다. )
반면에 2) const 의 경우에는 컴파일러에게도 인식되며, 기호 테이블 ( Symbol-Table ) 에도 포함된다.
----- [ 기호 테이블 ( Symbol-Table ) 이란 ?
c, c++ 컴파일 과정에서 .obj, .o 형식의 오브젝트 파일이 생성된다.
이 파일 안에 심볼 테이블이라는 데이터 구조가 존재하게 되는데, 이것을 통해 obj 파일의 여러 항목을 링커가 이해할 수 있는 이름으로 매핑시킨다.
그 밖에도 Variable name, Function name Objects, Classes, Interfaces 가 포함되며 보통 Hash Table 형태로 구현 된다. ] -----
또한 #define의 경우에는 사용한 횟수만큼 사본이 생성되지만, const 의 경우에는 사본이 단 한 번만 생성된다.
여기까지만 정리해도 #define의 단점을 명확하게 알 수 있다.
퍼포먼스가 더 나쁘고, 디버그도 더 어렵게 만든다.
솔직히 이것만으로도 지양해야하는 충분한 이유가 될 수 있다.
하지만 이미 #define으로 작성된 코드는 어떻게 해야할까? 전부 const 키워드로 교체해야할까?
그렇게 할 수 있다면 좋겠지만, 이것은 전적으로 자신이 선택해야할 사항이다.
분명한 것은 앞으로 작성될 코드에서는 #define을 지양하는 것이 좋다는 점이다.
그리고 #define을 const로 변경시키는 것 역시 어느 정도 주의해야할 사항들이 존재한다.
(1) 상수 포인터 (Constant Pointer)를 정의하는 경우 : 포인터 자체에 대한 상수화와 더불어 가리키는 대상 역시 const로 선언할 것.
즉, " const char* const p = greeting; // 상수 포인터, 상수 데이터 " 이런 형식
( 위의 예시가 무슨 말인지 모르겠다면 const 에 대해서 다룬 포스트를 참고하자, 링크 )
(2) 클래스 멤버로 상수를 정의하는 경우
class GamePlayer
{
private:
static const int NumTurns = 5; // 상수 선언
int scores[NumTurns]; // 상수를 사용하는 부분
...
};
여기서 NumTurns는 '정의' 한 것이 아닌 '선언'된 것이다.
보통 C++에서 사용하고자 하는 것에 대해 '정의'가 마련되어 있어야 하는 게 보통이지만, 정적 맴버로 만들어지는 정수류(각종 정수 타입, char, bool등) 타입의 클래스 내부 상수는 예외다. ( 즉, static const 클래스 멤버 )
이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없게 되어 있다.
하지만 별도의 정의를 제공해야 한다면 ( 구식 컴파일러 ),
const int GamePlayer::NumTurns; // NumTruns의 정의
이런 식으로 작성해서 반드시 "구현 파일(.c, .cpp ...)" 에 둬야 한다.
위의 예시에서 NumTurns 는 선언될 당시에 바로 초기화가 이루어지기 때문에 정의에는 따로 상수의 초기값이 있으면 안 되기 때문이다.
이게 무슨 말이냐?
일반적으로 static 멤버는 선언 및 정의가 따로 이루어져야 한다.
즉, 선언과 동시에 초기화가 진행될 수 없다는 말이다.
하지만 숫자 타입의 static const 멤버는 선언과 동시에 초기화를 해야한다.
( 어쩌면 당연한 말이다.
const로 선언 되었다는 말 자체가 값이 고정된다는 말인데, 나중에 초기화를 하겠다? 이상하지 않은가? )
일반적인 최신 컴파일러의 경우에는 위의 경우로만 사용해도 아무 문제가 없다.
하지만 구식 컴파일러는 static const 멤버에 대해서 선언과 동시에 초기화를 한 뒤에, 정의까지 따로 제공을 해줘야 한다는 의미이다.
그것도 위의 예시처럼 값은 할당하지 않은채로 말이다. ( 이상하다고 생각되면 정상이다. 그래서 구식 컴파일러인 것이다. )
class MathTool {
public:
#define pi 3.14
MathTool() {
cout << "in constructor: "<< pi << "\n";
}
};
int main()
{
MathTool *a = new MathTool();
cout << "main : " << pi << endl;
return 0;
}
#define 자체를 클래스 내부에서 작성해도 컴파일러는 별 문제없이 컴파일 해준다.
그래서 이런 코드가 어딘가에 사용이 될까?
→ 아니다. 오히려 아무런 이득이 없는 코드 작성 방식이며 절대 지양해야 한다.
#define 은 #undef 되지만 않으면 컴파일일 끝날 때까지 유효하기 때문에 class 의 캡슐화 혜택을 하나도 받을 수 없게 된다.
즉, private 접근 지정자의 #define 같은 개념은 존재하지 않는다는 말이다.
만약 선언과 동시에 초기화되는 형식을 지원하지 않는 또 다른 구식 컴파일러라면,
class GamePlayer
{
private:
static const int NumTurns;
int scores[NumTurns] //error!
};
// 구식 컴파일러는 클래스 정적 상수 멤버가 선언과 동시에 초기화되는 것을 지원하지 않는다.
const int GamePlayer::NumTurns = 5;
위의 상황에서 컴파일 과정 중 NumTurns라는 클래스 상수를 정의 내리지 못하고 scores배열의 길이 값도 알지 못하여 에러가 발생한다. ( scores 배열 입장에서는 자신이 선언될 때, NumTurns 의 값을 알지 못한다. )
해당 상황에서는 class 멤버 상수를 포기해야 할까?
→ Enum Hack 기법을 활용
class GamePlayer
{
private:
enum { NumTurns = 5 }; //나열자 둔갑술 - 인트형 상수를 만드는데 쓰이는 팁
int scores[NumTurns];
};
Enum의 동작 방식은 const 보다는 #define에 가까운데, 이 경우에 접근 방식 자제는 const 처럼 사용할 수 있다. ( private enum, public enum... )
하지만 const와는 다르게 #define 처럼 주소나 참조자를 얻는 행위 자체가 허용되지 않으며, 동시에 메모리 역시 따로 추가해서 쓰이지 않는다.
즉, Enum Hack 방식이 특정 클래스에서만 사용할 수 있는 멤버 상수를 만드는데 굉장히 유용하다고 말할 수 있다.
하지만 이 방식은 오로지 정수형 상수를 만드는 데에만 쓰일 수 있다는 것을 명심해야 한다.
( 애초에 Enum 타입이 int 타입으로도 쓰일 수 있기 때문에 나온 방식 )
( 또한 다시 한 번 언급하지만, 해당 방식은 멤버 상수에 대한 선언 및 초기화가 동시에 되지 않는 구식 컴파일러에서 사용하는 궁여지책에 해당한다.
일반적으로, c++ 11 부터 멤버 상수에 대한 선언 및 초기화를 동시에 하는 행위가 가능하다. )
// a와 b 중에 큰 것을 f에 넘겨 호출한다.
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
이런 식의 매크로 함수는 단점이 굉장히 많아서 지양해야 하는 코드이다.
가장 먼저, 1) 코드의 직관성, 가독성이 매우 떨어지며 쾌적한 디버깅을 방해한다.
위의 예시와 비슷한 매크로 함수가 10개 정도에 그것들을 사용하는 코드까지 포함한다면 실수를 유발하기 딱 좋은 상황이라고 볼 수 있다.
2)
#define f(a) printf("%d\n", a);
// a와 b 중에 큰 것을 f에 넘겨 호출한다.
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
int main()
{
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a가 두 번 증가한다.
printf("a: %d, b: %d\n", a, b);
CALL_WITH_MAX(++a, b + 10); // a가 한 번 증가한다.
printf("a: %d, b: %d\n", a, b);
}
f가 호출되기 이전에, a에 대한 비교를 통해서 처리한 결과가 어떤 것이냐에 따라 최종 결과가 달라지기 때문에 발생하는 현상이다.
애초에 매크로 함수를 사용하는 가장 큰 목적은 함수 호출을 제거해준다는 점이다.
→ inline 함수에 대한 템플릿이 완벽한 대안이 될 수 있다.
// 기존 매크로 함수
#define CALL_WITH_MAX(a, b) f( (a) > (b) ? (a) : (b) )
// inline 템플릿 함수 형식으로 변경
// T가 정확히 무엇인지 모르기 떄문에, 매개변수로 상수 객체에 대한 참조자를 쓴다.
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
기존 매크로의 효율을 그대로 유지함은 물론 정규 함수의 모든 동작 방식(유효 범위, 접근 규칙 등) 및 타입 안전성까지 완벽히 취할 수 있는 방법이다.
( 기존 매크로 함수처럼 어떤 인자도 받을 수 있고,
함수 호출 과정 없이 진행되지만 일반 함수처럼 스택 영역을 가지며 비교 과정이 최종 결과 값 연산에 영향을 주지 않기 때문에 훨씬 안전하다. )
항목 6) 암시적으로 생성되는 함수들을 금지하기 (0) | 2021.12.05 |
---|---|
항목 5) C++가 자동으로 만들고 호출하는 함수들 (0) | 2021.12.05 |
항목 4) 객체 사용 전에 반드시 먼저 초기화하자 (0) | 2021.12.05 |
항목 3) const 에 대해서 (1) | 2021.08.15 |
항목 1) C++ 언어란? (0) | 2021.08.15 |
댓글 영역