배움 저장소
[홍정모의 따라하며 배우는 C++] 11. 상속 본문
11.1 상속의 기본 (1)
Derived Class(상속받은 객체)는 Generalized Class(상위 객체)의 인스턴스를 멤버를 가지고 있다
class Mother {
private:
int m_i;
public:
int& getValue(){ return m_i; }
void setValue(int i){ m_i = i; }
};
class Child: public Mother {};
int main(){
Mother mom;
mom.setValue(45);
cout << mom.getValue() << endl;
Child son;
son.setValue(15);
cout << son.getValue() << endl;
son.Mother::setValue(20);
cout << son.getValue() << endl;
}
(1) 이 때 상속받은객체는 상위객체의 private에 접근할 수 없다. 외부에서 접근은 불가능하되 상속받은 객체에서는 접근 가능하게하기 위하여 protected keyword를 사용할 수 있다.
(2) 상속받은 객체에서 멤버를 정의할 때 멤버가 상위객체에서 정의된 멤버와 같은 이름을 가지고 있다면 상위객체의 멤버를 무시한다
(3) 상속받은 객체의 initializer list에서 상위 객체의 멤버 변수 값을 할당할 수 없다. 대신 initializer list에서 상위객체의 생성자를 호출할 수 있다.
class Mother {
private:
int m_i;
public:
Mother(const int& i):m_i(i){}
int& getValue(){ return m_i; }
void setValue(const int &i){ m_i = i; }
};
class Child: public Mother {
private:
double m_d;
public: // below needs default constructor that pass 0 arguement
Child(const int &i, const double& d):Mother(i), m_d(d) {}
//Child(const int& i, const double& d) :m_d(d) { Mother::setValue(i); } // Error!
double& getValue(){ return m_d; }
void setValue(const double &d){ m_d = d; }
};
int main(){
Mother mom(45);
//mom.setValue(45);
cout << mom.getValue() << endl;
Child son(15, 30);
//son.setValue(15);
cout << son.getValue() << endl;
son.Mother::setValue(20);
cout << son.getValue() << endl;
}
11.2 상속의 기본 (2)
Inheritance(is-a relationship)
공통으로 사용하는 기능은 상위 객체를 만들어 상속받자. 개별적인 기능은 해당 객체에만 구현하자. 유지-보수가 쉽다
큰 프로그램에서 상위 객체의 멤버변수를 상위객체가 관리해야 유지-보수가 편리하다. private로 막아버리자
class Mother {
protected:
int m_i;
};
class Child : public Mother {
public:
Child() {
m_i = 1;
this->m_i = 2;
Mother::m_i = 3;
this->Mother::m_i = 3;
}
};
11.3 유도된 클래스들의 생성 순서
Derived
(1) 상속받은 객체 내부에서 상위객체의 생성자를 호출하지 않으면 기본 생성자를 호출한다. 이 때 상위객체에서 생성자를 직접 구현할 경우 기본 생성자를 호출할 수 없어 에러가 발생할 수 있다
(2) 상속받은 객체의 initializer list에는 상위객체의 기본 생성자가 숨어있다.
(3) 상속받은 객체의 initializer list에서 상위객체의 생성자를 호출해보자. 다른 매개변수의 순서와 관계없이 항상 제일먼저 실행된다
class Mother {
protected:
int m_i;
public:
Mother(int i=0) :m_i(i) {
cout << "Mother Constructor Executed " << i << endl;
}
const int& getValue() const { return m_i; }
};
class Child : public Mother {
private:
double m_d;
public:
Child(double d=0, int i=1):m_d(d), Mother(i) {
cout << "Child Constructor Executed " << m_d << endl;
}
};
int main() {
Child child(1,2);
}
Mother Constructor Executed 2
Child Constructor Executed 1
11.4 유도된 클래스들의 생성과 초기화
상위객체가 상속된 객채 내부에서 차지하고 있는 저장공간
- 멤버변수 맨 앞에 위치해있음을 추측해볼 수 있다. 다음 예시로 padding을 확인할 수 있다.
class Mother {
protected:
int m_i;
public:
Mother(int i=0) :m_i(i) {
cout << "Mother Constructor Executed " << i << endl;
cout << "Size = " << sizeof(*this) << endl;
}
};
class Child : public Mother {
private:
double m_d;
int m_i=0;
public:
Child(double d=0, int i=1):m_d(d), Mother(i) {
cout << "Child Constructor Executed " << m_d << endl;
cout << "Size = " << sizeof(*this) << endl;
}
};
int main() {
Child child(1,2);
}
Mother Constructor Executed 2
Size = 4
Child Constructor Executed 1
Size = 24
위에서 Child 객채 내부에있는 m_i와 m_d의 순서를 바꾸어주자
- 순서를 바꾸면 Child 객체의 크기가 줄어든다. 위에서 24byte가 나오는 이유는 정보를 주고받는 최소단위가 8byte이기 때문이다. 숨겨져 있는 상위객체는 상속된 객체 맨 앞 저장공간에 위치하고 있다. 8byte double 자료형을 중간에 끊어진 형태로 저장할 수 없기 때문에 앞에 있는 상위 객체의 저장공간에 padding을 넣었고 뒤에 int 자료형에도 마찬가지이다.
- 이 때 int 자료형을 앞으로 당겨 상위 객체와 int 자료형을 8byte에 섞어놓을 수 있게 되었다.
class Child : public Mother {
private:
int m_i=0;
double m_d;
// ...
};
Mother Constructor Executed 2
Size = 4
Child Constructor Executed 1
Size = 16
소멸자 호출 순서
- 상속된 객체가 자신의 소멸자를 호출하여 내부 코드를 실행시킨다. 상위 객체를 사용할 수 있기 때문에 종료 직전에 상위 객체의 소멸자를 호출한다.
class Mother{
protected:
int m_i;
public:
Mother(int i=0) :m_i(i) {
cout << "Mother Constructor Executed " << i << endl;
cout << "Size = " << sizeof(*this) << endl;
}
~Mother() {
cout << "Mother Destructor executed " << endl;
}
};
class Child : public Mother {
private:
int m_i = 0;
double m_d;
public:
Child(double d=0, int i=1):m_d(d), Mother(i) {
cout << "Child Constructor Executed " << m_d << endl;
cout << "Size = " << sizeof(*this) << endl;
}
~Child() {
cout << "Child Destructor executed " << endl;
}
};
int main() {
Child child(2,1);
}
Mother Constructor Executed 1
Size = 4
Child Constructor Executed 2
Size = 16
Child Destructor executed
Mother Destructor executed
11.5 상속과 접근 지정자
상속에서 사용하는 access specifier(접근지정자)
class {객체 이름} : {access specifier} {상위 객체 이름}
- 상위 객체에서 물려받을 멤버 변수의 접근지정자를 정의한다.
- 외부에서 해당 멤버변수를 사용할 수 있는지 정의할 수 있다.
class Base {
public:
int m_public = 0;
protected:
int m_protected = 1;
private:
int m_private = 2;
};
class Derived_public : public Base {
public:
Derived_public() {
m_public = 0;
m_protected = 10;
//m_private = 20;
}
};
class Derived_protected : protected Base {
public:
Derived_protected() {
m_public = 0;
m_protected = 10;
//m_private = 20;
}
};
class Derived_private : private Base {
public:
Derived_private() {
m_public = 0;
m_protected = 10;
//m_private = 20;
}
};
int main() {
Derived_public de_pub;
de_pub.m_public = 100;
Derived_protected de_pro;
//de_pro.m_public; // Error!
Derived_private de_pri;
//de_pri.m_private; // Error!
}
- protected는 상속받은 객체에서 사용할 수 있지만 외부에서 사용할 수 없다.
참고) Base(상위객체) - Derived_(상속받은 객체) - Grand_(하위 객체)
다시 한번 상속을 받아 하위 객체에서 접근권한을 확인해보자
class Grand_pub : Derived_public {
public:
Grand_pub() {
m_public;
}
};
class Grand_pri : Derived_private {
public:
Grand_pri() {
m_public; // Error!
}
};
11.6 유도된 클래스에 새로운 기능 추가하기
상속된 객체에서 상위 객체의 private멤버변수 값을 변경하고싶다 어떻게 할까?
상위객체에 SetValue멤버함수 구현 VS 상위객체 멤버변수를 protected로 구현
- 상위객체의 멤버변수는 상위객체 내부에서 관리함이 적절하다. 그러나 상속된 객체의 멤버 함수를 구현할 때 다음과 같은 상황을 마주할 수 있다. 상속된 객체의 여러 멤버 변수를 변경하면서 상위객체의 멤버변수 값을 함께 변경해야하는 상황이 생긴다.
1. 상위객체에 SetValue 멤버함수를 구현하여 사용하자
ㄴ 장점 : 상위 멤버변수의 값이 어떻게 관리되는지 SetValue 함수로 추적할 수 있다.
ㄴ 단점 : 멤버 변수를 변경하는데 오버헤드가 붙는다. 자원을 낭비한다.
2. 상위 객체 멤버변수를 protected로 지정하자
ㄴ 장점 : 상위 멤버변수에 접근하여 바로 값을 변경할 수 있다.
ㄴ 단점 : 상위 멤버변수 값이 어떻게 관리되는지 확인하기 까다롭다.
11.7 상속받은 함수를 오버라이딩 하기
상속받은 객체의 멤버함수명과 상위객체의 멤버함수명이 동일한 상황이다. 상속받은 객체의 멤버함수 내부에서 상위객체의 멤버함수를 호출하고 싶다면 어떻게 해야할까? 아래와 같이 상위 객체의 멤버함수를 내부에서 호출해주면 된다
class Base {
public:
void print() {
// do sth..
}
};
class Derived: public Base{
void print() {
Base::print();
// do sth..
}
};
friend로 지정된 함수는 상속된 객체의 외부에서 정의된 것과 동일하다. 상위객체의 멤버함수를 호출할 수 없다. 이때 static_cast<{해당 자료형}>( )을 사용하면 상위 객체의 멤버함수를 호출할 수 있다.
상속된 객체의 첫 주소로 시작되는 저장공간에 상위 객체가 자리잡음을 기억하자. 상위 객체는 반드시 상속된 객체보다 같거나 작은 자료형을 가지고 있다. 형변환으로 인한 truncation을 의도적으로 사용한다.
class Base {
public:
friend ostream& operator << (ostream& out, const Base &base) {
out << "Operator Overloading on Base";
return out;
}
};
class Derived: public Base{
public:
friend ostream& operator << (ostream& out, const Derived& derived) {
out << static_cast<Base>(derived) << endl; // tracation happend
out << "Operator Overloading on Derived"; // fitting to parent class
return out;
}
};
11.8 상속 받은 함수를 감추기
상속받은 함수를 외부에서 사용금지하기
(1) 객체 내부에서 using 활용하기
- 아래 예제를 보자. Base::m_i는 protected로 지정되어 상속받은 객체에서 접근할 수 있지만 외부에서 접근할 순 없다.
이 때 접근 지정자 내에서 이때 using keyword를 사용하여 public으로 변경할 수 있다. 이후 m_i는 public으로 설정되며 외부에서 접근가능하다. private로 설정할 수도 있고 상위 객체의 멤버함수에도 적용가능하다. 이때 함수명 뒤 parenthesis는 생략한다.
(2) 객체 내부에서 delete 사용하기
- 상위 객체에서 정의된 protected 혹은 public 멤버함수를 상속받은 객체에서 삭제할 수 있다. 더 이상 해당 멤버함수를 호출할 수 없다.
class Derived: public Base{
private:
double m_d;
public:
Derived(const int &i=0, const double &d=1.1): Base(i), m_d(d) {}
using Base::m_i; // now this parent's member v become public
private:
using Base::print; // now this parent's member f become private
void test() = delete; // now you can't call parent's member f
};
아래는 위를 활용한 예제이다
class Base {
protected:
int m_i;
public:
Base(const int &i): m_i(i){}
void print() {
cout << __FUNCTION__ << " is working value = " << m_i << endl;
}
void test() {
cout << "test test !!" << endl;
}
};
class Derived: public Base{
private:
double m_d;
public:
Derived(const int &i=0, const double &d=1.1): Base(i), m_d(d) {}
using Base::m_i; // now this parent's member v become public
private:
using Base::print; // now this parent's member f become private
void test() = delete; // now you can't call parent's member f
};
int main() {
Derived d;
d.m_i = -1;
d.print(); // Error! Derived class change access specifier with 'using'
d.test(); // Error! test function was no longer exist
}
11.9 다중 상속
Multiple Inheritance
아래 예제는 여러 상위 객체를 동시에 상속받는 객체를 보여준다. 상위 객체의 각 멤버를 모두 활용할 수 있다
- 중복된 이름을 가지고 있는 resolution operator( :: )를 활용하자
class USB {
private:
long m_id;
public:
USB(long id): m_id(id){}
long getID(){ return m_id; }
void plugAndPlay(){}
};
class Network{
private:
long m_id;
public:
Network(long id) : m_id(id) {}
long getID() { return m_id; }
void netWorking() {}
};
class USB_N_Network : public USB, public Network{
public:
USB_N_Network(long usb_id, long network_id): USB(usb_id), Network(network_id){}
};
int main() {
USB_N_Network versatile_device(0,2);
versatile_device.netWorking();
versatile_device.plugAndPlay();
versatile_device.getID(); // Error!
versatile_device.USB::getID(); // Try this one!
versatile_device.Network::getID(); // specifing Class!
}
'Programming Language > C++' 카테고리의 다른 글
[홍정모의 따라하며 배우는 C++] 13. 템플릿 (0) | 2021.12.30 |
---|---|
[홍정모의 따라하며 배우는 C++] 12. 가상함수들 (0) | 2021.12.28 |
[홍정모의 따라하며 배우는 C++] 10.객체들 사이의 관계에 대해 (0) | 2021.12.26 |
[홍정모의 따라하며 배우는 C++] 9.연산자 오버로딩 (0) | 2021.12.25 |
[홍정모의 따라하며 배우는 C++] 8. 객체지향의 기초 (0) | 2021.12.24 |