const 변수
변수 앞에 const 키워드를 붙이면 해당 변수의 값은 바뀔 수 없다.
말하자면 생김새는 변수지만 본모습은 상수인 셈.
const int a = 0;
a = 10; // Error
이러한 특성을 이용하면 const를 #define처럼 사용하는 것도 가능하다.
const double PI = 3.1415926535;
원주율처럼 절대 바뀔 일이 없는 값을 #define으로 선언할지 const 변수로 선언할지 고를 수 있다.
const 포인터와 const 레퍼런스
const 포인터
int* IntPtr = nullptr;
IntPtr = new int[10];
IntPtr[0] = 10;
위처럼 동적 메모리를 할당받기 위해선 포인터를 사용해야 한다.
그렇다면 여기서 IntPtr에 할당해준 int형 배열의 값이 바뀌는 것을 막고 싶다면 어디에 const를 붙여야 할까?
const int* IntPtr = nullptr;
IntPtr = new int[10];
IntPtr[0] = 10; // Error
int const* IntPtr = nullptr;
IntPtr = new int[10];
IntPtr[0] = 10; // Error
이 중 어떤 방법을 쓰든 배열의 값(원본 데이터)을 지킬 수 있다.
그렇다면 반대로 IntPtr 자체, 포인터 자체의 값을 바꿀 수 없게 하려면 어떻게 해야 할까?
int* const IntPtr = nullptr;
IntPtr = new int[10]; // Error
위와 같이 int* 자료형 뒤에 const를 붙이면 IntPtr의 값은 바뀔 수 없다.
다음은 포인터의 값과 포인터가 가르키는 값 모두 변경되지 않게 해보자.
int const* const IntPtr = nullptr;
const int* const IntPtr = nullptr;
음... 더럽게 복잡하네...
앞서 보았던 원본 데이터를 지킬 때 const를 붙이는 위치와
포인터 자체의 값을 지킬 때 붙이는 const의 위치가 합쳐진 것이라 보면 그나마 쉽게 볼 수 있다.
겉보기엔 막무가내 문법 같지만 나름의 규칙이 있다.
int NewInt = 100;
int const* const IntPtr = &NewInt;
맨 처음 const 키워드 앞에는 int 자료형이 있는 것을 볼 수 있다.
그렇다면 const는 IntPtr이 가리키고 있는 원본값에 적용되고 따라서 IntPtr이 가르키고 있는 NewInt는 값을 변경할 수 없다.
int NewInt = 100;
int const* const IntPtr = &NewInt;
*IntPtr = 100; // Error
두번째 const 앞에는 *연산자가 있다.
int의 포인터인 IntPtr에 적용되며 IntPtr 자체의 값, 즉 포인터의 변경이 불가능해진다.
그런데 혼란스럽게도 위 코드의 첫 const 위치를 바꿀 수 있다.
int NewInt = 100;
const int* const IntPtr = &NewInt;
*IntPtr = 100; // Error
규칙을 알아보았고 예외적인 문법도 보았다.
하지만 역시 직관적이지 않고 이해하기 힘든 것 같다.
한 가지 팁은, const 변수 선언은 오른쪽에서 왼쪽에서 읽는 편이 이해하기 쉽다는 것.
지금까지 왼쪽에서 오른쪽으로 코드를 읽었다면 이번에 반대로 읽어보자.
int NewInt = 100;
int const* IntPtr = &NewInt; // == (const int* IntPtr = &NewInt);
"IntPtr는 const int 자료형을 가르킨다."
다음 코드도 한번 읽어보자.
int* const IntPtr = nullpt;
"IntPtr는 const 포인터로 int 자료형을 가르킨다."
흔히 프로그래머들은 코드를 읽을 때 오른쪽에서 왼쪽으로 읽으라고 말한다.
어쩌면 const 변수 선언을 쉽게 파악하기 위한 팁이 숨겨져 있었던 걸지도 모르겠다는 생각이 든다.
const 키워드가 붙은 변수들의 공통점은 const가 붙은 변수는 선언과 동시에 초기화를 해줘야하며
초기화할 때 바뀌지 않길 원하는 값으로 넣어줘야 한다는 사실.
....익숙한 규칙이다.
const 레퍼런스
그렇다. 레퍼런스를 사용할 때에도 선언과 초기화를 동시에 해줘야만 했다.
이미 레퍼런스 또한 const 속성을 가지고 있던 것.
void Fuc(const MyClass& _MyClass)
{
_MyClass = 10; // Error
}
const를 통해 매개변수의 값이 바뀌는 것을 막을 수 있고
참조형까지 더하면 MyClass 자료형의 크기로 받지 않아 프로그램 성능에도 도움이 된다.
const 메서드
변수에 const가 붙여지는 것을 보았으니 이제 함수에 const가 붙여지는 것을 보자.
const int Func()
{
return 24;
}
반환 타입에 const가 붙은 경우는 제외한다.
엄밀히 말하면 const가 붙은 건 메서드가 아닌 메서드가 반환하는 자료형이기 때문.
그럼 const 메서드는 어떤 모습일까?
const 메서드는 함수 뒤에 const가 붙는다.
class MyClass
{
private:
int A;
int B;
public:
void Func() const
{
A = 10; // Error
}
};
함수 뒤에 const를 붙이면 해당 클래스 멤버 변수에 접근은 가능하지만 값을 변경할 수는 없게 된다.
하지만 mutable 키워드를 붙이면 변경할 수 있게 된다.
mutable는 나중에 알아보자.
constexpr
constexpr는 c++ 11에 추가된 키워드이다.
우리는 constexpr를 통해 상수 표현식이라는 것을 사용할 수 있다.
상수 표현식은 뭘까?
상수 표현식은 상수가 필요한 곳에서 쓰인다.
정확히는 상수인지 아닌지 모르는 녀석을 상수라고 명시해줄 때 쓰인다.
이게 무슨 말인지 한번 보도록 하자.
const int ReturnSize()
{
return 24;
}
int main()
{
int IntArr[ReturnSize()]; // Error: 식에 상수값이 있어야 합니다.
return 0;
}
보통 배열의 크기를 정할 때는 상수를 넣어줘야 한다. 가변 배열이 아니라면
그러므로 위 코드는 안되는 것이 당연하다.
하지만 ReturnSize()의 const를 constexpr로 바꾸면 어떻게 될까?
constexpr int ReturnSize()
{
return 24;
}
int main()
{
int IntArr[ReturnSize()];
return 0;
}
에러가 사라진다.
심지어 상수와 연산할 수도 있다.
constexpr int ReturnSize()
{
return 24;
}
int main()
{
int IntArr[ReturnSize() + 1];
return 0;
}
상수 표현식은 컴파일 타임에 평가된다.
때문에 constexpr를 사용하면 컴파일러가 최적화를 훨씬 더 효과적으로 할 수 있다.
constexpr 변수
constexpr가 컴파일 타임에 평가되기 때문에 constexpr 변수 또한 컴파일 타임에 초기화될 수 있어야 한다.
즉, 초기화를 하지 않거나 상수가 아닌 값으로 초기화를 할 수 없다.
int a = 0;
constexpr int b = a; // Error: 식에 상수 값이 있어야 한다.
constexpr int c; //'c': constexpr 개체를 초기화해야 한다.
constexpr 메서드
constexpr float Exp(float x, int n)
{
return n == 0 ? 1 :
n % 2 == 0 ? Exp(x * x, n / 2) :
Exp(x * x, (n - 1) / 2) * x;
}
<constexpr 함수의 규칙>
- constexpr 함수는 리터럴 타입만 반환해야 한다.
- constexpr 함수는 재귀호출이 가능한다.
- 가상함수일 수 없다.
- goto문이나 try 블록을 사용할 수 없다.
- if문과 switch문, for문, while문 및 do-while문을 포함한 모든 반복문은 사용할 수 있다.
이 밖에도 규칙은 더 많이 있다.
constexpr와 const의 차이점
constexpr은 컴파일 타임에 값이 확정되어야 하는 상수이며,
const는 런타임 혹은 컴파일 타임에 값이 확정되어야 하는 상수다.
constexpr와 const의 공통점
한번 할당된 값을 바꿀 수 없다.
'둥지 > CS' 카테고리의 다른 글
C# 인터페이스와 추상 클래스 정리 (0) | 2023.07.21 |
---|---|
C/C++ 더블 포인터 사용 까닭 (0) | 2023.05.01 |
캐싱 (0) | 2023.04.03 |