[C++] 상속 심화

강민석·2022년 10월 18일
0

이것이 C++이다

목록 보기
6/8
post-thumbnail

7.1 가상 함수

  • '가상 함수(Virtual Function)'는 virtual 예약어를 앞에 붙여서 선언한 메서드를 말한다.
  • 가상 함수는 '자기 부정'을 전제로 작동한다.
  • 일반 메서드는 참조 형식을 따르고, 가상 함수는 실 형식을 따른다.
  • 가상 함수의 호출은 미래에 파생 클래스에서 재정의될 함수를 호출하는것이다.
  • 가상 함수를 적절히 사용하기 위해선, 해당 함수가 어디에서 호출되는지 파악하는것이 중요하다.

7.1.3 소멸자 가상화

  • 상위 클래스로 하위 파생 클래스를 참조할 때, 심각한 메모리 누수 오류가 발생할 수 있다.
// 기본 클래스
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;
    }
  • 기본 클래스의 소멸자를 가상화하면 파생 클래스의 소멸자까지 제대로 호출된다.

7.2 가상 함수 테이블(vtable)

  • 'Virtual function table(vtable)'
  • vtable은 '함수 포인터 배열'이라고 이해하면 쉽다.
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 이라는 지역변수가 존재한다.
  • 이것이 바로 'vtable 포인터'인데, 이 포인터를 통해 가상 함수로 선언된 멤버 함수들의 주소에 배열 형태로 접근할 수 있다.
  • 위 예제에서 파생 클래스인 CMyDataEx의 생성자가 호출되면 이어서 기본 클래스인 CMyData의 생성자가 호출된다.
  • CMyData의 생성자가 실행되면 __vfptrCMyData 내부에 정의된 두 가상함수의 주소를 가리키게 된다.
  • 뒤이어 CMyDataEx의 생성자가 실행되면 __vfptrCMyDataEx 내부에 재정의된 두 가상함수의 주소값으로 덮어씌워진다.
  • 따라서 참조 형식이 CMyData인 변수 *pData의 vtable은 실 형식인 CMyDataEx의 가상 함수를 가리키고 있으므로 CMyDataEx::TestFunc2()가 호출되는 것이다.

  • 함수나 변수의 주소가 컴파일 타임에 결정되면 '이른 바인딩(Early binding)', 프로그램 실행중에 결정되면 '늦은 바인딩(Late/Dynamic binding)'이라고 한다.
    • 가상 함수의 경우는 '늦은 바인딩'에 해당된다. (인스턴스가 생성되는 시점에 함수의 주소가 결정되므로)

7.3 순수 가상 클래스

  • '순수 가상 클래스'는 '순수 가상 함수'를 멤버로 가진 클래스를 의미한다.

  • '순수 가상 함수'는 정의부가 생략된, 즉 선언부만 존재하는 함수를 의미한다.

    • 정의부를 단순히 생략하는 것이 아니라, = 0을 꼭 넣어줘야 한다.
    • ex) virtual int GetData() const = 0;
  • 순수 가상 클래스는 인스턴스를 직접 생성할 수 없으며(상속을 통해 파생 클래스의 인스턴스를 생성할 수 있음), 파생 클래스는 기본 클래스의 순수 가상 함수를 반드시 재정의 해야한다.


  • 맥락상 자바의 Abstract Class 내지는 Interface 정도로 이해하면 될 듯 하다.

7.4 상속과 형변환

  • C++의 형변환 연산자는 다음 네 가지이다.
    • const_cast<> : 상수형 포인터에서 const를 제거한다.
    • static_cast<> : 컴파일 시 상향 혹은 하향 형변환한다.
    • dynamic_cast<> : 런탐임 시 상향 혹은 하향 형번환한다.
    • reinterpret_cast<> : C의 형변환 연산자와 비슷하다. (??)

7.4.1 static_cast

  • 상속 관계일 때 파생 형식을 기본 형식(부모 클래스의 형식)으로 포인팅할 수 있다. (묵시적인 상향 형변환)

  • 기본 형식 포인터가 가리키는 대상을 파생 형식 포인터로 형변환하는 '하향 형변환'은 상속 관계에서만 가능하다.

  • 하향 형변환 예제

    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;
    }

7.4.2 dynamic_cast

  • dynamic_cast가 등장했다는 것은 좋지 못한 방향으로 흘러가고 있다는 증거.
  • 꼭 필요한 경우가 아니라면 dynamic_cast는 절대 사용하지 말자.

7.5 상속과 연산자 다중 정의

  • 기본적으로 모든 연산자는 파생 형식에 자동으로 상속된다. (단순 대입 연산자 제외)

  • 기본 형식에 + 연산자 함수가 정의되어 있는 경우, 함수의 반환값은 기본 클래스의 인스턴스이므로 파생 클래스의 인스턴스에 대입할 수 없다.

  • 다음과 같이 파생 클래스에 + 연산자 함수를 정의하여 해결할 수 있다.

    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=;
    }

7.6 다중 상속

  • 자바에서는 클래스 다중 상속이 문법적으로 불가능하다.
  • C++에서는 가능하긴 하나 되도록이면 사용하지 않도록 하자.

7.6.3 인터페이스 다중 상속

  • 인터페이스(순수 가상 클래스)는 다중 상속이 유일하게 좋은 결과로 나타나는 경우.
  • 자바도 클래스는 다중 상속이 불가능하지만, 인터페이스는 가능하다.

0개의 댓글