클래스의 멤버 함수 안에서만 사용할 수 있는 포인터

클래스 이름에 *가 붙은 포인터 타입 (ClassName*)으로
클래스의 비정적 멤버 함수 내부에서 사용가능하다.
this값 자체를 함수 내부에서 바꿀 수 없으므로 상수 포인터(ClassName* const)의 성질을 갖는다.
값으로 호출한 객체의 주소를 가지고있다.

A* a = new A(); 
a->func();        // func(A* this)

A** _a = &a;
(*_a)->func();     // func(A* this)

A*** __a = &_a;
(**__a)->func();// func(A* this)

                // this는 `클래스 이름`의 포인터 이다 
                // 또한 this의 기준은 `함수` 기준이다.

개체 자체의 일부가 아니므로 sizeof 에 포함되지 않는다.


일반적으로 레지스터로 전달되고 필요 시 스택에 저장할 수도 있음. (디버그, 최적화 비활성화, 호출 규약, 레지스터 모자라면(spill))

컴파일러는 this처럼 자주사용하는 값을 레지스터에 올리고 사용해 최적화한다. 레지스터 자리가 여유가 있으면 짧은 생존 범위의 변수도 전달되기도 한다.

  • 지역 변수(local variable)
  • 임시 값(temporary)
  • 함수 인자
  • this, 포인터 등
    int foo(int a, int b) 
    {
      int x = a + b;
      int y = x * 2;
      return y;
    }

힙 변수는 메모리 주소만 레지스터에 올릴 수 있고 실제 데이터는 힙 영역에 존재한다.

int* p = new int(42);
int x = *p + 1;

p는 레지스터 상주 가능, *p 값을 읽기 위해서 메모리 접근이 필요하고, 그 값을 레지스터에 잠시 load하고 사용할 수 있음.

  • const 멤버 함수에서 thisconst ClassName* 으로 동작한다.

  • this 는 호출 시점에 컴파일러에 의해 레지스터 또는 스택(ABI마다 다름)에 인자로 전달되는 값이다.

      class Player 
      {
      public:
          int hp;
          void damage(int value) { hp -= value; }
      };
    
      int main() 
      {
          Player p1;
          p1.hp = 10;
          p1.damage(5);
      }
    
      // 컴파일 시점에 컴파일러에 의해 
      // 아래와 같이 코드가 변환된다고 생각하면됨. (실제 코드아님)
      void damage(Player* this, int value) { this->hp -= value; }
    
      int main() 
      {
          Player p1;
          p1.hp = 10;
          damage(&p1, 10); // this로 &p1을 전달
      }

    디셈블리 했을 때

      Player p;
      lea         rcx,[p]  // this 포인터
      call        Player::Player (07FF64997172Bh)  
      p.damage(10);
      mov         edx,0Ah  
      lea         rcx,[p]  // this 포인터
      call        Player::damage (07FF649971730h)  
      public:
          int hp = 100;
          void damage(int value) { hp -= value; }
       mov         dword ptr [rsp+10h],edx  ; int value
       mov         qword ptr [rsp+8],rcx    ; this 전달 레지스터, 
                                              디버그 목적으로 저장됨.
                                              디버깅을 위한 강제 메모리화
       ... 생략
       mov         rax,qword ptr [this]     ; this 포인터 주소를 rax에 로드
       mov         ecx,dword ptr [value]    ; value 값을 ecx에 로드
       mov         eax,dword ptr [rax]      ; rax(this) 객체의 hp값 로드
       sub         eax,ecx                  ; hp -= value 계산
       mov         rcx,qword ptr [this]     ; 다시 rcx에 this 로드
       mov         dword ptr [rcx],eax      ; 계산된 값 hp에 저장
       ... 생략

    Windows x64 기준으로 thisrcx(함수 호출 첫 번째 인자) 레지스터로 전달됨.
    멤버 함수 실행에서만 인자로 전달 받는다.

    this는 따로 저장되어지는 값이 아님.


ABI(Application Binary Interface)

컴파일이 끝난 바이너리들이 서로 호출하고 데이터를
교환하는 방식에 대한 규약

함수 호출 규약, 자료형 크기 및 정렬, Name mangling, 객체 레이아웃, RTTI 등
MSVC : MSVC C++ ABI
GCC / Clang : Itanium C++ ABI


this는 전달인자로써 만들어진 객체의 타입에 영향을 받는다

    class Player
    {
    public:
        const int constValue = 100;
        // 컴파일러에 의해 인자로써 추가된 this
        void func(const Player* this, int value) const { cout << this->constValue + value << '\n'; }
    };

    int main()
    {
        const Player p1;
        p1.func(10);

        return 0;
    }

  • 멤버 함수 타입에 따라 this 의 타입도 달라진다.
    const 멤버 함수 가 호출되면 const Class* 타입의 this 가 전달된다.
멤버 함수 선언 this로 명명된 class에 대한 myClass 포인터의 형식
void Func() myClass *
void Func() const const myClass *
void Func() volatile volatile myClass *
void Func() const volatile const volatile myClass *


전역함수와 정적 함수에는 this 가 없다.

정적 함수는 객체가 아닌 클래스 범위에 속하며
객체 관리 함수가 아니라서 this를 인자로 전달할 수 없기 때문이다.
클래스 정적 멤버에는 접근 가능하지만 객체 멤버에 접근할 수 없다.

friend 함수는 접근 권한만 부여받을 뿐,
멤버 함수가 아니므로 this는 존재하지 않는다.


멤버 함수 내부에서 delete this

객체 스스로 수명을 끊는 행위.
해당 객체가 반드시 Heap에 할당되었고, 삭제 후 멤버에 접근하지 않는다면 가능하다.

사용에 신중해야 한다.


nullptr this 호출

class A
{
public:
    void func() { cout << "A\n"; }
}
>
A* a = nullptr;
a->func();

결과는 에러없이 A가 출력된다.
내부에서 this포인터를 사용하고 있지 않기 때문에 가능함.

a->func()는 내부적으로 func(a)와 같이 변환된다.
anullptr이라도 func() 함수 주소는 이미 컴파일 타임에 결정되어 있다.
CPU가 그냥 func() 주소로 점프하고 내부에서 this->멤버변수 시도가 없기 때문에
결과적으로 nullptr을 역참조하는게 아님

그래도 유효하지 않은 this 포인터를 넘겼으니가 사용하면 안된다.


Method chaining

this 반환을 통해 연속으로 멤버 함수를 호출 할 수 있다.

class Player {
    int hp = 100;
public:
    Player& damage(int v) { 
        hp -= v; 
        return *this; 
    }
    // 참조로 반환해야 원본 객체를 사용할 수 있음
    Player& heal(int v) { 
        hp += v; 
        return *this; 
    }
};

int main() {
    Player p;
    p.damage(10).heal(5);  // 멤버 함수 체이닝
}

이렇게 되면 불필요한 복사 없이 두번의 함수 호출이 가능해진다.

이런 특성이 연산자에서 사용 된다.

Player& operator=(const Player& other) 
{
    if (this != &other) // 자기 자신을 대입할 때 
                        // 발생할 수 있는 자원 해제를 막음
                        // 자기 대입 시 기존 자원을 지워버리면
                        // 복사할 원본 데이터가 사라진다.
    {
        hp = other.hp;
    }
    return *this;
}

Player a, b, c;
a = b = c;  // b = c 먼저 수행, 그 결과 반환값(a의 참조)으로 a = (b = c)

Thunk(this adjustment)

Thunk(청크/썽크)는 가상 함수 호출 시 this 포인터를 적절한 베이스 클래스 위치로 조정하는 코드를 실행한다.
실제 함수로 가기 전, this 레지스터에 offset을 더하거나 빼는 중간 다리 역할.

class Base1
{
public:
    int b1;
};

class Base2
{
public:
    int b2;
};

class Derived : public Base1, public Base2
{
public:
    int d;
};

다중 상속에서 Derived객체가 Base1Base2를 상속받을 때
메모리 구조상 Base2 서브오브젝트는 Derived객체의 시작점으로부터 일정 offset만큼 뒤에 배치된다.

MSVC / Itanium ABI에서 다중 상속 시 파생 클래스는 상속 받는 첫번째 요소와 시작주소가 같아진다(offset 0).

class D : B, C {}; // D == B, C
class D : C, B {}; // D == C, B

class A {};
class B : public virtual A{};
class C : public virtual A{};
class F{};
class D : public F, public B, public C {};
// A == B, C, D == F
혹은
class D : public F, public C, public B {};
// A == C, B, D == F

시작주소가 같은경우 Base1
Derived객체의 시작주소와 Base1 서브오브젝트의 시작주소가 일치, this 조정없음

시작주소가 다른 경우 Base2 (Thunk)
Base2*타입 포인터로 Derived객체를 가리키면, 컴파일러는 포인터에 offset을 더해
Base2서브오브젝트의 시작 위치를 가리키게함.

NormalDerived* nd = new NormalDerived();
// 컴파일러가 Base2인 b2를 NormalDerived의 시작점으로부터 sizeof(Base1)만큼 더함
Base2* b2 = (Base2*)nd;
mov rax, qword ptr [nd]  ; 1. nd(객체 시작 주소)를 rax에 로드
add rax, 4               ; 2. rax에 '4'를 더함 (컴파일 타임에 결정된 상수값)
mov qword ptr [rbp+1B8h], rax
...
mov qword ptr [b2], rax  ; 3. 결과 주소를 b2 포인터에 저장

함수 진입 전 변환

가상 함수 호출 시 컴파일러는 객체의 vptr을 통해 vtable에서 함수 포인터를 가져온다.
다중 상속이 있는 경우 vtable에서 실제 함수 주소 대신
this 포인터를 보정하는 Thunk 함수를 가리킬 수 있다.
Thunk가 this 포인터에 필요한 오프셋을 더하거나 빼서
올바른 서브 객체를 가리키도록 한 뒤 실제 가상 함수를 호출한다.

this
 ↓
vptr 접근(원래 this로)
 ↓
vtable에서 thunk 가져옴
 ↓
thunk 실행
 ↓
this 보정
 ↓
real virtual function 호출

Offset을 직접 더해보기

class A
{
public:
    virtual void afunc() {}
    void afunc1() {}
};

class B
{
public:
    virtual void bfunc() {}
};

class AB_Derived : public A, public B
{
public:
    virtual void afunc() { cout << "afunc()\n"; }
    virtual void bfunc() { cout << "bfunc()\n"; }

    virtual void abfunc() { cout << "abfunc()\n"; }
    virtual void abfunc1() { cout << "abfunc1()\n"; }

};
AB_Derived* ab = new AB_Derived();
long long* vptr1 = (long long*)ab;
long long* vtable1 = (long long*)*vptr1;
cout << "객체 주소 : " << (void*)ab << '\n';
cout << "vtable 주소 : " << (void*)vtable1 << '\n';
cout << "vtable[0] " << (void*)vtable1[0] << '\n';
cout << "vtable[1] " << (void*)vtable1[1] << '\n';
cout << "vtable[2] " << (void*)vtable1[2] << '\n';

// B는 offset을 따로 구해준다.
long long* vptr2 = (long long*)((char*)ab + sizeof(A));
long long* vtable2 = (long long*)*vptr2;
cout << "vtable[3] " << (void*)vtable2[0] << '\n';

typedef void(*FuncPtr)();
FuncPtr f1 = (FuncPtr)vtable1[0];
FuncPtr f2 = (FuncPtr)vtable1[1];
FuncPtr f3 = (FuncPtr)vtable1[2];
FuncPtr f4 = (FuncPtr)vtable2[0];

f1();
f2();
f3();
f4();
객체 주소 : 000001F8EE796050
vtable 주소 : 00007FF72285CEA0
vtable[0] 00007FF72285147E
vtable[1] 00007FF72285133E
vtable[2] 00007FF722851276
vtable[3] 00007FF722851046
afunc()
abfunc()
abfunc1()
bfunc()

가상 상속 시 Thunk 동작

가상 상속에서는 가상 기반 클래스를 찾아가기 위한 vbptr이 사용된다.
이전의 this는 컴파일러가 offset을 포인터에 더해 서브오브젝트의 시작 위치를 가리키게 했다.
가상 기반의 경우 컴파일 타임에 위치를 확정할 수 없으므로

런타임에 서브오브젝트까지의 offset을 vbptr → vbtable → offset 과정을 거쳐 구하고,
this 포인터를 조정한다.

class VirtualBase
{
public:
    int vb;
    virtual void func() {}
};

class Middle1 : virtual public VirtualBase
{
public:
    int m1;
};

class Middle2 : virtual public VirtualBase
{
public:
    int m2;
};

class VirtualDerived : public Middle1, public Middle2
{
public:
    int vd;
};
[VirtualDerived  오브젝트 객체 구조 주소: 000002994B44C1D0 (크기: 56)]
  +0 bytes: 00007FF738890270
  +8 bytes: 0000000000000000
  +16 bytes: 00007FF738890288
  +24 bytes: 0000000000000000
  +32 bytes: 0000000000000000
  +40 bytes: 00007FF738890268
  +48 bytes: 0000000000000000
------------------------------------------
주소 비교 :
VirtualDerived  ptr:  000002994B44C1D0
Middle1 ptr: 000002994B44C1D0 (Offset: 0)
Middle2 ptr: 000002994B44C1E0 (Offset: 16)
VirtualBase ptr:     000002994B44C1F8 (Offset: 40)

VirtualDerivedMiddle1은 주소가 같지만, Middle1, Top의 주소는 다르게 출력된다.
가상 상속된 VirtualBaseVirtualDerived객체의 가장 끝부분에 위치하게 되므로 offset 차이가 크다.

Middle1Middle2 영역 안에는 각각 vbptr이 들어있고 vbtable에는
자기 자신으로 부터 VirtualBase까지 떨어져있는 바이트 수가 적혀있다.

또한 가상 상속의 경우 최종 파생 클래스로부터 가상 기반 클래스 까지의 offset이 런타임에 결정 되기때문에
기존의 컴파일 타임에 계산되던 offset이 런타임에 계산된다.

VirtualDerived* vdp = new VirtualDerived();
// VirtualBase의 위치는 객체 레이아웃의 가장 뒤쪽에 위치, vbptr을 참조하여 런타임에 결정.
VirtualBase* vb = (VirtualBase*)vdp;
mov rax, qword ptr [vdp]    ; 1. vdp(객체 시작 주소)를 rax에 로드
mov rax, qword ptr [rax]    ; 2. rax가 가리키는 곳(vbptr)의 값을 읽어 vbtable 주소를 rax에 로드
movsxd rax, dword ptr [rax+4] ; 3. vbtable의 +4 위치에서 실제 오프셋 값을 읽어옴 (메모리 참조)
mov rcx, qword ptr [vdp]    ; 4. 다시 vdp 시작 주소를 rcx에 로드
add rcx, rax                ; 5. 읽어온 동적 오프셋(rax)을 시작 주소에 더함
mov rax, rcx
...
mov qword ptr [vb], rax     ; 6. 최종 결정된 주소를 vb 포인터에 저장

결과적으로 C++은 this 포인터 시스템을 통해
함수 코드를 객체마다 따로 복사하지 않고 단 하나만 유지하면서 여러 객체가 공유할 수 있게 한다.
코드는 한 곳에 두고 인자(this)만 바꿔서 어떤 객체의 데이터를 다룰지 결정하니까 메모리가 절약된다.


문제

10 문제
https://gemini.google.com/share/408b4a2b49e4
또 다른 10문제
https://gemini.google.com/share/4e0a3c9c5ff3

https://learn.microsoft.com/ko-kr/cpp/cpp/this-pointer?view=msvc-170
https://www.en.cppreference.com/w/cpp/language/this.html

내용에 대한 질의나, 수정 요청은 저에게 큰 도움이 됩니다.

'C\C++' 카테고리의 다른 글

Scope  (0) 2026.04.01
Object  (0) 2026.04.01
표현식  (0) 2026.04.01
포인터  (0) 2026.04.01
Virtual  (0) 2026.02.03