[c++] 상속(Inheritance)의 이해



상속에 관한 내용을 다룬다.

윤성우의 열혈 C++ 교재를 바탕으로 작성되었다.


상속에 들어가기에 앞서

👀 상속은 특히 적용이 중요한 문법이다. 따라서, 확실히 이해하고 적절한 때에 선별적으로 적용할 수 있도록 노력하자!😉

📌 상속(Inheritance)의 이해를 위한 접근 방식 제안

​ ✔1단계 : 문제의 제시

​ 상속과 더불어 다형성의 개념을 적용해야만 해결 가능한 문제를 먼저 제시한다.

​ ✔2단계 : 기본 개념 소개

​ 상속의 문법적 요소를 하나씩 소개해 나간다. 그리고 그 과정에서 앞서 제시한 문제의 해결책을 함께 고민한다.

​ ✔3단계 : 문제의 해결

​ 처음 제시한 문제를, 상속을 적용하여 해결한다.✨

📌 상속에 대한 새로운 관점의 이해

과거에는 “기존에 정의해 놓은 클래스의 재활용을 목적으로 만들어진 문법적 요소가 상속이다.”라는 관점으로 상속을 바라보았다.

📌 문제 제시를 위한 시나리오 도입

상속의 이해를 위한 예시 클래스 스크립트.cpp

​ 🔎 가상의 문제를 제시 👉 A회사가 운영하는 ‘급여관리 ‘시스템이고, 직원의 근무형태는 ‘정규직’하나이다. 이 시스템은 정규직 직원을 관리하기 위한 형태로 설계되었다.

class PermanentWorker //'정규직'이라는 객체: 데이터적 성격이 강하다.
{
private:
    char name[100];
    int salary; //매달 지불해아 하는 급여액
public:
    PermanentWorker(char* name, int money)
        :salary(money)
    {
       strcpy(this->name name);     
    }
    int GetPay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        cout<<"name: "<<name<<endl;
        cout<<"salary: "<<GetPay()<<endl<<endl;
    }
};
class EmployeeHandler //handler->기능적 성격이 강하다.
{
private:
    PermanentWorker* empList[50];
    int empNum;
public:
    EmployeeHandler():empNum(0)
    { }
    void AddEmployee(PermanentWorker* emp)//정규직 class를 인수로 받음
    {
        for(int i=0; i<empNum;i++)
            empList[i]->ShowSalaryInfo();
    }
    void ShowTotalSalary() const
    {
        int sum=0;
        for(int i=0; i<empNum; i++)
            sum+=empList[i]->GetPay();
        cout<<"salary sum: "<<sum<<endl;
    }
    ~EmployeeHandler()
    {
        for(int i=0; i<empNum; i++)
            delete empList[i];
    }
}

👉 class EmployeeHandler는 기능적 성격이 강한데, 이렇게 기능의 처리를 실제로 담당하는 클래스를 가리켜 ‘컨트롤(control) 클래스’또는 ‘핸들러(handler)클래스’라 한다.

위의 두 클래스를 기반으로 작성된 main함수.cpp

int main(void)
{
    // 직원관리를 목적으로 설계된 컨트롤 클래스의 객체 생성
    EmployeeHandler handler;
    
    // 직원 등록
    handler.AddEmployee(new PermanentWorker("Kim",1000));
    handler.AddEmployee(new PermanetWorker("Lee",1500));
    handler.AddEmployee(new PermanentWorker("Jun", 2000));
    
    // 이번 달에 지불해야 할 급여의 정보
    handler.ShowAllSalaryInfo();
    
    // 이번 달에 지불해야 할 급여의 총합
    handler.ShowTotalSalary();
    return 0;
}

📌 문제의 제시

​ 🔎 앞서 만든 시스템을 사용하던 A회사가 다음과 같은 요구를 했다.

” 부서가 세분화 되어, 직원이 늘면서 직원의 고용형태가 달라졌다. 영업직,임시직이 추가 되었다.”

👉 단순히 종류가 늘은 것이 아닌 고용직의 급여, 영업직의 급여, 임시직의 급여가 각각 다른 계산방식으로 적용되어야 한다.

이러한 문제는 클래스만 늘린다고 해결 되는 것이아니다. 각각의 클래스에서의 함수, 그리고 그것들을 관리하는 배열들도 각각 생성해야한다.

🤔 결과적으로 전체 코드를 바꿔야한다. --- ## 상속의 문법적인 이해 📌 상속이란? > UnivStudent 클래스가 Person 클래스를 상속한다. 👉 `UnivStudent `클래스가 `Person `클래스를 상속하게 되면, UnivStudent 클래스는 Person 클래스가 지니고 있는 모든 멤버를 물려받는다. 즉, UnivStudent 객체에는 UnivStudent 클래스에 선언되어 있는 멤버뿐만 아니라 Person 클래스에 선언되어 있는 멤버도 존재하게 된다. 📌상속의 방법과 그 결과 `class Person 예시.cpp` ```c++ class Person { private: int age; //나이 char name[50]; //이름 public: Person(int myage, char * myname) : age(myage) { strcpy(name, myname); } void WhatYourName() const { cout<<"My name is "<<name<<endl; } }; ``` `class Univstudent 예시.cpp` ```c++ class UnivStudent : public Person //Person 클래스의 public 상속을 의미함 { private: char major[50]; // 전공과목 public: UnivStudent(char * myname, int myage, char * mymajor) : Person(myage, myname) { strcpy(major, mymajor); } void WhoAreYou() const { WhatYourName(); HowOldAreYou(); //클래스 내부에 정의 되어 있지 않아도 상속받은 Person클래스의 멤버함수이기 떄문에 호출이 가능하다. cout<<"My major is"<<major<<endl<<endl; } }; ``` 💡 `상속`을 하게 되면 **상속의 대상이 되는 클래스의 멤버**까지도 **객체 내**에 **포함**이 된다. 📌 상속받은 클래스의 생성자 정의 ```c++ UnivStudent(char*myname, int myage, char * mymajor) : Person(myage, myname) //member initialize, Person의 멤버를 초기화 하기 위한 인자의 전달 까지 요구하고 있다. { strcpy(major, mymajor); //멤버변수에 복사 } ``` ❔ **UnivStudent의 생성자는 Person 클래스의 멤버까지 초기화해야 할 의무가 있을까?** ​ UnivStudent 객체가 생성되면, 그 객체 안에는 다음의 멤버변수가 존재하게 된다. - Person의 멤버변수 : age, name - UnivStudent의 멤버변수: major ​ _따라서, UnivStudent 생성자는 Person 클래스의 멤버까지 초기화해야할 의무가 있다._ 💡 UnivStudent 생성자가 Person 클래스의 생성자를 호출해서 Person 클래스의 멤버를 초기화 하는 것이 좋다!😉 📌 상속관련 완전한 예제의 확인 및 실행 ```c++ #include #include using namespace std; class Person { private: int age; //나이 char name[50]; //이름 public: Person(int myage, chsr * myname) : age(myage) { strcpy(name, myname); } void WhatYourName() const { cout<<"My name is "<<name<<endl; } void HowOldAreYou() const { cout<<"I'm"<<age<<" years old"<<endl; } }; class UnivStudent : public Person { private: char major[50]; //전공과목 public: UnivStudent(char * myname, int myage, char * mymajor) : Person(myage, myname) //Person 객체의 'private'멤버이기 떄문에 public 함수를 통해 간접적으로 접근해야 한다. { strcpy(major, mymajor); } void WhoAreYou() const { WhatYourName(); HowOldAreYou(); cout<<"My major is "<<major<<endl<<endl; } }; int main(void) { UnivStudent ustd1("Lee", 22, "Computer eng."); ustd1.WhoAreYou(); UnivStudent ustd2("Yoon", 21, "Electronic eng."); ustd2.WhoAreYou(); return 0; } ``` > ❔ UnivStudent 클래스의 멤버함수(또는 생성자) 내에서는 Person 클래스에 Private으로 선언된 멤버변수 age와 name에 접근이 가능한가? 💡 **접근 제한의 기준은 [클래스]() 이다.** 클래스 외부에서는 private 멤버에 접근이 불가능하다. 따라서 UnivStudent의 멤버함수 내에서는 Person의 멤버변수에 `직접접근`이 불가능하다. ​ ✔ `private`으로 선언된 멤버도 상속이 이루어 진다. ​ ✔ `직접접근`이 불가능 하기 때문에, Person클래스 내에 있는 `public함수`를 통해서 [간접적으로]() 접근 해야 한다. 👉 _**정보의 은닉**은 하나의 객체 내에서도 진행이 된다._ 📌 용어의 정리 | Person | ↔ | UnivStudent | | :----------------: | :--: | :------------------: | | 상위 클래스 | ↔ | 하위클래스 | | 기초(base) 클래스 | ↔ | 유도(derived) 클래스 | | 슈퍼(super) 클래스 | ↔ | 서브(sub) 클래스 | | 부모 클래스 | ↔ | 자식 클래스 | 👀 `기초 클래스`와 `유도 클래스`라는 말이 일반적으로 사용된다. 📌 유도 클래스의 객체 생성과정 ```c++ #include using namespace std; class SoBase { private: int baseNum; public: SoBase() : baseNum(20) { cout<<"SoBase()"<<endl; } SoBase(int n) :baseNum(n) { cout<<"SoBase(int n)"<<endl; } void ShowBaseData() { cout<<baseNum<<endl; } }; class SoDerived : public SoBase { private: int derivNum; public: SoDerived() : derivNum(30) //기초 클래스의 생성자에 대한 언급이 없다. { cout<<"SoDerived()"<<endl; } SoDerived(int n) : derivNum(n) //기초 클래스의 생성자에 대한 언급이 없다. { cout<<"SoDerived(int n)"<<endl; } SoDerived(int n1, int n2): SoBase(n1), derivNum(n2) //n1을 인자로 받는 기초클래스의 생성자를 직접 명시하고 있다. { cout<<"SoDerived(int n1, int n2)"<<endl; } void ShowDerivData() { ShowBaseData(); cout<<derivNum<<endl; } }; int main(void) { cout<<"case1.... "<<endl; SoDerived dr1; dr1.ShowDerivData(); cout<<"----------"<<endl; cout<<"case2..... " <<endl; SoDerived dr2(12); dr2.ShowDerivData(); cout<<"----------"<<endl; cout<<"case3..... "<<endl; SoDerived dr3(23, 24); dr3.ShowDerivData(); return 0; } ``` 💡 `유도 클래스의 객체생성 과정`에서 **기초 클래스의 생성자**는 100% 호출된다. 💡 유도 클래스의 생성자에서 [기초 클래스의 생성자 호출을 명시히지 않으면,]() 기초 클래스의 `void 생성자가 호출`된다. ✔ `유도 클래스`의 객체 생성과정에서는 생성자가 `두번` 호출된다.(기초 클래스의 생성자 & 유도 클래스의 생성자 ) > [클래스의 멤버는 해당 클래스의 생성자를 통해서 초기화 해야 한다.]() 📌 유도 클래스 객체의 소멸과정 ```c++ #include using namespace std; class SoBase { private: int baseNum; public: SoBase(int n) :baseNum(n) { cout<<"SoBase() : "<<baseNum<<endl; } ~SoBase() { cout<<"~SoBase() : "<<baseNum<<endl; } }; class SoDerived : public SoBase { private: int derivNum; public: SoDerived(int n) : SoBase(n), derivNum(n) { cout<<"SoDerived() : "<<derivNum<<endl; } ~SoDerived() { cout<<"~SoDerived() : "<<derivNum<<endl; } }; int main(void) { SoDerived drv(15); SoDerived drv(27); return 0; } //result /* SoBase() : 15 SoDerived() :15 SoBase() :27 SoDerived(): 27 ~SoDerived(): 27 ~SoBase() :27 ~SoDerived() :15 ~SoBase() : 15 */ ``` 💡 유도 클래스의 객체가 소멸될 때에는, 유도 클래스의 소멸자가 실행되고 난 다음에 기초 클래스의 소멸자가 실행된다. 💡 스택에 생성된 객체의 소멸순서는 생성순서와 **반대**이다. ✔`생성자`에서 **동적 할당**된 `메모리 공간`은 `소멸자`에서 **해제**한다. ```c++ #include #include using namespace std; class Person { private: cahr * name; public: Person(char * myname) { name = new char[strlen(myname)+1]; strcpy(name, myname); } ~Person() { delete []name; //소멸자에서 생성자에서 할당한 메모리 공간을 해제해도록 정의하였다. } void WhatYourName() const { cout<<"My name is "<<name<<endl; } }; class UnivStudent : public Person { private: char * major; public: UnivStudent(char * myname, char * mymajor) :Person(myname) { major = new char[strlen(mymajorj)+1]; strcpy(major, mymajor); } ~UnivStudent() { delete []major; //유도 생성자에서는 자신의 생성자에 할당된 메모리 공간에 대한 해제만 책임지고 있다. 기초 클래스의 소멸자가 호출이 되면서 기초 클래스의 생성자에서 할당한 메모리 공간을 해제하기 떄문이다. } void WhoAreYou() const { WhatYourName(); cout<<"My major is "<<major<,endl<<endl; } }; int main(void) { UnivStudent st1("Kim", "Mathmatics"); st1.WhoAreYou(); UnivStudent st2("Hong", "Physics"); st2.WhoAreYou(); return 0; } ``` --- ## protected 선언과 세 가지 형태의 상속 📌 protected로 선언된 멤버가 혀용하는 접근의 범위 ​ 🔎 **c++의 접근제어 지시자** : `private `, `protected` , `public` ​ 🔎 **허용하는 접근 범위** : `private ` < `protected` < `public` ```c++ class Base { private: int num1; protected: int num2; public: int num3; void ShowData() { cout<<num1<<", "<<num2<<", "<<num3; } }; class Derived : public Base { public: void ShowBaseMember() { cout<<num1; //compile error cout<<num2; //compile OK cout<<num3; //compile OK } } ``` 💡 `protected`로 선언된 멤버변수는 이를 상속하는 유도 클래스에서 접근이 가능하다. (반면에, `private`은 유도 클래스에서는 접근이 불가능하다.) 📌 세가지 형태의 상속 ```c++ class Derived : public Base { ..... } class Derived : protected Base { ..... } class Derived : private Base { ..... } ``` 📌 **protected** 상속 ```c++ class Base { private: int num1; protected: int num2; public: int num3; }; class Derived : protected Base { //empty! }; ``` `protected`상속이 의미하는 바 👉 _protected 보다 접근 범위가 넓은 멤버는 protected로 변경시켜서 상속하겠다._ _따라서 클래스는 다음과 같은 형태가 된다. ```c++ class Derived : protected Base { 접근불가: //num1은 private이기 때문에 유도 클래스에서는 아예 접근이 불가능하다. int num1; protected: int num2; public: int num3; }; ``` 📌 **private** 상속 ```c++ class Base { private: int num1; protected: int num2; public: int num3; }; class Derived : private Base { //empty! }; ``` `private`상속이 의미하는 바 👉 _private 보다 접근 범위가 넓은 멤버는 private로 변경시켜서 상속하겠다._ _따라서 `protected`와 `public`을 변경시켜, 클래스는 다음과 같은 형태가 된다. ```c++ class Derived : private Base { 접근불가: //num1은 private이기 때문에 유도 클래스에서는 아예 접근이 불가능하다. int num1; private: int num2; private: int num3; }; ``` 👉 num2와 num3는Derived 클래스 내에서만 접근이 가능한 멤버가 된다. 🤔 만약 다른 클래스가 Derived 클래스를 다음과 같이 상속한다면.............. ```c# class DeDerived : public Derived { //empty! }; ``` ```c++ class DeDerived : public Derived { 접근불가: int num1; 접근불가: int num2; 접근불가: int num3; }; ``` 👉 `private 상속이 이뤄진 클래스를 다시 상속할 경우` , 멤버함수를 포함하여 모든 멤버가 '접근불가'가 되기 때문에 사실상 의미 없는 상속이 되고 만다. 📌 **protected** 상속 > ✔ private을 제외한 나머지는 그냥 그대로 상속한다. > > ✔ C++의 상속은 public 상속만 있다고 생각해라. --- ## 상속을 위한 조건 📌 상속을 위한 기본 조건인 **IS-A** 관계의 성립 - 전화기 ➡ 무선 전화기 ( 무선 전화기는 일종의 전화기이다.= 무선 전화기 is a 전화기) - 컴퓨터 ➡ 노트북 컴퓨터 ( 노트북 컴퓨터는 일종의 컴퓨터이다.= 노트북 컴퓨터 is a 컴퓨터) ✔ 상속관계가 성립하려면 기초 클래스와 유도 클래스간에 `IS-A`관계가 성립해야 한다. `ISAInheritance.cpp 예제` ```c++ #include #include using namespace std; class Computer { private: char owner[50]; public: Computer(char * name) { strcpy(owner, name); } void Calculate() { cout<<"요청 내용을 계산합니다."<<endl; } }; class NotebookComp : ppublic Computer { privatet: int Battery; public: NoteBookComp(char * name, int initChag) :Computer(name), Battery(initChag) { } void Charging() { Battery +=5;} void UseBattery() {Battery -=1;} void MovinCal() { if(GetBatteryInfo()<1) { cout<<"충전이 필요합니다."<<endl; return; } cout<<"이동하면서 "; Calculater(); UseBattery(); } int GetBatteryInfo() { return Battery; } }; class TabletNotebook : public NotebookComp { private: char regstPenModel[50]; public: TabletNotebook(char * name, int initChag, char * pen) : NotebookComp(name, initChag) { strcpy(regstPenModel, pen); } void Write(char * penInfo) { if(GetBatteryInfo()<1) { cout<<"충전이 필요합니다."<<endl; return; } it(strcmp(regstPenModel, penInfo)!=0) { cout<<"등록된 펜이 아닙니다."; return; } cout<<"필기 내용을 처리합니다."<<endl; UseBattery(); } }; int main(void) { NotebookComp nc("이수종", 5); TabletNotebook tn("정수영", 5, "ISE-241-242"); nc.MovingCal(); tn.Write("ISE-241-242"); return 0; } ``` 📌 **HAS-A** 관계도 상속의 조건은 되지만 복합 관계로 이를 대신하는 것이 일반적이다. ✔ `소유`의 관계 `HASInheritance.cpp 예제` HAS-A 관계를 상속으로 표현한 예제이다. ```c++ #include #include using namespace std; class Gun { private: int bullet; //장전된 총알의 수 public: Gun(int bnum) : bullet(bnum) { } void Shot() { cout<<"BBANG!"<<endl; bullet--; } }; class Police : public Gun { private: int handcuffs; //소유한 수갑의 수 public: Police(int bnum, unt bcuff) : Gun(bnum), handcuffs(bcuff) { } void PutHandcuff() { cout<<"SNAP!"<<endl; handcuffs--; } }; int main(void) { Police pman(5,3); //총알 5, 수갑 3 pman.Shot(); pman.PutHandcuff(); return 0; } ``` ✏ `HASComposite.cpp 예제` ```c++ # include # include using namespace std; class Gun { private: int bullet; //장전된 총알의 수 public: Gun(int bnum) : bullet(bnum) { } void Shot() { cout<<"BBANG!"<<endl; bullet--; } }; class Police { private: int handcuffs; // 소유한 수갑의 수 Gun * pistol; //소유하고 있는 권총 public: Police(int bnum, int bcuff) : handcuffs(bcuff) { if(bnum>0) pistol=new Gun(bnum); else pistol=NULL; } void PutHandcuff() { cout<<"SNAP!"<<endl; handcufs--; } void Shot() { if(pistol == NULL) cout<<"Hut BBANG!"<<endl; else pistol ->Shot(); } ~Police() { if(pistol !=NULL) delete pistol; } }; int main(void) { Police pman1(5, 3); pman1.Shot(); pman1.PutHandcuff(); Police pman2(0, 3); //권총을 소유하지 않은 경찰 pman2.Shot(); pman2.PutHandcuff(); return 0; } ``` 💡 앞서 **HAS-A**관계를 상속으로 표현한 경우 - 권총을 소유하지 않은 경찰을 표현해야 함 - 경찰이 권총과 수갑뿐만 아니라, 전기봉도 소유하기 시작함. 과 같은 요구사항들을 반영하기가 쉽지 않다. _상속으로 묶인 두 개의 클래스는 강한 연관성을 띤다._ _상속은 IS-A 관계의 표현에 매우 적절하다. 그리고 경우에 따라서는 HAS-A 관계의 표현에도 사용될 수 있으나, 이는 프로그램의 변경에 많은 제약을 가져다 줄 수 있다._




© 2021.08. by Yeram522

Powered by theorydb