상속은 3가지 관점에서 정의 할 수 있다.
1.코드를 재사용 하는 방법
2.규모의 확장
3.관계의 한가지 유형
객체지향 프로그래밍에서 개발자는 유지보수 를 최대한 신경 써야한다
그점에서 상속은 미래에 개발을 진행할 개발자를 위해서 라도 제대로 규정해야 한다
상속은 다음과 같이 코드를 작성한다
class 파생클래스이름 : 접근제어지시자 부모클래스 이름
예를 들어 다음과 같이 작성할수 있다
class MyDataEx : public MyData
이제부터 상속의 특징을 공부하고 코드를 통해 어떻게 실행되는지 살펴보자
먼저 3가지 특징을 알고 가자
1.파생 클래스의 인스턴스가 생성될 떄 기본 클래스의 생성자도 호출 된다
2.파생 클래스는 기본 클래스의 멤버에 접근 가능하다.단 Private 선언된 클래스 멤버는 접근 불가
3.사용자 코드에서는 파생 클래스의 인스턴스를 통해 기본 클래스 메서드 호출이 가능하다
실제 코드와 출력 결과를 통해 알아보자
//초기 개발
class MyData
{
public:
MyData()
{
cout << "MyData()" << endl;
}
~MyData()
{
}
int GetData()
{
return m_nData;
}
void SetData(int nParam)
{
m_nData = nParam;
}
protected:
void PrintData()
{
cout << "MyData::PrintData()" << endl;
}
private:
int m_nData = 0;
};
//상속 클래스
class MyDataEx : public MyData
{
public:
MyDataEx()
{
cout << "MyDataEx()" << endl;
}
~MyDataEx()
{
}
void TestFunc()
{
PrintData();
SetData(5);
cout << MyData::GetData() << endl;
}
private:
};
//사용자 코드
int main(void)
{
MyDataEx data;
//기본 클래스(MyData) 멤버의 접근
data.SetData(10);
cout << data.GetData() << endl;
//파생 클래스(MyDataEx) 멤버의 접근
data.TestFunc();
return 0;
}
출력결과
MyData()
MyDataEx()
10
MyData::PrintData()
5
출력결과 에서 MyData클래스의 생성자(기본 클래스 생성자)가 호출 된것이다
출력 결과를 보면 기본 클래스 생성자가 파생 클래스 생성자 보다 먼저 호출 된것으로 보이지만
파생 클래스 생성자가 먼저 호출되지만 실행이 나중에 되버린다.
이러한 동작 과정 때문에 우리가 한 가지 주의 해야 하는게 있다
부모형식 멤버를 파생 형식 생성자 에서 절대 초기화 하면 아니 된다
오직 생성자는 객체 자신을 초기화 해야한다
data.SetData(10);, data.TestFunc(); 코드 같은 경우 파생 클래스에 직접적으로 상위 클래스의 private멤버의 접근 할 수 없지만 위와 같이 메서드를 통해 접근이 가능하다
메서드 재정의는 오버라이드 라고도 불린다.
기본적으로 재정의는 기존의 것을 무시 한다
기존 클래스 메서드를 새롭게 대체 한다고 생각하면 된다
//초기 개발
class MyData
{
public:
MyData()
{
}
~MyData()
{
}
int GetData()
{
return m_nData;
}
void SetData(int nParam)
{
m_nData = nParam;
}
private:
int m_nData = 0;
};
//상속 클래스
class MyDataEx : public MyData
{
public:
MyDataEx()
{
}
~MyDataEx()
{
}
void SetData(int nParam)
{
if (nParam < 0)
MyData::SetData(0);
if (nParam > 10)
MyData::SetData(10);
}
private:
};
//사용자 코드
int main(void)
{
MyData data;
MyDataEx data_new;
data.SetData(15);
data_new.SetData(15);
cout << data.GetData() << endl;
cout << data_new.GetData() << endl;
return 0;
}
출력결과
15
10
기본 클래스 에서 SetData를 정의했다 그리고 파생 클래스 에서 SetData를 재정의 했다
사용자 코드에 기존 클래스 인스턴스의 메서드랑 파생 클래스 인스턴스의 메서드는 이름은 같지만 정의가 다르기 떄문에 당연히 출력 값이 다른게 보인다.
우리는 여기서 조금 더 생각을 확장해 보자
일단 파생 클래스의 SetData를 다음처럼 바꾸면 어떻게 될까?
//바꾸기 전
void SetData(int nParam)
{
if (nParam < 0)
MyData::SetData(0);
if (nParam > 10)
MyData::SetData(10);
}
//바꾼 후
void SetData(int nParam)
{
if (nParam < 0)
SetData(0);
if (nParam > 10)
SetData(10);
}
위와 같이 소속 클래스 명시를 하지 않으면 기본적 으로 아래 함수는 재귀호출 함수가 되버린다.
그렇기에 파생 형식에서 기본 형식의 동일 메서드를 호출하려면 소속 클래스를 명시하자
앞서 코드에서 사용자 코드를 조금 변경 하겠다
//사용자 코드
int main(void)
{
MyDataEx data;
MyData &rdata = data;
rdata.SetData(15);
cout << data.GetData() << endl;
return 0;
}
출력결과
15
MyDataEx를 MyData 형식으로 참조 했다.
파생형식을 기본형식 으로 참조하는 것은 가능하다
여기서 중요한 건 출력 값 15이다
값이 15가 나왔다는 것은 기본 클래스의 SetData가 호출되었다는 것을 의미한다.
메서드가 호출되는 기준은 다음과 같다
1.메서드가 일반 =>접근형식
2.메서드가 Virtual => 실형식
즉 접근형식이 기본 클래스 이므로 파생 클래스를 참조 했다고 하나 접근형식을 따라서 메서드가 호출된다
참고로 포인터 참조를 통해서도 호출이 가능하다
//사용자 코드
int main(void)
{
MyData *pclass = new MyDataEx;
pclass ->SetData(15);
cout << pclass->GetData() << endl;
delete pclass;
return 0;
}
앞서 잠깐 생성자에 호출과 실행 순서에 대해서 알아봤는데 여기서 조금 더 구체적으로 살펴보자
다음과 같은 상속관계가 있다고 생각해 보자.
생성자와 소멸자의 관계는 다음 같다
1.C클래스 인스턴스를 선언하면 생성자 호출 순서는 C,B,A 이다
2.하지만 가장 먼저 실행되는 생성자는 C가 아니라 A다
3.C클래스 인스턴스가 소멸하면 C 클래스의 소멸자가 가장 먼저 호출되고 실행된다
4.즉 생성자는 호출과 실행 순서가 역순이고 소멸자는 같다