객체지향 언어의 특징은 아래와 같다.
1. 캡슐화 (은닉성)
2. 상속
3. 다형성
4. 추상화
그중에서 상속에 대해 알아본다.
클래스 CParent를 상속받는 자식 클래스 CChild는 다음과 같이 선언한다.
#include <iostream>
class CParent
{
private:
int m_i;
public:
CParent()
: m_i(0)
{}
~CParent()
{}
};
class CChild : public CParent
{
private:
float m_f;
public:
CChild()
: m_f(0.f)
{}
};
어떤 클래스의 상속을 받는다는 것은 그 클래스의 기능을 자식 클래스에 가져오게 한다는 뜻이다.
그러므로 상속을 받은 클래스는 최소한 원래 부모가 가지고 있던 클래스의 기능을 기본적으로 가지고 있으면서,
자신만의 새로운 기능을 추가적으로 구현하여 사용할 수 있다.
따라서 어떤 기능이 여러 클래스에서 반복적으로 사용되어야 할 때, 그것을 기반으로 가진 클래스 하나에 그 기능을 구현하면 다른 클래스에서는 그 클래스를 상속받는 방식으로 간편하게 그 코드를 재사용할 수 있다.
상속을 받은 클래스의 메모리 크기는 상속한 클래스의 메모리 크기 + 본인의 메모리 크기이다.
앞서 살펴본 코드에서 CParent는 4바이트 정수 타입인 int형 변수를 하나 가지고 있고,
그것을 상속받은 CChild는 4바이트 실수 타입인 float 변수를 하나 가지고 있다.
그러므로 CChild의 전체 크기는 8바이트가 된다.
이때 주의해야 할 점은 메모리상에서 데이터가 배치되는 순서는 부모 -> 자식 순이라는 것이다.
CParent (4byte) | CChild(4byte) |
만약 여기서 CChild를 상속받는 클래스 CChildChild 클래스가 추가된다면 메모리 상으로는
CParent -> CChild -> CChildChild순으로 배치될 것이다.
또한, 자식 클래스가 부모 클래스의 기능을 무조건 모두 상속 받을 수 있는 것은 아니다. parent 클래스가 어떤 변수의 접근 제한 지정자를 private로 선언했다면 상속받은 자식 클래스에서는 변수에 접근할 수 없다.
모든 곳에서 접근 가능한 public 외애 자식 클래스에게만 접근 가능한 변수를 만들고 싶다면 protected로 해당 요소를 선언하면 된다. 이 경우 자식 클래스 외의 클래스에서는 해당 요소를 호출할 수 없게 된다.
또한 클래스 내부 변수에 대한 초기화의 경우 부모 클래스를 상속받았다고 해서 그 변수를 부모 클래스에서만 초기화 해선 안된다. 반드시 각 클래스가 상속 받은 변수를 포함한 전체 변수에 대해 각자 초기화를 해주어야 한다.
마지막으로 상속 받은 클래스의 생성자가 동작하는 방식에 대해 생각해 보자.
프로그램이 실행되면서 생성자 호출하면 상속받은 클래스(자식)가 먼저 호출되고 상속한 부모가 그다음으로 호출된다.
하지만 반대로 생성자가 메인 함수등 다른 객체에 의해 호출받았을 때와 값을 초기화할 때는 부모가 먼저 호출되고, 자식 클래스가 다음으로 호출된다.
그렇다면 상속 관계에 있을 때 소멸자는 어떻게 작동할까?
소멸자는 호출 뿐만 아니라 실행도 모두 자식 쪽에서 먼저 수행이 되고, 그 이후에 부모 쪽 기능을 호출하도록 작동한다.
소멸자의 호출 순서를 보면 실제로 눈에 보이지는 않지만 상속받은 부모의 소멸자를 호출하는 코드가 있다.
#include <iostream>
using namespace std;
class CParent
{
protected:
int m_i;
public:
void SetInt(int _a)
{
m_i = _a;
}
void Output()
{
cout << "Parent" << endl;
}
public:
CParent()
: m_i(0)
{
cout << "부모 생성자" << endl;
}
~CParent()
{
cout << "부모 소멸자" << endl;
}
};
class CChild : public CParent
{
private:
float m_f;
public:
void SetFloat(float _f)
{
m_f = _f;
m_i = 100;
}
void Output()
{
cout << "Child" << endl;
}
public:
CChild()
: m_f(0.f)
{
m_i = 0;
cout << "자식 생성자" << endl;
}
~CChild()
{
cout << "자식 소멸자" << endl;
}
};
void FuncA()
{
cout << "Function A" << endl;
}
void FuncB()
{
FuncA();
cout << "Function B" << endl;
}
int main()
{
CParent parent;
CChild child;
return 0;
}
코드를 실행해보면 자식 소멸자가 먼저 호출되고, 부모 소멸자가 이후로 호출 되는 것을 알 수 있다.
부모 소멸자 부분을 디버깅으로 자세히 살펴보면, 자식 소멸자 -> 자식 클래스가 가지고 있는 부모 클래스의 소멸자 -> 원본 부모 클래스의 소멸자 순서로 실행 된다.
이를 함수 호출로 예를 들면 다음과 같다.
void FuncA()
{
cout << "Function A" << endl;
}
void FuncB()
{
cout << "Function B" << endl;
FuncA();
}
이렇듯 소멸자는 실행, 호출한 순서와 상관 없이 자식에서 부모 순서로 호출된다.
중요 특징을 다시 한번 정리한 내용은 아래와 같다.
<상속의 특징 정리>
- 자식 or 부모 클래스는 상속관계에서 다른 클래스의 멤버를 초기화할 수 없다.
- 생성자 호출 순서는 자식 -> 부모 순서이다.
- 반대로 생성자의 실행 순서와 초기화 순서는 부모 -> 자식 순서이다.
- 소멸자의 실행 및 호출 순서는 자식 -> 부모 순이다.
'C,C++' 카테고리의 다른 글
다형성 (0) | 2023.02.13 |
---|---|
오버라이딩 (0) | 2023.02.12 |
Tree 구현 + Enum (0) | 2023.02.10 |
[C++] list iterator (0) | 2023.02.06 |
[C++] iterator (0) | 2023.02.05 |