소년코딩

this 포인터

객체 지향 프로그래밍에서 가장 많은 질문 중 하나는 "클래스의 멤버 함수를 호출할 때 C++는 어떻게 호출할 객체(인스턴스)를 찾는가?" 이다. 이 질문에 대한 정답은 this라는 숨겨진 포인터를 사용한다는 것이다.

this를 자세히 알아보자:

class Simple
{
private:
    int m_ID;

public:
    Simple(int id)
    {
        SetID(id);
    }

    void SetID(int id)
    {
        m_ID = id;
    }

    int GetID()
    {
        return m_ID;
    }
};

위 클래스를 이용한 간단한 프로그램:

int main()
{
    Simple simple(1);
    simple.SetID(2);
    cout << simple.GetID() << endl; // 2

    return 0;
}

simple.SetID(2);를 호출하면 C++은 SetID() 멤버 함수가 객체(인스턴스) simple에서 작동해야 한다는 것을 알고 m_IDsimple.m_ID를 참조한다. 이 과정이 어떻게 작동하는지 살펴보자.


숨겨져 있는 this 포인터

simple.SetID(2);

멤버 함수 setID()는 하나의 인수만 가지고 호출하는 것처럼 보이지만 실제로는 두 개의 인수를 가지고 있다! 이 코드를 컴파일하면 다음과 같이 변환된다.

simple.SetID(&simple, 2); // 이렇게 함수 인수가 변환된다.

즉, 멤버 함수의 인수로 객체(인스턴스)의 주소가 전달된다는 것이다.

멤버 함수 호출에 매개 변수가 추가되었으므로 멤버 함수의 정의 역시 매개 변수를 하나 더 받도록 수정되어야 한다.

void SetID(int id)
{
    m_ID = id;
}

위 멤버 함수의 정의는 컴파일러에 의해 다음과 같이 변환된다:

void SetID(Simple* const this, int id) // 이렇게 함수 정의가 변환된다.
{
    this->m_ID = id;
}

컴파일할 때 컴파일러는 this를 함수에 추가한다. 이 this 포인터는 멤버 함수가 호출된 객체의 주소를 가리키는 숨겨진 포인터다.

멤버 함수 안에서 모든 클래스의 멤버 변수의 참조 또한 this->가 추가되는 형식으로 변환되므로 위 코드에서는 m_ID에 대한 참조가 this->m_ID로 변환되었다. 결과적으로 this->m_IDsimple->m_ID가 되는 것이다.

- 컴파일러 수행 과정 요약

  1. simple.SetID(2); 를 호출하면 컴파일러는 실제로 simple.SetID(&simple, 2); 로 변환해서 호출한다.
  2. setID() 멤버 함수 내부에서 'this' 포인터는 simple 객체의 주소를 가리킨다.
  3. SetID() 내부의 모든 멤버 변수 앞에는 this->가 붙는다. 그래서 mID = id; 는 실제로 this->mID = id;로 변환된다.

이 모든 과정은 컴파일러에 의해 자동으로 수행되므로 세세하게 기억하지 않아도 된다. 그러나 기억해야 할 것은 모든 멤버 함수는 함수가 호출된 객체(인스턴스)를 가리키는 this 포인터를 가지고 있다는 것이다.


1. this는 항상 호출된 객체(인스턴스)를 가리킨다.

int main()
{
    Simple A(1); // =Simple A(&A, 1); -> Simple 클래스의 생성자 내부에서: this = &A    
    Simple B(2); // =Simple B(&B, 2); -> Simple 클래스의 생성자 내부에서: this = &B  
    A.SetID(3);  // =A.SetID(&A, 3);  -> 멤버 함수 SetID 내부에서: this = &A 
    B.SetID(4);  // =B.SetID(&B, 4);  -> 멤버 함수 SetID 내부에서: this = &B 

    return 0;
}

지금까지 설명한 내용처럼 this 포인터는 어떤 객체(인스턴스)에서 멤버 함수를 호출했는지에 따라 가리키는 주소가 다르다.

또한, this는 함수 매개 변수에 불과하므로 클래스에 메모리 사용량을 추가하지 않는다. (함수가 실행되는 동안에만 스택에 매개 변수 쌓인다.)

2. 명시적으로 this를 참조해야 하는 경우가 있다.

this를 명시적으로 참조해서 유용할 수 있는 몇 가지 경우가 있다.

첫째: 멤버 변수와 이름이 같은 매개 변수를 가진 생성자(또는 멤버 함수)가 있는 경우: this를 사용해서 구분할 수 있다.

class Something
{
private:
    int data;

public:
    Something(int data)
    {
        this->data = data;
        // this->data는 멤버 변수이고,
        // data는 매개 변수
    }
};

위 예제 코드에서는 멤버 변수의 이름과 매개 변수의 이름이 같다. 이럴때 this-> 를 명시적으로 사용해서 구분할 수 있지만, 멤버 변수 이름에 m_과 같은 접두사를 이용해서 이름 중복을 방지하는 게 좀 더 바람직하다. (여기서 m은 member의 약자를 나타내는 코딩 관행이다.)

3. 멤버 함수 체이닝 기법

둘째로 클래스 멤버 함수가 작업 중이던 객체(인스턴스)를 반환하는 방식이 유용할 때가 종종있다. 이렇게 하면 같은 객체의 여러 멤버 함수를 연속해서 호출할 수 있는 방법이다.

다음 클래스를 보자:

class Calc
{
private:
    int m_Value = 0;

public:
    void Add(int value) { m_Value += value;}
    void Sub(int value) { m_Value -= value;}
    void Mul(int value) { m_Value *= value;}

    int GetValue() { return m_Value; }
}

위 클래스와 함께 5를 더하고 3을 빼고 4를 곱하려면 다음과 같이 해야 한다:

int main()
{
    Calc calc;

    calc.Add(5); // return void
    calc.Sub(3); // return void
    calc.Mul(4); // return void

    cout << calc.GetValue() << endl; // 8
    return 0;
}

그러나 만약 각각의 멤버 함수가 this를 반환한다면 체이닝 기능이 있는 새로운 버전의 Calc를 만들 수 있다.

class Calc
{
private:
    int m_Value = 0;

public:
    Calc& Add(int value) { m_Value += value; return *this; }
    Calc& Sub(int value) { m_Value -= value; return *this; }
    Calc& Mul(int value) { m_Value *= value; return *this; }

    int GetValue() { return m_Value; }
}

이제 Add(), Sub()Mul() 멤버 함수는 *this를 반환한다. 따라서 다음과 같이 프로그래밍할 수 있다.

int main()
{
    Calc calc;

    calc.Add(5).Sub(3).Mul(4);

    cout << calc.GetValue() << endl; // 8
    return 0;
}

코드 3줄을 단 1줄로 바꾸었다!

각 멤버 함수가 호출되면서 calc 객체의 m_Value를 수정하고 *this를 반환함으로써 자기 자신(인스턴스)을 반환한다.

1. calc.Add(5).Sub(3).Mul(4);
2. (calc.Add(5)).Sub(3).Mul(4); // m_Value = 0 + 5
3. (calc.Sub(3)).Mul(4);        // m_Value = 5 - 3
4. calc.Mul(4);                 // m_Value = 2 * 4; => 8

요약

  • this 포인터는 모든 멤버 함수에 추가되는 숨겨진 매개 변수다.
  • 호출된 객체의 주소를 가리키는 상수 포인터다.
  • 상수 포인터 자료형이므로 포인터 자체가 다른 것을 가리키도록 할 수는 없다.
  • 멤버 변수 이름과 멤버 함수의 매개 변수 이름이 같으면 명시적으로 this를 참조해서 구분할 수 있다. (ex: this->data = data;)
  • *this를 반환하는 방식으로 함수 체이닝 기법을 만들 수 있다.

cpp 번역: 이 포스트의 원문은 https://www.learncpp.com/cpp-tutorial/8-8-the-hidden-this-pointer/ 입니다.

댓글 로드 중…

블로그 정보

소년코딩 - 소년코딩

소년코딩, 자바스크립트, C++, 물리, 게임 코딩 이야기

최근에 게시된 이야기