상속 심화
핵심 개념
- 가상함수(virtual): virtual 예약어를 앞에 붙여 선언한 메서드. 부모 형식과 관계없이 파생 형식에서 메서드를 다시 재정의가 가능함.
- 가상 클래스: 가상 함수를 가진 클래스. 가상 클래스의 소멸자는 virtual예약어 선언이 없더라도 자동으로 가상화 된다.
- 다중 상속: 한 클래스가 두 개이상의 크래스를 동시에 상속받는 경우.
목표
- 가상 함수와 상속의 차이점
- 가상 클래스를 통한 메서드의 재정의 방법
- 상속 관계에서 형변환이 이루어지는 과정
- 다중 상속의 개념과 새로운 개체 정의 방법 이해
가상 함수
virtual 예약어를 앞에 붙여 선언한 메서드
기본적으로 '자기 부정'을 전제로 작동한다. -> 파생 형식에서 메서드를 재정의하면 과거의 정의가 완전히 무시된다.
기본 형식
virtual [반환형식] [메서드 이름] virtual void PrintData();
VirtualFunction
class CMyData{ public: virtual void PrintData(){ cout << "CMyData: " << m_nData << endl; } void TestFunc(){ cout << "**TestFunc()**" << endl; PrintData(); cout << "************" << endl; } protected: int m_nData = 10; } class CMyDataEx : public CMyData{ public: virtual void PrintData(){ cout << "CMyDataEx: " << m_nData * 2 << endl; } };
main
int main(){ CMyDataEx a; a.PrintData(); CMyData& b = a; b.PrintData(); a.TestFunc(); return 0; }
메인 함수에서
b.PrintData()
와a.PrintData()
의 출력 결과가 같다.
일반 메서드인 경우 b는 자식 객체인 a를 참조했지만 부모 메서드를 호출할 것이다.
하지만 virtual 예약어를 통해 과거의 정의가 완전히 무시가 되었으므로 새로 정의된 a의 메서드를 호출하게 된다.
기존의 오버라이딩과 비교해서 부모가 자식을 참조하였을 때 부모 자신의 메서드가 완전히 무시가 된다는 점이 기존과 다르다는 것을 알 수 있다.
일반 메서드의 경우 참조 형식이 무엇인지에 따라 메서드 호출이 결정하고, 가상 함수는 실 형식의 메서드를 호출일반 메서드는 참조형식, 가상 함수는 실 형식*
a(자식 객체)로 부모 메서드인 TestFunc()를 호출하면 이 안에 있는 메서드 PrintData()는 오버라이딩된 a의 메서드를 호출하게 된다.
즉, TestFunc()를 정의할 때 미래의 선언될 파생 형식의 PrintData()를 호출한다고 볼 수 있다.
(virtual예약어를 통해 기존의 함수를 따로 두는 것이 아닌 정의를 덮어 씌우기로 해석함)
final
예약어: 특정 가상 함수가 미래에 재정의되는 것을 막아줌
소멸자 가상화
- 자식 소멸자를 호출하고 실행한 후 부모 소멸자를 호출하고 실행한다.
만일 자식 객체를 선언하고 소멸자를 호출했다면 자동적으로 부모의 소멸자를 호출할 것이다.CMyData *pData = new CMyDataEx; delete pData;
하지만 위와 같이 선언할 경우 pData의 소멸자가 호출이 된다. 즉, 부모의 소멸자만 호출이 된다.
결국 함수 종료 후 실 형식인 자식 객체는 소멸자가 호출이 되지 않아메모리 누수
가 일어나게 된다. - virtual 예약어로 소멸자를 가상화 시켜준다.
virtual ~CMyData(){ delete m_pszData; }
- 이렇게 해주면 가상 함수의 특성으로 오버라이딩된 자식 클래스의 소멸자가 호출이 될 것이다.
- 바로 위에서 가상화된 일반 메소드를 호출하는 이유와 같다.
- 자식 소멸자는 실행 이후 부모 소멸자를 호출 하므로 메모리 누수 없이 종료할 수 있다.
CMyData *pData = new CMyDataEx;에서
CMyData를 추상 자료형(Abstract Data Type)이라 한다.
가상 함수 테이블(vtable)
Virtual funcion table: 함수 포인터 배열
컴파일러는 가상함수를 갖는 클래스 안에 가상 함수 테이블을 가리키는 포인터 (__vfptr
)을 몰래 추가시킨다.
vtable
은 가상 함수로 선언된 멤버함수들의 주소에 배열 형태로 저장되어 있는 것- 자식 클래스가 부모 클래스의 가상 메서드를 오버라이딩 했을 때 다음과 같이 쓸 경우
CMyData *pData = new CMyDataEx;
- 먼저 CMyDataEx가 생성을 하는데, 생성자는 CMyData를 먼저 실행하므로 __vfptr에는 부모 클래스의 vtable 주소를 저장하게 된다.
- 이후 자식 클래스의 생성자가 실행되면서 __vfptr에 자식 클래스의 vtable 주소를 덮어 씌우기가 될 것이다.
바인딩
함수나 변수의 주소가 결정되는 것 - 이른 바인딩
- 바인딩이 컴파일 때 결정될 때
- 늦은 바인딩 (동적 바인딩 "Dynamic binding")
- 바인딩이 실행하는 도중에 결정될 때
순수 가상 클래스
순수 가상 함수를 멤버로 가진 클래스를 말함
순수 가상 함수: 선언만
해두고 정의는 미래에 하는 함수. 가상 함수선언 뒤에 = 0
을 붙여준다.
virtual int GetData() conts = 0; //순수 가상 함수
순수 가상 클래스 특징
- 인스턴스를 직접 선언할 수 없다.
- 순수 가상 클래스의 파생 클래스는 반드시 부모 클래스의 순수 가상함수를 Overrride를 해야한다.
순수 가상 함수도 가상 함수이므로 추상 자료형으로 객체를 참조할 때 실 형식인 자식 객체의 메서드를 호출하게 된다.
이를 이용하여 다음에 나오는 인터페이스를 구현할 수 있게 된다.
인터페이스 상속
인터페이스: 서로다른 두 객체가 상호작용할 수 있는 통로나 방법.
일반적으로 가장 많이 사용되는 보편적 인터페이스를 선택한다.
인터페이스 예시
class CMyObject { public: CMyObject(){ } virtual ~CMyObject(){ } virtual int GetDeviceID() = 0; protected: int m_nDeviceID; }; class CMyTV :public CMyObject { public: CMyTV(int nID) { m_nDeviceID = nID; } virtual int GetDeviceID() { cout << "CMyTV::GetDeviceID()" << endl; return m_nDeviceID; } }; class CMyPhone :public CMyObject { public: CMyPhone(int nID) { m_nDeviceID = nID; } virtual int GetDeviceID() { cout << "CMyPhone::GetDeviceID()" << endl; return m_nDeviceID; } };
사용 예시
void PrintID(CMyObject* pObj) { cout << "Device ID: " << pObj->GetDeviceID() << endl; } int main() { CMyTV a(5); CMyPhone b(10); PrintID(&a); PrintID(&b); }
PrintID라는 함수는 CMyObject 형식에 대한 포인터를 매개변수로 삼는다.
따라서, CMyObject에서 파생된 클래스들은 모두 포인팅이 가능하다.
main 함수에서 CMyTV의 객체와 CMyPhone의 객체 주소를 넘겨 주고, 함수에서
CMyObject* pObj = &a; / &b;
로 매개변수를 받게된다.
만일 인터페이스 개념을 가지지 않고 함수를 사용하려면 메서드 오버로딩으로 구현하면 된다. 하지만 이는 매우 비효율적이다.
void PrintID(CMyTV* pObj) {
cout << "Device ID: " << pObj->GetDeviceID() << endl;
}
void PrintID(CMyPhone* pObj) {
cout << "Device ID: " << pObj->GetDeviceID() << endl;
}
int main() {
CMyTV a(5);
CMyPhone b(10);
PrintID(&a);
PrintID(&b);
}
미래의 개발자가 다른 클래스를 새로 구현할 때마다 PrintID라는 함수를 그 클래스에 맞게 오버로딩을 해야하기 때문이다.
하지만 인터페이스 개념으로 만들게 될 때 단지 클래스만 만들면 될 뿐 더이상의 오버로딩은 불필요하다.
추상 자료형의 예
추상 자료형: 기능의 구현 부분을 나타내지 않고 순수한 기능이 무엇인지 나열한 것
빠른 연산이 필요한 순간에 swtich-case문이나 다중 if문을 사용하는 것은 매우 비효율적이다.
따라서, 빠른 연산이 필요할 때 이 연산을 미리 처리해두면 매우 유리하다.
switch-case문 또는 다중 if문을 사용할 때는 여유로운 순간인 '사용자 입력'
때 사용하는 것이 효율적이다.
class CPerson{
public:
CPerson() {}
virtual ~CPerson() {
cout << "virtual ~CPerson()" << endl;
}
//요금계산 인터페이스 (순수 가상 함수)
virtual void CalcFare() = 0;
virtual unsigned int GetFare() { return m_nFare; }
protected:
unsigned int m_nFare = 0;
};
//초기 혹은 후기 제작자
class CBaby : public CPerson{
public:
//영유아(0~7세)
virtual void CalcFare() {
m_nFare = 0; //0%
}
};
class CChild : public CPerson{
public:
//어린이(8~13세) 요금계산
virtual void CalcFare() {
m_nFare = DEFAULT_FARE * 50 / 100; //50%
}
};
class CTeen : public CPerson{
public:
//어린이(14~19세) 요금계산
virtual void CalcFare() {
m_nFare = DEFAULT_FARE * 75 / 100; //75%
}
};
class CAdult : public CPerson{
public:
//20세 이상 성인
virtual void CalcFare() {
m_nFare = DEFAULT_FARE; //100%
}
};
//사용자 코드
int main(){
CPerson* arList[3] = { 0 };
int nAge = 0;
//1. 자료입력: 사용자 입력에 따라서 생성할 객체 선택
for (auto& person : arList) {
cout << "나이를 입력하세요: ";
cin >> nAge;
if (nAge < 8)
person = new CBaby;
else if (nAge < 14)
person = new CChild;
else if (nAge < 20)
person = new CTeen;
else
person = new CAdult;
//생성한 객체에 맞는 요금이 자동으로 계산된다.
person->CalcFare();
}
//2. 자료출력: 계산한 요금을 활용하는 부분
for (auto person : arList)
cout << person->GetFare() << "원" << endl;
//3. 자료삭제 및 종료
for (auto person : arList)
delete person;
return 0;
}
위와 같이 사용자 입력에서 그저 경우에 따른 호출을 하고 미리 연산되어있는 함수를 호출하여 빠른 연산처리를 가능케 한다.
현재는 사용자가 큰 기능을 수행하지 않지만 각 선택에 따라 수행해야할 연산이 많다면 각 if문에 많은 연산할 코드가 입력되고, 비중이 계속 커진다.
또한 각 if문에서 연산을 수행하게 되므로 효율적이지 못하다.
위와 같이 미리 연산을 해두고 사용자의 선택에 따라 연산된 값을 가져와서 사용하면 더 효율적인 코드를 완성할 수 있다.
다중 if /switch-case문은 직렬적 수행
추상 자료형은 병렬적 수행 -> 각 자료가 똑같은 부하 수준으로 대응 가능
상속과 형변환
const_cast<>: 상수형 포인터에서 const를 제거함
reinterpret_cast<>: C의 형변환 연산자와 흡사
- 어떤 경우라도 성공시키고 만다.
static_cast<>: 컴파일 시 상향 혹은 하향 형변환
상속 관계일 때 자식 객체를 부모 클래스로 포인팅 할 수 있다(추상 자료형)
- 이때 묵시적으로
상향 형변환
이 이루어짐
- 이때 묵시적으로
부모 형식의 포인터를 자식 형식의 포인터로 형변환 하는 것은 상속 관계에서만 가능
사용 방법
CMyData* pData = new CMyDataEx; CMyDataEx* pNewData = NULL; pNewData = static_cast<CMyDataEx*>(pData);
static_cast는 문법적으로 적절한 상향 / 하향 형변환이 아니면 컴파일 오류가 발생한다.
dynamic_cast<>: 런타임 시 상향 혹은 하향 형변환
- 상속 관계가 아닐 경우에 사용할 경우 컴파일 에러는 발생하지 않는다.
다만 형변환에 실패하면 NULL을 반환하여 바른 변환인지 아닌지 확인할 수 있다.- 이러한 확인 방법을 RTTI(Run-Time Type Information)이라 한다.
- RTTI 방법으로 typeid 연산자도 있으나 dynamic_cast와 같이 성능이 나쁘다.
- dynamic_cast는 꼭 필요한 경우가 아니라면 사용하지 말고 나올 경우는 코드 흐름이 좋지 않은 방향으로 흘러가고 있다는 것이다.
- 상속 관계가 아닐 경우에 사용할 경우 컴파일 에러는 발생하지 않는다.
상속과 연산자 다중정의
기본적으로 모든 연산자는 파생 형식에 자동으로 상속된다.
class CMyData{
public:
CMyData(int nParam) :m_nData(nParam) {}
CMyData operator + (const CMyData& rhs){
return CMyData(m_nData + rhs.m_nData);
}
CMyData& operator = (const CMyData& rhs){
m_nData = rhs.m_nData;
return *this;
}
operator int() { return m_nData; }
private:
int m_nData = 0;
};
class CMyDataEx : public CMyData{
public:
CMyDataEx(int nParam) : CMyData(nParam) {}
};
int main(){
CMyData a(3), b(4);
cout << a + b << endl;
CMyDataEx c(3), d(4), e(0);
e = c + d;
cout << e << endl;
return 0;
}
이는 e = c + d;에서 컴파일 에러가 발생한다.
자식 클래스는 상속을 받고 연산자와 관련하여 아무 다중정의를 하지 않았으므로 c + d는 부모 클래스의 연산자 호출을 한다.
즉 c + d의 반환 형식은 CMyData가 된다.
e는 자식 객체이고 CMyDataEx = CMyData꼴이 된다.
하지만 이러한 연산자는 오버로딩을 하지 않았으므로 에러가 발생한다.
- 이러한 문제는 + 연산의 반환 형식이 문제이므로 operator +만 오버로딩을 해주면 해결이 된다.
CMyDataEx operator+(const CMyDataEx &rhs){ return CMyDataEx(static_cast<int>(CMyData::operator+(rhs))); }
- operator= 는 따로 안해줬기 때문에 부모 CMyData&를 반환하는데
이 경우 e는 부모인가? 자식인가?
- operator= 는 따로 안해줬기 때문에 부모 CMyData&를 반환하는데
- using: 정의는 상위클래스의 것을 사용, 인터페이스만 맞춤
using CMyData::operator+; using CMyData::operator=;
다중 상속
한 클래스가 두 개 이상의 클래스를 동시에 상속받는 것
사용방법
class CMyPicture : public CMyImage, public CMyShape
- 다중 상속의 모호성
- 위 CMyImage와 CMyShape에 같은 메소드 GetSize가 있다고 하자
- CMyPicture의 객체를 통해 GetSize를 호출하게 되면 어떤 부모 클래스의 메서드를 호출해야할지 모호하다.
- 이를 해결하기 위해
명시적으로 표현(::)
을 해준다.가상 상속
상속을 받을 때 virtual 예약어를 선언해 주는 것
필요성: 다음과 같은 경우를 해결하기 위함class CMyDataEx : virtual public CMyData
class CMyObject { public: CMyObject() { cout << "CMyObject()" << endl; } };
class CMyImage : public CMyObject {
public:
CMyImage() { cout << "CMyImage(int, int)" << endl; }
};
class CMyShape : public CMyObject {
public:
CMyShape() { cout << "CMyShape(int)" << endl; }
};
class CMyPicture : public CMyImage, public CMyShape {
public:
CMyPicture() { cout << "CMyPicture()" << endl; }
};
int main() {
CMyPicture a;
return 0;
}
이는 CMyImage와 CMyShape가 각각 CMyObject를 상속받고 CMyPicture가 CMyImage와 CMyShape를 상속을 받았다.
이때 생성자는 CMyPicture 한 번 (CMyImage 한 번, CMyObject 한 번), (MyShape 한 번, CMyObject 한 번) 총 5회 호출된다. 즉, 최상위 클래스가 `두 번 호출`되는 상황이 된다.
이를 해결하기 위해 **virtual 예약어**를 사용해서 가장 마지막에 선언된 생성자를 통해 *한 번만* 호출한다.
```c++
class CMyImage : virtual public CMyObject
class CMyShape : virtual public CMyObject
인터페이스 다중 상속
알맹이는 하나인데 이 알맹이를 활용하는 방법이 여러가지인 경우
class CMyUSB
{
public:
virtual int GetUsbVersion() = 0;
virtual int GetTransferRate() = 0;
};
class CMySerial
{
public:
virtual int GetSignal() = 0;
virtual int GetRate() = 0;
};
class CMyDevice : public CMyUSB, public CMySerial
{
public:
//USB 인터페이스
virtual int GetUsbVersion() { return 1; }
virtual int GetTransferRate() { return 2; }
//시리얼 인터페이스
virtual int GetSignal() { return 3; }
virtual int GetRate() { return 4; }
};
int main(){
CMyDevice dev;
return 0;
}
이 때 dev라는 객체는 CMySerial 클래스와 CMyUSB 클래스의 인터페이스 모두를 사용
가능하다.
만일 main함수를 다음과 같이 쓴다면
int main(){
CMyDevice dev;
CMyUSB* u = &dev;
cout << u->GetUsbVersion() << endl;
return 0;
}
dev에서 오버라이딩된 GetUsbVersion메서드와 GetTransferRate메서드만 사용이 가능하고 CMySerial의 인터페이스들은 사용이 불가하다.
연습문제
가상 함수를 사용하는 가장 큰 이유
- 클래스의 다향성을 위해
소멸자를 반드시 가상화해야 하는 경우는?
- 추상 자료형으로 선언했을 경우
늦은 바인딩이란?
- 바인딩: 함수나 변수의 주소가 결정되는 것
- 늦은 바인딩: 컴파일 타임에 결정되면 이른 바인딩 / 실행되는 도중에 주소가 결정되면 늦은 바인딩
순수 가상 클래스의 파생 클래스에서 반드시 해야하는 일?
- 부모 클래스의 순수 가상 함수를 오버라이딩을 해야한다.
다중 상속의 모호성을 회피하려면?
- 범위 지정 연산자(::) 사용
'C++ > 이것이 C++이다.' 카테고리의 다른 글
Ch10. 예외 처리 (0) | 2021.01.15 |
---|---|
Ch6. 상속 기본 (0) | 2021.01.15 |
Ch5. 연산자 다중정의 (0) | 2021.01.15 |
Ch4. 복사 생성자와 임시 객체 (0) | 2021.01.15 |
Ch3. 클래스 (0) | 2021.01.15 |
최근댓글