read

열일곱번째 열혈강의


program

지난번에 이어서 유도클래스에 대해 계속 알아보도록 하자.

이전 포스팅에서 유도 클래스의 생성과정에 대해 알아보았으니, 이제 유도 클래스의 소멸과정 에 대해 알아 보도록 하겠다.


유도 클래스의 소멸과정


유도 클래스의 생성과정에서는 생성자가 2번 호출됨을 알았으니, 소멸과정에서도 2번 호출이 될 것이라는 것을 유추할 수 있다. 그럼 간단한 예제부터 보도록 하자.

#include <iostream>
using namespace std;

class SoBase{
  private:
    int baseNum;

  public:
    SoBase(int n) : baseNum(n){
      cout << "SoBase() : " << baseNum << endl;
    }

    ~SoBase(){
      cout << "~SoBase() : " << baseNum << endl;
    }
};

class SoDrived : public SoBase{
  private:
    int derivNum;

  public:
    SoDerived(int n) : SoBase(n), derivNum(n){
      cout << "SoBased() : " << derivNum << endl;
    }

    ~SoDerived(){
      cout << "~SoDerived() : " << derivNum << endl;
    }
};

int main(void){
  SoDerived drv1(15);
  SoDerived drv2(27);
  return 0;
}

출력을 한번 보도록 하자.

SoBase() : 15
SoDerived() : 15
SoBase() : 27
SoDerived() : 27
~SoDerived() : 27
~SoBase() : 27
~SoDerived : 15
~SoBase() : 15

이제 결론을 도출해 보겠다.

유도 클래스의 객체가 소멸될 때에는, 유도 클래스의 소멸자가 실행되고 난 다음에 기초 클래스의 소멸자가 실행된다.

스택에 생성된 객체의 소멸순서는 생성순서와 반대이다.

여기서 중요한 사실은 기초 클래스의 소멸자도, 유도 클래스의 소멸자도 호출 된다는 것이다. 이러한 객체의 소멸특성때문에 상속과 연관된 클래스의 소멸자는 다음과 같은 원칙을 지켜서 정의해야 한다.

생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제한다.

다음 예제를 보자.

#include <iostream>
#include <cstring>
using namespace std;

class Person{
  private:
    char* 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(mymajor) + 1];
      strcpy(major, mymajor);
    }

    ~UnivStudent(){
      delete []makor;
    }

    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 선언과 세 가지 형태의 상속


이전에 멤버들에 대한 public과 provate 선언에 대해서는 설명했지만, protected에 관한 선언은 상속과 관련이 있기 때문에 미루어 두었었다. 이제 상속을 알게 되었으니 해당 부분에 대해 설명해 보겠다.

C++ 접근제어 지시자에는 private, protected, public 이렇게 3가지가 존재한다. 그리고 이들이 허용하는 접근의 범위는 다음과 같다.

private < protected < public

즉, public이 가장 허용범위가 넓고, private는 가장 좁다. 반면 protected는 그 중간에 해당한다. 하지만 private와 굉장히 유사하다.

다음 예제를 보도록 하자.

class Base{
  private:
    int num1;

  protected:
    int num2;

  public:
    int num3;
    void ShowData(){
      cout << num1 << ", " << num2 << ", " << num3;
    }
};

위 예제는 각각의 접근제어 지시자에 int 값이 주어졌다. 그러나 이 상태에서는 private와 protected의 차이가 없다. 둘 다 클래스의 외부에서는 접근이 불가능하지만, 클래스 내부에서는 가능 하기 때문이다. 하지만 이 클래스가 상속이 되면 조금 달라진다.

class Derived : public Base{
  public:
    void ShowBaseMember(){
      cout << num1;
      cout << num2;
      cout << num3;
    }
};

위 클래스는 Base 클래스를 상속받는다. 따라서 public인 num3는 접근이 가능하지만, num1은 private이기 때문에 접근이 되지 않는다. 그럼 protected는 어떨까?

protected로 선언된 멤버변수는 이를 상속하는 유도 클래스에서 접근이 가능하다.

이것이 private와 protected의 유일한 차이점이다. 참고로 이런 특성을 가지는 protected는 자주 사용하지 않는다. 물론 유도 클래스에게만 제한적으로 접근을 허용하므로 유용하게 사용할 수 있는 키워드지만, 기본적으로 기초 클래스와 이를 상속하는 유도 클래스 사이에도 '정보은닉'은 지켜지는 것이 좋다.


세가지 형태의 상속


다음 예시를 보자.

class Base{
  private:
    int num1;

  protected:
    int num2;

  public:
    int num3;
}

이제 이 클래스를 다음과 같이 protected로 상속해 보겠다. 참고로 상속의 유형을 설명하는데 있어서 유도 클래스의 멤버는 필요가 없어서 빈 상태로 정의한다.

class Derived : protected Base{
  //empty
}

위 클래스는 protected 상속이면 그 의미는 다음과 같다.

protected보다 접근의 범위가 넓은 멤버는 protected로 변경시켜서 상속하겠다.

그런데 접근 범위가 넓은 것은 public 멤버 뿐이니, protected 상속을 한 Derived 클래스느 다음의 형태가 된다.

class Derived : protected Base{
  private:
    int num1;
  protected:
    int num2;
  protected:
    int num3;
};

위의 예제는 사실 모든 멤버를 클래스안에 선언되었으므로 protected Base를 상속한다는 선언이 맞지 않다. 이는 보여주기 위함이므로 선언을 한 것일 뿐이고 정말 잘못된 부분이 하나 있다.

위 코드처럼 num1이 그대로 private가 된다면 이 멤버는 Derived 클래스 내에서 접근이 가능하다. 그러나 num1은 선언이 된 Base 클래스 이외의 영역에서 접근이 불가능 하므로 다음과 같이 하는 것이 옳다.

class Derived : protected Base{
  접근불가:
    int num1;
  protected:
    int num2;
  protected:
    int num3;
};

접근 불가라는 키워드는 없지만, 존재는 하되 접근이 불가능 하다는 의미에서 이렇게 적어 주었다. 그럼 어떻게 접근 제어 범위가 변경되는 지 알아보자.

#include <iostream>
using namespace std;

class Base{
  private:
    int num1;
  protected:
    int num2;
  public:
    int num3;

    Base() : num1(1), num2(2), num3(3){

    }
};

class Derived : protected Base{}; //empty

int main(void){
  Derived drv;
  cout << drv.num3 << endl;
  return 0;
}

이 예제는 drv.num3를 출력할 때 에러가 발생한다. 이유는 Base 클래스를 protected로 상속했기 때문이다. protected로 상속했기 때문에 public 변수인 num3는 Derived 클래스에 protected 멤버가 된다. 그리고 이로 인해 외부에서는 접근이 불가능한 멤버가 된 것이다.


private 상속


위에서 protected 상속에 대해 설명하였지만, 개념적으로 설명했으므로 public 상속과 private 상속에 대해서도 이해가 가능 했을 것이다. 따라서 예제를 한번더 읽어보자.

class Base{
  private:
    int num1;
  protected:
    int num2;
  public:
    int num3;
};

class Derived : private Base{
  //empty
}

private 보다 접근의 범위가 넓은 멤버는 private로 변경시켜 상속한다.

그런데 private보다 접근범위가 넓은 멤버는 나머지 둘 다이다. 따라서 형태는 다음과 같다.

class Derived : private Base{
  접근불가 :  
    int num1;
  private:
    int num2;
  private:
    int num3;
};

때문에 num2와 num3는 Derived 클래스 내에서만 접근 가능한 멤버가 된다. 그리고 다른 클래스가 이를 다시 상속한다면

class DeDerived : public Derived{
  //empty
};

Derived 클래스의 모든 멤버가 private 또는 접근불가 이므로 다음과 같다.

class DeDerived : private Derived{ 접근불가:
int num1; 접근불가: int num2; 접근불가: int num3; };

이렇게 private 상속이 이뤄진 클래스를 다시 상속하면 모두 접근 불가로 의미없는 클래스가 된다.


public 상속


이제 마지막으로 public 상속이다. 이미 많이 경험했으므로 정리하자면

public 보다 접근의 범위가 넓은 멤버는 public으로 변경시켜서 상속하겠다.

그런데 public은 가장 넓은 범위이므로 다음으로 정의하는 것이 좋다.

private를 제외한 나머지는 그냥 그대로 상속한다.

즉, private 멤버는 접근불가의 형태로 상속하지만, protected 멤버는 protected로, public멤버는 public으로 상속이 진행된다. 참고로 상속의 대부분은 public으로 진행이 된다. 실제로 public만 사용된다고 말하는 분도 있다. 따라서 public 이외의 상속은 다중상속과 같이 특별한 경우가 아니면 잘 사용하지 않는다.


상속을 위한 조건


smart

상속으로 클래스의 관계를 구성하기 위해서는 조건이 필요하다. 그리고 조건과 그에 따른 필요가 충족되지 않으면, 상속은 하지 않는 것만 못하다고 전문가들은 이야기 한다. 참고로 이어서 하는 얘기는 상속을 언제? 왜? 하는가에 대한 답이 되지 않는다. 이에 대한 답은 다음 챕터에서 확장성 문제를 해결하면서 내리고 여기는 상속을 위한 최소한의 조건이다.


상속을 위한 조건인 IS-A 관계 성립


상속의 기본 문법에서 보이듯이, 유도 클래스는 기초 클래스가 지니는 모든 것을 지니고, 거기에다가 유도 클래스만의 추가적인 특성이 더해진다. 그렇다면 현실 세계에서는 이러한 상황이 언제 연출이 될까? 다음 예시를 보자.

전화기 -> 무선 전화기

컴퓨터 -> 노트북 컴퓨터

무선이라는 이슈로 예를 들어보았다. 위 예에서 보인 전화기와 컴퓨터의 기본 기능은 각각 통화계산 이다. 그런데, 무선 전화기와 노트북 컴퓨터는 여기에 이동성 이라는 특성이 추가 되었다. 따라서 전화기와 컴퓨터를 기초 클래스롸, 무선 전화기와 노트북 컴퓨터를 유도 클래스로 정의하는 것은 매우 타당하다. 그리고 상속관계가 성립되면 다음과 같은 문장이 구성되는 특징이 있다.

무선 전화기는 일종의 전화기다.

노트북 컴퓨터는 일종의 컴퓨터다.

다시 말해 무선 전화기도 전화기고, 노트북도 컴퓨터다. 이 두 문장을 영어 반, 한글 반을 표현하면

무선 전화기 is a 전화기

노트북 컴퓨터 is a 컴퓨터

가 된다. 즉, 상속관계가 성리하려면 기초클래스와 유도클래스간에 IS-A 관계가 성립해야 한다. 만약 상속관계로 묶고 싶은 것들이 IS-A로 표현되지 않으면, 이는 상속관계가 아닐 확률이 크니 조심해야한다. 이를 예시를 들어 표현하되, 상속의 깊이(몇 단계를 걸쳐서 상속이 이루어지는가)를 생각해서 보이겠다.

#include <iostream>
#include <cstring>
using namespace std;

class Computer{
  private:
    char owner[50];

  public:
    Computer(char* name){
      strcpy(owner, name);
    }

    void Calculate(){
      cout << "요청 내용을 계산합니다." << endl;
    }
};

class NotebookComp : publc Computer{
  private:
    int Battery;
  public:
    NotebookComp(char* name, int initChag) : Computer(name), Battery(initChag){}
    void Charging(){Battery += 5;}
    void UseBattery(){Battery -= 1;}
    void MovingCal(){
      if(GetBatteryInfo() < 1){
        cout << "충전이 필요합니다." << endl;
        return ;
      }
      cout << "이동하면서 ";
      Calculate();
      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;
      }
      if(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;
}

이 예제에서 보인 TabletNotebook 클래스의 객체 생성과정에는 상속하는 두 클래스의 생성자가 모두 호출된다. 이 과정은 이미 우리가 공부한 부분이다.

이는 위에서 말한 IS-A 관계도 자연스럽게 연결된다.

노트북 컴퓨터는 컴퓨터이다

태블릿 컴퓨터는 노트북 컴퓨터이다

또한 한가지 더 하자면

태블릿 컴퓨터는 컴퓨터이다

도 성립이 되는 것으로 상속관계가 적절하다는 것이 나온다.

해당 사항에 대해 UML(Unified Modeling Language) 그림을 본다면 더욱 보기 쉬울 것이다.


HAS-A 관계도 상속의 조건은 되지만 복합 관계로 대신하는 것이 일반적이다.


이제 IS-A 관계 외에도 상속이 형성될만한 관계가 있을 수 있다는 질문을 할 수 있다.

물론 한가지가 더 있다. 바로 소유의 관계 이다. 상속의 기본문법이 보여주듯, 유도 클래스는 기초 클래스가 지니고 있는 모든 것을 소유한다. 따라서 다음과 같이 표현이 될 수 있다.

#include <iostream>
#include <cstring>
using namespace std;

class Gun{
  private:
    int bullet;

  public:
    Gun(int bnum) : bullet(bnum){}

    void Shut(){
      cout << "BANG!!" << endl;
    }
};

class Police : public Gun{
  private:
    int handcuffs;
  public:
    Police(int bnum, int bcuff) : Gun(bnum), handcuffs(bcuff){}
    void PutHandcuff(){
      cout << "CNAP!" << endl;
      handcuffs--;
    }
};

int main(void){
  Police pman(5, 3);
  pman.Shut();
  pman.PutHandcuff();
  return 0;
}

이 예제는 권총을 소유하는 경찰을 표현하고 있다. 따라서 이를 영어 반, 한글 반 섞어서 표현하면 경찰 has a 총 이 된다. 즉, HAS-A 관계도 상속으로 표현할 수 있다. 그런데 이러한 소유의 관계는 다른 방식으로도 얼마든지 표현이 가능하다. 다음 예제에서는 상속이 아닌 다른 방식을 표현한다.

    #include <iostream>
    #include <cstring>
    using namespace std;

    class Gun{
      private:
        int bullet;
      public:
        Gun(int bnum) : bullet(bnum){}
        void Shut(){
          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;
          handcuffs--;
        }

        void Shut(){
          if(pistol == NULL)
            cout << "Hut BBANG!" << endl;
          else
            pistol->Shut();
        }
        ~Police(){
          if(pistol != NULL)
            delete pistol;
        }
    };

    int main(void){
      Police pman1(5,3);
      pman1.Shut();
      pman1.PutHandcuff();

      Police pman2(0, 3); //권총을 소유하지 않음
      pman2.Shut();
      pman2.PutHandcuff();
      return 0;
    }

위 예제를 보면 어떻게 다른지가 보인다. 일반적인 상황에서는 앞서 보인 상속 기반의 예제보다 위의 에제가 보다 좋은 모델이다. 코드 양은 늘었지만 이 상황에서는 이는 큰 문제가 되지 않는다. 오히려 앞으로는 앞선 예제가 코드양이 훨씬 많아질 확률이 높다. 왜냐하면 다음 요구사항을 반영하기 쉽지 않기 때문이다.

권총을 소유하지 않은 경찰을 표현해야 한다.

경찰이 권총과 수갑뿐만 아니라, 전기봉도 소유한다.

상속으로 묶인 두 개의 클래스는 강한 연관성을 띈다 즉, Gun 클래스를 상속하는 Police 클래스로는 총을 소유하는 경찰만 표현이 된다. 그러나 NULL 값으로 넣어주는 상황이 있고 이를 두번째 예제에서는 매우 간단히 표현되었다. 또한 전기봉을 소유하려면 간단히 객체 하나만 만들어서 표현해 줄 수 있다. 그러나 첫 예제에서는 이러기가 쉽지가 않다. 결론을 내려보면 상속은 IS-A 관계의 표현이 매우 적절하다. 또한 경우에 따라 HAS-A 관계도 가능하나 많은 제약을 줄 수 있다.

이 이외의 관계는 없을까? 없다고 보면 된다. HAS-A도 굉장히 많이 봐준것이다.

오늘 포스팅은 여기까지 마무리하고 다음엔 OOP 단계별 프로젝트 05단계와 다형성을 하도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview