객체지향프로그래밍(13) - 상속(3), this 포인터

메소드 호출 원리 이해하기

Featured image

🔚 짧게 하는 복습

✅ 1. 클래스와 구조체의 메모리 구조의 공통점과 차이점을 안다.

✅ 2. 상속 시 생성자의 작동원리에 대해 안다.

✅ 3. 상속 시 메소드의 호출 순서에 대해 안다.

혹시 기억이 안 난다면, 다시 돌아가자


저번 시간에 배운 상속 시 메소드 호출 원리는 아래와 같다.

자신의 클래스부터 아래에서 위로, 그런데 저번 강의에서 또 객체는 자신만의 함수 포인터가 없다고 했다.

그렇다면 컴퓨터는 어떻게 메소드를 호출한 객체를 알 수 있을까?

사실 메소드는 숨겨진 매개변수가 있다. 오늘은 이를 알아보겠다.


this 포인터

사실 우리가 정의하는 모든 메소드에는 this라는 포인터형 매개변수가 숨어있다.

무슨 말이냐? 디폴트 메소드부터 사용자 정의 메소드까지 this라는 포인터가 매개변수가 이미 포함되어있다는 뜻이다.

this 포인터메소드를 호출한 객체의 주소를 가리킨다.

이를 명시적으로 볼 방법이 있는데, 아래의 코드를 직접 실행해보자.

여기서 this_check()과 check()은 사실 같은 함수이다.

왜냐하면 check()이라는 메소드 안에도 this라는 포인터가 이미 포함되어있기 때문이다.

사실 이 포인터를 명시적으로 사용할 일은 그렇게 많지는 않은데, 원리는 알 필요가 있으므로 강의에 포함했다.


this 포인터의 명시적인 이용

우선 parameter와 이름과 class의 멤버 변수 이름이 같을 때, this를 통해서 구분할 수 있다.

class human{
private:
    int age;
    double power;
    string name;
public:
    human(string _name) : age(1), power(1.5), name("unknown") {}

    human(int age, int power, string name){
        this->age = age;
        this->power = power;
        this->name = name;
    }

};

물론 ‘이렇게 적을 수 있다’라는 거지, 이렇게 작성하지는 말자.

두 번째, 유의미한 용도는, 객체의 참조자 반환과 함께 체이닝이라는 기능을 구현할 수 있다.

체이닝이란 c.foo().foo1().foo2().foo3() 이렇게 객체 하나에 메소드를 여러 개를 연속 호출하는 방법을 말한다.

참조자 반환 함수와 this 포인터를 이용하면 되는데, 아래의 예제를 보자.

#include <iostream>

class Counter {
private:
    int count;
public:
    Counter() : count(0) {}

    Counter& increment() {
        this->count++;
        return *this; // 현재 객체를 반환하여 체이닝 가능하게 함
    }

    Counter& decrement() {
        this->count--;
        return *this; // 현재 객체를 반환하여 체이닝 가능하게 함
    }

    int getCount() {
        return this->count;
    }
};

int main() {
    Counter c;
    std::cout << "Initial Count: " << c.getCount() << std::endl;

    c.increment().increment().decrement(); // 체이닝을 이용한 메소드 호출
    std::cout << "Final Count: " << c.getCount() << std::endl;

    return 0;
}

참조자 반환이라고 하면 조금 이해가 안 갈 수 있다.

왜냐하면, 참조자 반환은 웬만하면 사용하지 말라고, 앞의 강의에서 다룬 적이 있기 때문이다.

참조자 반환이 문제가 되는 경우는 아래와 같다.

int& increment(int count){
    int temp = count;
    return temp++
}

위의 예와 무엇이 다른지 모르겠다면, temp 변수의 생존 범위를 확인하면 된다.

temp는 함수 스택 프레임에서 선언된 변수다.

스택 프레임에서 선언된 변수는 함수가 반환되면 사라진다.

그렇다면 이 함수에서 반환되는 값은 사실 temp인데, temp가 반환과 동시에 사라진다?

이런 경우가 주된 오류 발생 이유이다.

하지만 위 같은 경우에는 참조자가 참조하는 대상이 사라질 이유가 없기에 문제 거리가 없다.

Counter& decrement() {
    this->count--;
    return *this; // 현재 객체를 반환하여 체이닝 가능하게 함
}

여기서 this가 말하는 것이 무엇이라고 했는가? 바로 호출한 객체 자신의 주소이다.

그러면 this->count–가 진행되면 호출된 객체 자신의 count가 감소한다.

그리고 return *this이니까 호출한 객체 자신의 포인터 간접참조, 어렵게 말했다.

그냥 자신을 반환하는 것이다.

그런데 왜 참조자 반환일까? 만약 Counter decrement()라고 생각하면 무엇이 다를까?

정답은 count–가 줄어든 객체 자신의 복사본이 반환이 된다.

즉 두 번째 메소드부터는 복사본의 메소드 결과를 다시 복사해서 넘기고, 또 반복하고…. 원본 객체는 오직 첫 번째 메소드만 적용되는 것이다.

우리가 원하는 것은 객체 원본에 연속적으로 값을 변화하고 싶은 것이니, 체이닝을 구현하고 싶을 때는 참조자를 통해 원본을 반환해야 한다.


강의를 마치며

위에서 말한 명시적인 this의 활용은 사실 피하는 것이 좋다.

참조자 반환 함수는 조금만 잘못 건드려도 빈번하게 오류가 난다.

함수의 parameter와 클래스의 멤버 변수의 이름을 같게 하는 것 역시 좋은 전략이 아니다.

this를 통한 메소드 호출 원리를 이해하는 것이 목표였기에 의도적으로 예시를 보여준 것일 뿐, 이런 코딩 방법을 일부로 따라 하지는 말자.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 메소드 함수 호출 원리를 안다.

✅ 2. this 포인터의 원리와 쓰임을 안다.

✅ 3. 참조자 반환 함수의 주의할 점과 활용을 안다.

⚠️ this를 명시적으로 사용하거나, 참조자 반환을 사용하는 것은 추천하지 않는다.

💣 과제, 없음