C++의 this
this의 실체와 nullptr->fun()이 동작하는 상황에 대한 정리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass
{
public:
void PrintHello()
{
cout << "Hello\n";
}
};
int main()
{
MyClass* ptr = nullptr;
ptr->PrintHello();
}
위 코드, 당연히 에러가 발생할 것 같지만 대부분 “Hello”가 출력됩니다. 분명, 실체화되지 않은 인스턴스에 접근한 것 같지만 명령이 수행되는 것입니다.
멤버 변수
먼저, 멤버 함수는 인스턴스가 각각 소유하는 것이 아니라, 메모리의 코드 영역에 위치합니다. Obj a, b로 선언하더라도 멤버 함수는 코드 영역에 하나만 존재합니다. 코드는 런타임에서 변경되지 않는 값이기 때문에 재사용이 가능하기 때문입니다.
또한, 코드 영역에 위치한다는 것은 인스턴스의 데이터와 완전히 다른 영역에 존재하는 것을 의미합니다. 함수를 호출하는 것 만으로는 아직까지 인스턴스의 멤버 변수에 접근하지 않았다는 것입니다.
그럼 인스턴스들은 각각의 데이터를 갖고 있는데, 동일한 명령어를 가져와서 사용한다면, 해당 함수가 누구의 데이터를 가져와야 하는지 구분할 수 있을까요?
this
non-static 멤버 함수가 호출되면, 현재 객체의 주소가 자동으로 같이 전달됩니다. 예를 들어,
1
2
PrintValue(...) // 클래스 a에서 호출
this = &a
1
2
PrintValue(...) // 클래스 b에서 호출
this = &b
같은 형태가 됩니다. 이를 통해서, 어떤 인스턴스가 함수를 호출했는지 구분할 수 있는 것입니다.
또한, this는 지금 이 멤버 함수를 호출한 객체를 가리키는 포인터입니다.
1
2
3
4
void PrintThis()
{
cout << this << '\n';
}
따라서, 이 경우에는 객체의 데이터를 읽는게 아니라 포인터 값 자체가 출력됩니다.
nullptr인 객체 포인터로 멤버 함수 호출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass
{
public:
void PrintHello()
{
cout << "Hello\n";
}
};
int main()
{
MyClass* ptr = nullptr;
ptr->PrintHello();
}
위 코드는 개념적으로는 이렇게 흘러갑니다.
1
2
3
4
5
6
7
8
9
ptr
↓
nullptr
ptr->PrintHello()
↓
PrintHello 함수 실행
↓
this = nullptr
여기서 중요한 포인트는, 함수의 코드와 객체의 데이터는 별개라는 점입니다. PrintHello()의 코드는 이미 코드 영역에 존재하고 있습니다. 그래서 실제로 컴파일된 결과에서 this를 굳이 역참조하지 않는 함수라면
1
2
3
4
5
this = nullptr
↓
this를 사용하지 않음
↓
PrintHello 코드 실행
이런 식으로 별문제 없이 실행될 수 있는 것입니다.
여기서 중요한 점은 ->이 단순하게 ptr이 가리키는 객체로 이동한 다음, 그 객체의 PrintHello를 찾아서 실행하는게 아니라는 것입니다. non-virtual 멤버 함수의 경우, 컴파일러는 어떤 함수를 호출해야 하는지 컴파일 시점에 이미 알고 있는 경우가 많기 때문입니다.
즉 ptr->PrintHello();를 실행할 때, ptr이 가리키는 객체의 메모리를 읽어서 PrintHello()의 주소를 찾아낼 필요가 없다는 것입니다.
중요한 것은 정상적인 출력처럼 보이지만, 그렇지 않다는 것입니다. 이는 C++에서 미정의 동작이며, 정상작동을 보장하지 않습니다.
멤버 변수에 접근하는 순간 문제 발생
함수까지는 코드 영역에 이미 기계어 형태로 메모리에 올라와 있는 상태이기 때문에 호출할 수 있었지만, 멤버 변수는 메모리에 올라와있지 않습니다. 따라서, 멤버 변수에 접근하는 순간 문제가 발생합니다.
함수 인자로 다른 객체의 포인터를 넘기는 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class B
{
public:
int value = 10;
};
class A
{
public:
void Foo(B* other)
{
cout << other->value << '\n';
}
};
이렇게 호출한다고 해봅시다.
1
2
3
4
A* a = nullptr;
B b;
a->Foo(&b);
Foo() 내부에서는 개념적으로
1
2
3
4
5
6
7
8
9
this
↓
nullptr
other
↓
&b
↓
정상적인 B 객체
가 됩니다. 동일한 원리로 예외가 발생하지 않습니다.
다형성 함수 (virtual)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base
{
public:
virtual void Hello()
{
cout << "Base\n";
}
};
class Derived : public Base
{
public:
void Hello() override
{
cout << "Derived\n";
}
};
virtual 키워드를 사용하는 함수는 어떨까요? 일반적으로 다형성이 구현된 함수를 호출할 때에는 인스턴스의 vptr를 읽어야 합니다. 그런데 ptr이 nullptr이므로
1
2
3
4
5
6
7
nullptr
↓
vptr 읽기
↓
객체 메모리 접근
↓
접근 위반 가능
같은 순서로 수행되며, 즉시 문제가 생길 가능성이 훨씬 높습니다.



