다형성이란?
사전적 의미로는 ‘여러 형태를 갖는 것’ 으로 정의된다. C#, C++, Java 와 같은 객체지향 언어에서는 여러 개의 클래스가 같은 메시지에 대해서 각자 다른 방법으로 작동할 수 있는 능력 이라 할 수 있겠다.
쉽게 말해서,
부모의 추상함수를 자식 클래스들이 오버라이딩 하여, 부모의 인터페이스로 각각의 자식 클래스에 정의된 함수를 호출 할 수 있는 능력이라고 말할 수 있겠습니다.
ex ) 다형성을 통한 ptk 제어 /관리 가능. ( 소스에서 일단 오버라이딩 제외 )
class TimeKeeper
{
public:
TimeKeeper();
~TimeKeeper(); // 현재 가상소멸자가 아니다.
};
class AtomicClock : public TimeKeeper { };
class WaterClock : public TimeKeeper { };
class WristWatch : public TimeKeeper { };
class CTimeFactory
{
public:
static TimeKeeper * CreateAtomic(){ // 팩토리 함수
return new AtomicClock;
}
static TimeKeeper * CreateWater(){ // 팩토리 함수
return new WaterClock;
}
static TimeKeeper * CreateWrist(){ // 팩토리 함수
return new WristWatch;
}
}
void main()
{
TimeKeeper* ptk = CTimeFactory::CreateAtomic();
delete ptk; // 메모리 누수 방지
}
팩토리 함수란?
새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 함수를 의미한다.
위의 코드는 다음과 같은 의미를 갖는다.
1) CTimeFactory::CreateAtomic() 가 반환하는 포인터가 파생 클래스에 대한 포인터라는 점 ( AtomicClock* )
2) 이 포인터가 가리키는 객체가 삭제될 때는 기본 클래스 포인터(TimeKeeper* 포인터)를 통해 삭제되다는 점
3) 기본 클래스에 들어있는 소멸자가 비가상 소멸자라는 점
C++ 규칙에서, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때, 그 기본 클래스에 비가상 소멸자가 들어있으면 프로그램 동작은 어떻게 될지 모릅니다.
보통 그 객체의 파생 클래스 부분이 삭제가 되지 않습니다.
즉, 완전히 제거가 되지 않는다는 것이다.
그럼 이에 대한 해결책,
바로 기본 클래스에 가상 소멸자를 선언해주면 된다. virtual ~TimeKeeper();
보통
그 클래스에 가상함수를 하나라도 가지고 있다면, 가상 소멸자를 가지는 것이 맞다고 봅니다.
가상 소멸자를 가지고 있지 않다면,
이는 어떤 클래스의 기본 클래스로 쓰이는 경우가 없다고 이해하면된다.
그렇다고, 가상소멸자, 가상함수를 난무 해서는 안됩니다.
가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 들어 갑니다.
이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지를 결정하는데 쓰이는 정보입니다.
즉! 가상함수를 가지는 클래스에는 별도로 vptr(가상 함수 테이블 포인터)가 추가됩니다. 이는 가상 함수 테이블을 가리키는 포인터인데, 가상함수 테이블은 vtbl이라고 하며 이는 가상함수 포인터들을 담고 있는 배열입니다.
가상함수가 추가되면,
객체의 크기가 커질뿐만아니라, 다른언어로의 이식성은 기대를 접는것이 좋습니다.
1. 객체의 크기는 vptr( 가상 함수 테이블 포인터) 크기가 더해지면서, 커지고,
2. 이식성은 vptr을 구현환경에 따라 세부사항이 달라지는 문제가 있으므로 힘듭니다.
따라서
가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어있는 경우에만 한정
ex ) 가상함수가 전혀 없는데도 비가상 소멸자 때문에 오류인 예 STL string을 상속받은 예
class SpecialString : public std::string
{
...
}
void main()
{
SpecialString *pss = new SpecialString("ABC");
string* ps;
ps = pss;
delete ps;
}
위의 코드는 string 객체 ps(부모클래스 객체)를 소거 함으로써,
SpecialString 클래스의 소멸자가 호출되지 않는다. 이코드는 정상 수행을 할 수가 없다.
가상소멸자가 없는 클래스는 STL 컨테이너 타입 전부가 바로 여기에 속한다.
표준 컨테이너를 상속받아서 클래스를 만들지 않아야 한다.
경우에 따라서는 순수 가상 소멸자를 두면 편리하게 쓸 수도 있습니다.
추상클래스는
본래 기본 클래스로 쓰일 목적으로 만들어진 것이고, 기본 클래스로 쓰이려는 클래스는 가상 소멸자를 가져야 합니다. 그리고 순수가상함수가 있으면 바로 추상 클래스가 됩니다.
class AWOV
{
public:
virtual ~AWOV() = 0; // 순수 가상 소멸자
};
일단 위의 클래스는 순수가상함수를 가지므로, 추상 클래스 입니다.
동시에 순수 가상함수가 가상 소멸자이므로, 소멸자 호출 문제로 고민 할 필요도 없습니다.
여기서 중요한게,
이 순수 가상 소멸자의 정의를 두지 않으면 안된다는 것입니다.
AWOV :: ~AWOV() {} // 정의
컴파일러는 ~AWOV의 호출코드를 만들기 위해 파생 클래스 소멸자를 사용할 것이므로, 잊지 말고 꼭 정의 해야한다.
정리하자면,
기본 클래스에 가상 소멸자를 선언하는 것은 다형성(polymorphic)을 가진 기본클래스, 그러니까 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에만 적용됩니다.( 참고. 팩토리 패턴 )
이것 만은 잊지 말자!
◆ 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.
◆ 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.