C, C++/C++ 언어 / / 2024. 5. 25.

[C++] 추상 클래스와 순수 가상 소멸자

 

추상 클래스 ( Abstract Class )

추상 클래스는 순수 가상 함수( Pure Virtual Function )를 멤버 함수로 가진 클래스를 말합니다.

여기서, 순수 가상 함수란 순수 지정자( = 0) 구문을 사용하여 선언된 가상 함수입니다.

 

복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 추상화라고 합니다.

추상 클래스는 순수 가상 함수를 통해서 필요한 기능을 간추려낸 클래스라는 의미입니다.

 

다음 코드를 보며 설명하겠습니다.

#include <string>
using namespace std;

class CShape{	// 추상 클래스

public:
    virtual string GetName() = 0;   // 순수 가상 함수
    
    virtual float GetArea(){
    	return -1;
    }
};

string CShape::GetName(){	// 순수 가상 함수는 클래스 헤더에 구현할 수 없다
    return "Undefined";
}

class CRectangle : public CShape{
    
public:
    virtual string GetName(){
        return "Rectangle";
    }
};

int main(){

    CShape shape;   // error!! 추상 클래스는 객체를 생성할 수 없다

    CRectangle rect;    // ok. 순수 가상 함수의 기능을 구현하면 생성 가능

    return 0;
}

위의 CShape 클래스는 GetName()이라는 순수 가상 함수를 멤버 함수로 갖고 있기 때문에 추상 클래스입니다.

순수 가상 함수가 1개 이상이 되면 무조건 추상 클래스입니다.

 

이러한 추상 클래스는 객체를 생성할 수 없습니다.

생성하려고 하면, 컴파일러에 의해 경고를 받습니다.

또한, 이 추상 클래스를 상속받은 클래스라도 순수 가상 함수의 기능을 구현한 클래스만 객체를 생성할 수 있습니다.

순수 가상 함수의 기능을 구현하지 않은 파생 클래스도 여전히 추상 클래스입니다.

 

참고로, 추상 클래스도 순수 가상 함수의 기능을 구현할 수 있습니다.

클래스 헤더에 선언만 할 수 있는 것이 아닙니다.

하지만,  순수 지정자( = 0)와 함께 붙여 쓸 수는 없으므로, 클래스의 헤더 외부에 구현해야 합니다.

 

왜 추상 클래스를 만드는 걸까요?

추상 클래스는 클래스의 설계도와 비슷합니다.

파생 클래스에 꼭 필요한 기능이 구현되어야 하는데, 구현을 하지 않는 경우가 있을 수 있습니다.

 

위의 예에서, CRectangle 클래스에 GetArea() 기능이 필요한데, 구현하지 않았다고 가정해 보죠.

그럼, 기본 클래스 CShape의 GetArea() 함수가 호출되고, 잘못된 결과를 갖게 될 것입니다.

여기서, 가장 큰 문제는 어떠한 오류도 발생하지 않는다는 점에 있습니다.

 

프로그램의 크기가 크면 클수록, 클래스들을 설계하는 사람과 구현하는 사람이 의견을 나눌 기회가 점점 줄어들 것은 명백합니다.

 

이때, 필요한 것이 바로 설계도입니다.

이 설계도는 파생 클래스가 갖추어야 할 핵심적이고 필수적인 기능을 구현하도록 강제합니다.

그럼으로써, 커다란 배가 제대로 나아가도록 만드는 겁니다.

 

위의 예에선, CShape의 GetArea() 가상 함수를 순수 가상 함수로 변경합니다.

그러면, CRectangle 클래스의 객체를 생성하려고 할 때 컴파일러로부터 경고를 받게 될 것입니다.

따라서, 이제 CRectangle 클래스를 사용하려면 아래와 같이 기능을 추가해야 합니다.

class CRectangle : public CShape{
    
    float m_nWidth, m_nHeight;
    
public:
    virtual string GetName(){
        return "Rectangle";
    }
    
    virtual float GetArea(){	// 객체를 생성하려면, 가상 함수를 구현해야 한다
    	return m_nWidth * m_nHeight;
    }
};

 

 

순수 가상 소멸자 ( Pure Virtual Destructor )

클래스를 설계할 때, 클래스의 소멸자를 가상 함수로 할 것인지를 고민하는 것은 중요한 문제입니다.

 

아래의 예제를 실행해서 결과를 출력해 보면 문제를 발견하게 될 것입니다.

#include <iostream>
using namespace std;

class CBase {

public:
    ~CBase(){
        cout << "CBase destructor called\n";
    }
};

class CDerived : public CBase {
    int* m_pData;

public:
    CDerived(){
        m_pData = new int[100];
    }

    ~CDerived(){
        delete [] m_pData;	// 생성자에서 할당한 메모리 해제

        cout << "CDerived destructor called\n";
    }
};

int main(){

    CBase* pObj = new CDerived();

    delete pObj;

    return 0;
}

▼출력

CBase destructor called

 

CDerived 클래스는 생성자에서 데이터를 할당한 후, 소멸자에서 할당된 데이터를 해제하는 클래스입니다.

객체를 생성하고, 다 사용하고 나면 자동으로 메모리가 해제되길 바라고 작성했을 것입니다.

 

그런데, 기본 클래스 CBase 타입의 포인터로 객체를 생성 후, 삭제하면 CDerived 클래스의 소멸자가 호출되지 않고 CBase 클래스의 소멸자가 호출됩니다.

할당된 메모리가 제대로 해제되지 않는 문제가 발생한 거죠.

 

이 문제를 바로 잡으려면, CBase 클래스의 소멸자를 가상 함수로 변경해야 합니다.

그리고, 더 나아가 이 소멸자를 순수 가상 함수로 변경하면,  CBase를 추상 클래스로 바꾸고 CBase로부터 상속받는 모든 클래스에서 소멸자를 구현하도록 강제할 수 있습니다.

그러면 이러한 문제점을 사전에 차단할 수 있을 것입니다.

 

이때, 주의할 점이 있습니다.

컴파일러는 CDerived 클래스의 소멸자가 호출될 때, 기본(base) 클래스인 CBase의 소멸자를 암시적으로 호출합니다.

그래서, 만약 선언만 되어 있는 경우, 오류가 발생됩니다.

CBase의 소멸자를 순수 가상 함수로 변경 시, 클래스 선언 외부에 CBase의 소멸자를 구현해야 합니다.

class CBase {

public:
    virtual ~CBase() = 0;	// 순수 가상 소멸자
};

CBase::~CBase(){    // 클래스 선언 외부에 구현
    cout << "CBase destructor called\n";
}

▼출력

CDerived destructor called
CBase destructor called

이제, 모든 것이 제대로 돌아가네요.

 

정리

  • 추상( Abstract ) 클래스는 순수 가상 함수( Pure Virtual Function )를 가진 클래스입니다.
  • 클래스의 소멸자( Destructor )를 가상 함수로 만드는 것은 고려할 만한 가치가 있습니다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유