암시적인 포인터 this
간단한 예문으로 시작합니다.
이 예문은 한 클래스의 멤버 함수를 호출하는 것을 보여줍니다.
class CSomething{
int m_nValue = 0; // private member
public:
void addValue( int val ){
m_nValue += val;
cout << m_nValue << endl;
}
};
int main(){
CSomething A, B;
A.addValue( 10 );
B.addValue( 20 );
}
이때, CSomething 클래스의 A 객체는 멤버 함수 addValue를 값 10의 인수로 호출합니다.
그리고, B 객체는 값 20의 인수로 호출합니다.
그런데, 함수를 호출하기 전에는 A 객체의 멤버 함수를 호출한다는 것을 알 수 있습니다.
A.addValue 코드가 A 객체를 대상으로 한다는 것을 보여주니까요.
그런데, 함수 안으로 실행 제어가 넘어가면, 어떻게 함수 안에서 어떤 객체의 멤버 함수라는 것을 알 수 있는 걸까요?
이것이 이글의 주제입니다.
한 번쯤 생각해 본 적이 있나요?
이것은 마치 호텔의 방에 들어가는 것과 비슷합니다.
문 밖에 있으면, 문 앞에 붙어 있는 방 번호를 보고 어떤 방인지 알 수 있습니다.
그러나, 방 안으로 들어오면, 방안의 가구들의 종류와 배치는 옆 방과 정말 똑같습니다.
그래서, 방 번호를 알려면, 프런트에 전화를 하거나, 방 열쇠를 방에 들어갈 때 가지고 들어가야 합니다.
그 방 열쇠가 바로 this입니다.
결론부터 말하자면,
멤버 함수의 this는 클래스 포인터 타입의 암시적인 매개변수입니다.
즉, this는 클래스 객체 안에 저장되어 있는, 숨겨둔 멤버 변수 같은 신비로운 존재가 아니고 매개변수일뿐입니다.
그렇기 때문에 클래스 객체의 크기를 잡아먹지도 않습니다.
그리고, 멤버 함수 외의 장소에서 이것과 마주칠 수도 없습니다.
또한, 이 this는 암시적으로 멤버 함수에 전달되기 때문에, 눈에 보이지 않고, 이것이 멤버 함수를 특수한 함수라고 오해하게 만드는 원인이기도 합니다.
우리가 A.addValue 라는 코드를 작성하면, 컴파일러는 다음과 같은 함수를 작성합니다.
static CSomething::addValue( CSomething* const this, int val ){
this->m_nValue += val;
cout << (*this).m_nValue << endl;
}
그리고, 이 함수를 다음과 같이 호출하는 것입니다.
CSomething::addValue( &A, 10 ); // CSomething:: 범위 지정자 사용
CSomething::addValue( &B, 20 );
이렇게, 컴파일러는 호텔 방에 들어가기 전에, 방을 알려줄 객체를 포인터 타입으로 전달하는 것입니다.
그리고, 이 함수는 클래스라는 범위( scope )를 가진, 정적 멤버 함수로 구현되었습니다.
만약, 정적 멤버 함수( static member function )로 구현하지 않으면, 클래스의 private 멤버 변수에 접근할 수 없습니다.
또한, this는 클래스 객체의 포인터이므로, -> 연산자와 역참조( dereference ) 연산자 * 를 통해 객체의 멤버 변수에 접근할 수 있습니다.
this->m_nValue += val; // -> 연산자
cout << (*this).m_nValue << endl; // 역참조 연산자
그리고, 이러한 this를 암시적으로 만든 것은, 어떤 거대한 이유가 있는 것이 아니리, 단지 코드가 복잡해지기 때문입니다.
만일, 우리가 아래와 같이 코드를 작성하면, 컴파일러가 위와 같이 다시 작성해 줍니다.
m_nValue += val;
cout << m_nValue << endl;
이러한 매개변수 this는 컴파일러가 작성하기 때문에, 이 포인터가 잘못된 대상을 가리킬 수 없습니다.
그리고, 함수를 호출하기 전에 반드시 클래스 객체로 초기화됩니다.
그럼, 왜 "참조 변수( l-value reference )를 사용하지 않았을까"라는 의문이 들 수 있습니다.
이것도 의외로 간단한데, 이러한 멤버 함수를 호출하는 방식을 정했을 때는 아직 참조 변수 타입이 존재하지 않았기 때문입니다.
참고로, 언어의 후발 주자인 java나 C#은 this의 타입이 참조 타입입니다.
this의 const 지정자
위의 addValue 같은 비-상수( non-const ) 멤버 함수의, this 포인터의 정확한 타입은 클래스 명 * const 타입입니다.
static CSomething::addValue( CSomething* const this, int val );
즉, 이 this 포인터는 가리키는 대상이 상수 속성을 가진 것이 아니라, 가리키는 대상을 변경할 수 없는 const 포인터입니다.
const 포인터에 대한 자세한 내용은 여기에서 볼 수 있습니다.
만일, 상수 멤버 함수를 호출한다면, this의 타입은 어떻게 될까요?
class CSomething{
int m_nValue = 0; // private member
public:
void addValue( int val ){
m_nValue += val;
cout << m_nValue << endl;
}
int getValue() const { // 상수 멤버 함수
return m_nValue;
}
};
int main(){
CSomething A, B;
A.addValue( 10 );
B.addValue( 20 );
int value = A.getValue();
}
컴파일러는 상수 멤버 함수 getValue를 다음과 같이 변경하여 생성할 것입니다.
// 상수 멤버 함수의 this 타입
static int CSomething::getValue( const CSomething* const this, int val ){
return this->m_nValue;
}
이때의 this의 타입은 const CSomething * const 이 됩니다.
그럼, 아래와 같이 상수 객체가 비-상수 멤버 함수인 addValue를 호출하면 어떻게 될까요?
const CSomething C; // 상수 객체
C.addValue( 30 ); // 비-상수 멤버 함수. error !
int value = C.getValue(); // 상수 멤버 함수. ok
▼출력
error: passing 'const CSomething' as 'this' argument discards qualifiers [-fpermissive]
컴파일러는 객체 C를 addValue 함수에 CSomething) * const 포인터로 전달하려고 할 것입니다.
그런데, 이 객체는 상수이므로, (const CSomething) * const 타입으로 전달됩니다.
그래서, 위와 같은 오류 메시지가 발생하는 것입니다.
결론은, 상수 객체는 비-상수 멤버 함수를 호출할 수 없다는 것입니다.
하지만, 반대로 비-상수 객체의 상수 멤버 함수 호출은 합법입니다.
( CSomething 타입의 포인터는 const CSomething 타입의 포인터를 매개변수로 하는 함수에 전달할 수 있습니다 )
this를 통한 연쇄 함수 호출( chained function call )
멤버 함수를 작성하면서, 직접 this를 사용할 일은 거의 없습니다.
그중의 하나가, 매개 변수와 멤버 변수의 이름이 똑같을 때입니다.
class CNumber{
double data = 0; // 멤버 변수
public:
CNumber( double data){ // 매개 변수
this->data = data;
}
};
생성자 CNumber에서 등호 왼쪽의 data는 멤버 변수입니다.
그리고, 등호 오른쪽의 data는 매개 변수입니다.
이 경우 이름이 겹치게 되므로, 위와 같이 this를 사용해서 구분해야 합니다.
그리고, 연쇄 호출이 가능한 멤버 함수를 만들고자 할 때도 this를 사용합니다.
class CNumber{
double data = 0; // 멤버 변수
public:
CNumber( double data){ // 매개 변수
this->data = data;
}
CNumber& operator +( double val){
data += val;
return *this; // CNumber 타입의 참조를 반환
}
CNumber& operator -( double val){
data -= val;
return *this;
}
CNumber& operator *( double val){
data *= val;
return *this;
}
double getValue() const {
return data;
}
};
위에 설명한 것과 같이, this는 CNumber 타입의 포인터입니다.
따라서, CNumber 타입의 참조를 반환하려면 *this 로 반환할 수 있습니다.
이렇게 작성한 멤버 함수는 다음과 같이 사용할 수 있습니다.
int main(){
CNumber A( 10 );
CNumber B = (( A + 10 ) - 25) * 3;
cout << B.getValue() << endl;
}
▼출력
-15
위의 예문이 어떻게 작동하는 걸까요?
먼저, A + 10을 처리하기 위해 CNumber의 + 연산자가 호출됩니다.
그리고, 이 연산자는 암시적인 매개변수인 A의 포인터를 참조( *this )로 반환합니다.
따라서, ( A + 10 )은 표현식 A로 치환되고, 그다음 ( A - 25 )를 처리합니다.
그리고, 이 식도 다시 A로 치환되고, 마지막 A * 3을 계산하기 위해 * 연산자를 호출하는 것입니다.
이러한 방식을 연쇄 함수 호출이라고 합니다.
이러한 호출의 대표적인 예가 std::cout에 데이터를 출력하기 위한 삽입 연산자 << 가 있습니다.
int main(){
int iVal = 10;
char cVal = 'b';
double dVal = 3.14;
const char* pstr = "this is a string";
cout << iVal << cVal << dVal << pstr << endl; // 연쇄 함수 호출
}
이 글과 관련된 글들
클래스의 멤버 함수 포인터 (member function pointer)
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 함수 템플릿( function template )에 대한 설명 2 (0) | 2024.10.13 |
---|---|
[C++] 함수 템플릿 ( function template )에 대한 설명 1 (1) | 2024.10.13 |
[C++] 함수 삭제( = delete )를 통한 기능 제어 (1) | 2024.10.05 |
[C++] 사용자 정의 타입( user defined type )을 다른 타입으로 변경하기 (0) | 2024.10.01 |
[C++] 상수 좌측값 참조( l-value reference to const )의 성질 (0) | 2024.09.29 |