포인터의 상수성( constness )
포인터( pointer )는 객체의 주소를 값으로 하는 변수입니다.
그리고, 이 포인터는 역참조( dereference ) 연산자 *를 사용하여, 저장하고 있는 주소에 있는 객체의 값을 변경할 수 있습니다.
그렇기 때문에, 포인터와 관련되는 상수 속성은 두 개가 됩니다.
하나는 포인터가 가리키는 객체의 값을 변경할 수 있는지에 대한 속성이고, 다른 하나는 변수로서의 포인터의 값을 변경할 수 있는지에 대한 속성입니다.
const 객체에 대한 포인터( pointer to const )
포인터가 가리키는 객체의 값을 변경할 수 없는 경우는 두 가지입니다.
그리고, 이 두 가지 경우 모두, 포인터 대상의 타입 앞에 const를 붙입니다.
● 첫 번째는 포인터가 가리키는 대상이 const 객체인 경우입니다.
이 경우엔 대상 객체를 수정할 수 없기 때문에, const 속성이 없는 포인터는 이 객체의 주소를 저장할 수 없습니다.
#include <iostream>
class CObject{
int m_Data{}; // private 멤버
public:
CObject( int data) : m_Data( data ){};
void setData( int val){
m_Data = val;
}
void print() const { // const 멤버 함수
std::cout << m_Data << '\n';
}
};
int main(){
const CObject obj{ 10 }; // const 객체
CObject* pNormalPtr{ &obj }; // error !
const CObject* pPtrToConst{ &obj }; // ok
const CObject obj2{ 20 }; // 다른 대상을 지시
pPtrToConst = &obj2;
}
위의 일반 포인터인 pNormalPtr가 const 객체인 obj의 주소에 접근할 수 있다면, 이 주소를 사용해서 obj의 값을 변경할 수 있으므로, 컴파일러는 이러한 접근을 배제합니다.
● 두 번째는 포인터가 가리키는 비-const 대상을 포인터를 통해서 수정할 수 없도록 하는 경우입니다.
이 경우는 대상 자체가 const 객체가 아니므로, 일반 포인터와 const 객체에 대한 포인터( pointer to const ) 모두 이 객체의 주소를 저장할 수 있습니다.
이 경우는 주로 함수에 포인터로 전달된 객체의 값을 변경할 수 없도록 하기 위해 사용됩니다.
#include <iostream>
class CObject{
int m_Data{}; // private 멤버
public:
CObject( int data) : m_Data( data ){};
void setData( int val){
m_Data = val;
}
void print() const { // const 멤버 함수
std::cout << m_Data << '\n';
}
};
int main(){
CObject obj{ 10 }; // 일반 객체
obj.setData( 15 ); // ok
CObject* pNormalPtr{ &obj }; // ok
const CObject* pPtrToConst{ &obj }; // ok
pPtrToConst->print(); // ok
CObject obj2{ 20 }; // 다른 대상을 지시
pPtrToConst = &obj2;
pPtrToConst->setData( 18 ); // error ! const 멤버 함수만 호출 가능
}
위에서, obj 객체는 const 객체가 아니더라도, const 객체에 대한 포인터( pointer to const )를 사용해서, 포인터가 가리키는 객체를 const 객체로 취급할 수 있습니다.
그리고, const 객체는 const 멤버 함수( member function )만을 호출할 수 있으므로, pPtrToConst 포인터를 통해서 setData 함수를 호출하는 것은 오류입니다.
[C++] 객체를 변경할 수 없는 const 멤버 함수
const 멤버 함수C++에서는 기본 타입에 const 키워드를 붙여 값을 변경할 수 없는 변수를 정의할 수 있습니다. 그리고, 이러한 변수는 값을 변경할 수 없으므로, 정의할 때 반드시 초기화 과정을 거
codingbonfire.tistory.com
그리고, 이렇게 const 객체가 일반 멤버 함수를 호출하면 다음과 같은 오류 메시지를 만나게 됩니다.
error: passing 'const CObject' as 'this' argument discards qualifiers [-fpermissive]
● 참고로, const_cast를 사용해서, 위의 const가 아닌 객체 obj2를 가리키는 pPtrToConst 포인터의 const 속성을 제거할 수 있습니다.
void remove_const_attribute( const CObject* pPtrToConst){
CObject* pPtr = const_cast< CObject* >( pPtrToConst );
pPtr->setData( 18 ); // const 멤버 함수도 호출 가능
pPtr->print();
CObject* pPtr2 = ( CObject* )pPtrToConst; // C-style cast
pPtr2->setData( 13 ); // const 멤버 함수도 호출 가능
pPtr2->print();
}
int main(){
CObject obj2{ 20 }; // 다른 대상을 지시
const CObject* pPtrToConst = &obj2;
remove_const_attribute( pPtrToConst);
}
▼출력
18
13
그리고, 위에서 볼 수 있듯이, C-style cast를 통해서도 포인터의 const 속성을 삭제할 수 있는데, 이 경우의 타입 변환은 의도하지 않은 가운데 실행될 수 있기 때문에, 이러한 타입 변환을 시도하는 것은 코드의 안정성에 악영향을 미치게 됩니다.
그런데, 수정할 수 없는 대상 객체에 대한 포인터( pointer to const )는 대상의 값을 변경하는 것을 금지할 뿐이지, 포인터가 가리키는 대상을 다른 대상으로 변경할 수 없다는 것이 아닙니다.
그럼, 포인터가 가리키는 대상을 변경하지 못하게 하려면 어떻게 해야 할까요?
const 포인터( const pointer )
const int 타입 변수의 값을 변경할 수 없는 것처럼, const 포인터는 저장하고 있는 객체의 주소를 변경수 없는 포인터입니다.
그렇기 때문에, 이 포인터를 선언할 때는 반드시 초기화를 해주어야 합니다.
그리고, 이러한 포인터를 선언하기 위해서는 * 기호 뒤에 const를 사용해야 합니다.
int main(){
int val{ 10 };
int* const pConstPtr{ &val }; // const 포인터
*pConstPtr = 15; // 대상의 값 변경
int val2{ 20 };
pConstPtr = &val2; // error !
}
▼출력
error: assignment of read-only variable 'pConstPtr'
이 const 포인터는 포인터가 저장하고 있는 값( 대상 객체의 주소 )을 변경할 수 없을 뿐이므로, 이 객체의 주소를 통해 대상 객체의 값을 수정할 수 있습니다.
이러한 변수를 const 포인터( 변수 )라고 부르는 것이 다른 const 타입을 부를 때와의 일관성이 있습니다.
그런데, 재밌는 것은 일반적으로 const 객체에 대한 포인터( pointer to const )를 const 포인터라고 부른다는 것입니다.
이것은 주로 사용되는 것이 const 객체에 대한 포인터( pointer to const )인데, 이를 부를 때 너무 길기 때문일 것입니다.
다음은 클래스 객체의 멤버 함수를 호출하는 코드입니다.
class CObject{
int m_Data{}; // private 멤버
public:
void setData( int val){
m_Data = val;
}
};
int main(){
CObject obj;
obj.setData( 20 );
}
이 예문에서 컴파일러가 main 함수의 obj.setData( 20 ) 표현식을 마주치게 되면, 다음과 같은 함수를 생성하고, 이 함수에 obj 객체의 주소를 전달합니다.
static void CObject::setData( CObject* const this, int val ){
this->m_Data = val;
}
이때 사용되는 매개변수 this가 const 포인터입니다.
이 포인터는 가리키는 대상을 변경할 수 없고, 이것은 this가 잘못된 대상을 가리키는 일이 생기지 않음을 보장합니다.
const 객체에 대한 const 포인터( const pointer to const )
이 타입의 포인터는 위에서 얘기한 두 타입의 특징이 결합된 포인터로, 가리키는 대상의 값을 변경할 수 없을 뿐만 아니라, 가리키는 대상 자체도 변경할 수 없습니다.
그래서, const 포인터처럼 정의 시, 포인터가 가리키는 대상의 주소로 초기화되어야 하고, 한번 초기화되면 그 값을 변경할 수 없습니다.
struct Object{
int m_Data{};
};
int main(){
Object obj{ 10 };
const Object* const pConstPtrToConst{ &obj }; // 초기화
*pConstPtrToConst += 10; // error !
Object obj2{ 20 };
pConstPtrToConst = &obj2; // error !
}
정리
- 포인터의 타입 앞에 const를 사용하면, 대상 객체의 값을 변경할 수 없는 포인터( pointer to const )가 됩니다.
- 포인터의 이름 앞에 const를 사용하면, 대상 객체를 변경할 수 없는 const 포인터가 됩니다.
'C++' 카테고리의 다른 글
| [C++] 자동으로 변수의 타입을 완성하는 auto 키워드 (0) | 2025.02.24 |
|---|---|
| [C++] 클래스 생성자의 멤버 초기화 리스트( member initializer list ) (0) | 2025.02.20 |
| [C++] 객체를 변경할 수 없는 const 멤버 함수 (0) | 2025.02.17 |
| [C++] 항상 컴파일 시에 평가되어야 하는 consteval 함수 (0) | 2025.02.15 |
| [C++] 상수 표현식에 사용될 수 있는 constexpr 함수 (0) | 2025.02.13 |
