어느 순간 내가 캐스팅에 대해서 제대로 알고있나? 싶어서 C++의 캐스팅을 공부, 정리해보려고 합니다. 잘못된 내용있으면 알려주세요.
이론적으로 업캐스팅은 자식 클래스 객체가 부모 타입으로 변환되는 것.
구현적으로는 부모 타입의 포인터(Base)가 자식 객체(Derived)를 가리키는 것이다.
class Base {
public:
virtual void show() { // 가상 함수 (업캐스팅 후에도 자식 클래스의 메서드가 호출될 수 있음)
std::cout << "Base class\n";
}
void Print()
{
std::cout << "Base Print\n";
}
};
class Derived : public Base {
public:
void show() override { // 부모 클래스의 show()를 재정의
std::cout << "Derived class\n";
}
void additionalMethod() { // 자식 클래스에만 존재하는 메서드
std::cout << "Derived class only method\n";
}
void Print()
{
std::cout << "Derived Print\n";
}
};
Derived derivedObj;
// 업캐스팅: 자식 객체를 부모 타입으로 변환 (이론적 관점)
Base* basePtr = &derivedObj; // 부모 포인터가 자식 객체를 가리키는 것 (구현적 관점)
basePtr->show();
basePtr->Print();
예제 설명을 위해 위 와 같이 코드를 작성.
Base* basePtr = &derivedObj;
이 코드에서 업캐스팅이 진행됬다.
업캐스팅 후 부모 클래스에 정의된 멤버 함수와 변수만 사용 가능하다.
자식 클래스에만 있는 메서드는 사용할 수 없다.
이를 사용하려면 다운캐스팅이 필요.
업캐스팅은 다음과 같은 상황에서 유용하다.
위 예제에서 basePtr->show(); 는 Derived의 show를 호출한다.
(다형성)
이는 가상 함수(virtual function)가 vtable을 통해 실제 객체 타입(Derived)의 메서드를 호출하기 때문이다.
가상 함수가 아니라면 위 예제에서 Print 처럼 동작한다.(부모 클래스의 메서드를 호출)
Print는 가상함수가 아니라서 컴파일 시간에 결정된 함수를 호출.
=> 즉 업 캐스팅을 통해 다형성을 활용하려면 부모 클래스에 가상함수가 정의되어 있고 이를 자식클래스가 override 해야한다.
위 설명에서
업캐스팅 후 부모 클래스에 정의된 멤버 함수와 변수만 사용 가능하다.
자식 클래스에만 있는 메서드(additionalMethod)는 사용할 수 없다.
라고 했는데 부모 클래스에 가상 함수를 선언하고 자식 클래스에 이를 override하면 부모 포인터로 자식 객체의 메서드를 호출할 수 있다.
<부모 클래스에 가상 함수가 없는 경우의 문제점>
다형성(Polymorphism) 미작동
자식 클래스의 재정의된 메서드를 사용할 수 없다.
확장성 문제
부모 클래스의 인터페이스를 통해 자식 클래스의 동작을 일관되게 처리할 수 없기 때문에 코드 유지보수가 어려워짐.
=> 즉 공통된 기능을 바탕으로 다형성을 활용하고 싶다면 부모클래스에서 가상 함수로 선언해야 한다.
가상 함수 테이블은 가상 함수를 호출하기 위해 사용되는 테이블.
클래스당 하나의 vtable이 생성된다.
vtable에는 해당 클래스의 가상 함수들의 포인터 목록이 들어 있다.
vptr는 vtable을 가리키는 포인터다.
각 객체마다 하나의 vptr이 존재하며, 이 포인터는 해당 객체의 클래스에 맞는 vtable을 가리킨다.
객체가 생성될 때 vptr은 적절한 vtable을 가리키도록 설정된다.
단계별 동작
클래스 정의 시 컴파일러는 가상 함수 테이블(vtable)을 생성하고, 가상 함수들이 테이블에 저장된다.
객체 생성 시 각 객체는 내부적으로 vptr을 갖고 있으며, vptr은 해당 클래스의 vtable을 가리키게 초기화한다.
가상 함수 호출 시 vptr이 가리키는 vtable에서 함수 포인터를 검색하여 실제 함수를 호출.
<위 예제 코드로 설명>
클래스 생성 시 Base 클래스의 vtable에는 Base::show() 함수의 주소가 저장.
Derived 클래스의 vtable에는 Derived::show() 함수의 주소가 저장.
객체 생성 시 Derived 객체인 derivedObj는 vptr을 갖고, 이 vptr은 Derived 클래스의 vtable을 가리킨다.
함수 호출 시 basePtr->show(); 가 호출되면 vptr을 통해 vtable에서 Derived::show()의 주소를 찾고 해당 함수를 호출한다.
만약 가상 함수가 없으면 vtable과 vptr이 생성되지 않는다.
->
일반 함수 호출은 컴파일 타임에 결정되기 때문에 vtable을 거치지 않고 바로 호출됨.
업캐스팅 후에도 부모 클래스의 함수만 호출된다.
-> 다형성 적용 불가능.
속도 :
vtable을 사용한 가상 함수 호출은 일반 함수 호출보다 약간 느리다.
vptr을 통해 vtable에서 함수 주소를 검색하고 호출하기 때문에 간접 호출(indirect call)이 발생하지만
대부분의 경우 이 오버헤드는 무시할 수 있을 정도로 작다고 한다.
-> 속도 신경쓰지 말고 잘 활용하면 된다.
오버헤드 :
vtable은 클래스당 하나이기 때문에 메모리 사용량은 크지 않는다.
각 객체마다 vptr이 추가되므로, 객체의 메모리 크기가 약간 증가.
다음에는 다운 캐스팅과 C++에서 사용되는 캐스팅들에 대해서 정리해보겠습니다.
그리고 C#에서도 캐스팅에 대한 것을 정리해보려고 합니다.