virtual
예약어를 앞에 붙여서 선언한 메서드를 말한다.// 기본 클래스
class CMyData {
public:
CMyData() { m_pszData = new char[32]; }
~CMyData() {
cout << "~CMyData()" << endl;
delete m_pszData;
}
private:
char *m_pszData;
};
// CMyData의 파생 클래스
class CMyDataEx {
public:
CMyDataEx() { m_pnData = new int; }
~CMyDataEx() {
cout << "~CMyDataEx()" << endl;
delete m_pnData;
}
private:
int *m_npnData;
};
int main(int argc, char* argv[]) {
CMyData *pData = new CMyDataEx;
// 참조 형식에 해당하는 소멸자(CMyData::~CMyData())가 호출된다.
delete pData;
return 0;
}
위와 같은 경우에 참조 형식에 해당하는 소멸자(CMyData::~CMyData()
)만 호출되므로, CMyDataEx
의 멤버 변수인 *m_pnData
의 메모리는 해제되지 않는다.
virtual ~CMyData() {
cout << "~CMyData()" << endl;
delete m_pszData;
}
기본 클래스의 소멸자를 가상화하면 파생 클래스의 소멸자까지 제대로 호출된다.
class CMyData {
public:
CMyData() {
cout << "CMyData()" << endl;
}
virtual ~CMyData() { }
virtual void TestFunc1() { }
virtual void TestFunc2() { }
};
class CMyDataEx: public CMyData {
public:
CMyDataEx() {
cout << "CMyDataEx()" << endl;
}
virtual ~CMyDataEx() { }
virtual void TestFunc1() { }
virtual void TestFunc2() {
cout << "TestFunc2()" << endl;
}
}
int main(int argc, char* argv[]) {
CMyData *pData = new CMyDataEx;
pData->TestFunc2();
delete pData;
return 0;
}
this
포인터 아래에는 __vfptr
이라는 지역변수가 존재한다.CMyDataEx
의 생성자가 호출되면 이어서 기본 클래스인 CMyData
의 생성자가 호출된다.CMyData
의 생성자가 실행되면 __vfptr
은 CMyData
내부에 정의된 두 가상함수의 주소를 가리키게 된다.CMyDataEx
의 생성자가 실행되면 __vfptr
은 CMyDataEx
내부에 재정의된 두 가상함수의 주소값으로 덮어씌워진다.CMyData
인 변수 *pData
의 vtable은 실 형식인 CMyDataEx
의 가상 함수를 가리키고 있으므로 CMyDataEx::TestFunc2()
가 호출되는 것이다.'순수 가상 클래스'는 '순수 가상 함수'를 멤버로 가진 클래스를 의미한다.
'순수 가상 함수'는 정의부가 생략된, 즉 선언부만 존재하는 함수를 의미한다.
= 0
을 꼭 넣어줘야 한다.virtual int GetData() const = 0;
순수 가상 클래스는 인스턴스를 직접 생성할 수 없으며(상속을 통해 파생 클래스의 인스턴스를 생성할 수 있음), 파생 클래스는 기본 클래스의 순수 가상 함수를 반드시 재정의 해야한다.
맥락상 자바의 Abstract Class 내지는 Interface 정도로 이해하면 될 듯 하다.
const_cast<>
: 상수형 포인터에서 const
를 제거한다.static_cast<>
: 컴파일 시 상향 혹은 하향 형변환한다.dynamic_cast<>
: 런탐임 시 상향 혹은 하향 형번환한다.reinterpret_cast<>
: C의 형변환 연산자와 비슷하다. (??)상속 관계일 때 파생 형식을 기본 형식(부모 클래스의 형식)으로 포인팅할 수 있다. (묵시적인 상향 형변환)
기본 형식 포인터가 가리키는 대상을 파생 형식 포인터로 형변환하는 '하향 형변환'은 상속 관계에서만 가능하다.
하향 형변환 예제
class CMyData {
public:
CMyData() { }
virtual ~CMyData() { }
void SetData(int nParam) { m_nData = nParam; }
int GetData() { return m_nData; }
private:
int m_nData = 0;
};
class CMyDataEx: public CMyData {
public:
void SetData(int nParam) {
if(nParam > 10)
nParam = 10;
CMyData::SetData(nParam);
}
void PrintData() {
cout << "PrintData(): " << GetData() << endl;
}
};
int main(int argc, char* argv[]) {
CMyData *pData = new CMyDataEx;
CMyDataEx *pNewData = NULL;
pData->SetData(15);
// 기본 형식에 대한 포인터지만, 실제로 가리키는 대상은 파생 형식이다.
// 따라서 다음과 같이 파생 형식에 대한 포인터로 '하향 형변환'이 가능하다.
pNewData = static_cast<CMyDataEx*>(pData);
pNewData->PrintData();
delete pData;
return 0;
}
dynamic_cast
가 등장했다는 것은 좋지 못한 방향으로 흘러가고 있다는 증거.dynamic_cast
는 절대 사용하지 말자.기본적으로 모든 연산자는 파생 형식에 자동으로 상속된다. (단순 대입 연산자 제외)
기본 형식에 +
연산자 함수가 정의되어 있는 경우, 함수의 반환값은 기본 클래스의 인스턴스이므로 파생 클래스의 인스턴스에 대입할 수 없다.
다음과 같이 파생 클래스에 +
연산자 함수를 정의하여 해결할 수 있다.
class CMyDataEx: public CMyData {
public:
CMyDataEx(int nParam): CMyData(nParam) { }
CMyDataEx operator+(const CMyDataEx &rhs) {
return CMyDataEx(static_cast<int>(CMyData::operator+(rhs)));
}
};
알맹이는 상위 클래스의 것을 사용하고 인터페이스만 맞춰줄 생각이라면, 다음과 같이 간단하게 해결 가능하다.
class CMyDataEx: public CMyData {
public:
CMyDataEx(int nParam): CMyData(nParam) { }
using CMyData::operator+;
using CMyData::operator=;
}