ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 상속(심화), 두종류binding, virtual
    플밍/C++ (overview) 2012. 1. 3. 23:12
    2006/08/16 03:03



    상속을 때에 맞춰서 잘~ 활용해보자.

     

    (문제 상황에 따른 좋은 클래스 디자인 모델의 집합을 가리켜 "Design Patterns"라 한다.)

     

    1. is-a 관계의 상속

     



     

    class Vehicle{

          ....

    };

     

    class Airplane : public Vehicle{

          ....

    };

     

    이처럼 표현하면 된다.

     

     

    2. has-a 관계의 상속

     
     


     

    is-a관계와 마찬가지로,

     

    class Police{

          ....

    };

    class Cudgel : public Police{
          ....
    };
     
    이렇게 표현해도 틀린건 아니지만,
     
    class Cudgel{
          ....
    };
     
    class Police{
          Cudgel ccc;
          ....
    };
     
    이렇게 확실히 포함관계로 표현해주는것이 더 좋다.
    이때 Police에 포함된 ccc를 클래스객체 멤버(혹은, 객체 맴버) 라고 한다.
     
    단, 이렇게 표현할땐 주의할점이 있다.
     
    Police에 포함된 Cudgel객체는 반드시 void생성자를 가지고 있어야 한다.
    왜냐하면 객체 멤버가 메모리 공간에 할당되는 과정에서 void생성자를 호출하기 때문이다.
     
    그리고 이렇게 객체 멤버를 가지게 하면, 그림으로 표현하면 Cudgel은 Police의 한 멤버가
    되는것이다.즉, 완전히 포함되어 버린다.
     
    다른 표현방식으로,
     
    class Cudgel{
          ....
    };
     
    class Police{
          Cudgel * ccc;
          ....
    };
     
    이렇게도 나타낼수 있는데, 이때는 어딘가에 있는 Cudgel를 Police가 가리키는 구조가 된다.
     
    그러나, 두 방식은 논리적으로는 똑같이, Police가 Cudgel을 가지고 있다는것을 보여준다.
     

    그리고, 가능한한 has-a 관계는 이처럼 객체멤버(혹은 객체포인터멤버)를 가지는 구조

    표현하는게 좋다. 보통 상속처럼 표현을 하면, 두 클래스간의 결합도(coupling)가 높아지기

    때문이다. 

    ========================================================================================
     
    상속된 객체와 포인터(레퍼런스)의 관계
     
     
    1. 객체 포인터
     
    객체의 주소값을 저장할수 있는것이 객체 포인터 이다.
     
    여기에 상속의 개념을 더하면, AAA<-BBB의 관계가 있을때,
     
    AAA객체포인터는 AAA객체의 주소 뿐 아니라, AAA를 상속하는 객체(AAA의 자식객체),
    즉, BBB의 주소도 저장이 가능하다.
     
    class Person{
    public:
           void sleep();
    };
     
    class Student : public Person{
    public:
           void study();
    };
     
    class ParttimeStd : public Student{
    public:
           void work();
    };
     
    위와 같은 클래스가 있을때,
     
    int main(){
           Person *p1 = new Student;
           Person *p2 = new ParttimeStd;
           p1->sleep();
           p2->sleep();
    }
     
    이같은 main문은 전혀 문제가 되지 않는다.
     
    하지만,
     
    int main(){
           Person *p1 = new Student;
           Person *p2 = new ParttimeStd;
           p1->study();
           p2->work();
    }
     
    이같은 main문은 세번째, 네번째 문장에서 컴파일 에러가 발생한다.
     
    우선 p1->study()를 보자.
     
    p1은 Person이라는 껍데기에 있긴 하지만, 실체는 Student이다.
    Student객체는 study()를 가지고 있는데, 왜 오류가 나는가?
     
    이유는 객체포인터의 권한에 있다.
     
    객체 포인터 자신의 멤버와, 자신보다 상위 클래스(들)의 멤버에만 접근할수있다.
     
    p1은 Person이라는 껍데기를 가지고 있기때문에, 컴파일러는 실제로 속에 뭐가 들었든지,
    sleep()밖엔 볼수가 없다. (여기선 Person이 가장 상위클래스인데, 만약 Person이 자기보다
    상위클래스를 가지게 된다면, 그 부모의 멤버를 볼수 있겠죠? 바로 윗 부모 뿐 아니라, 자기보다
    상위 클래스의 멤버는 모두 볼수 있다.)
     
    컴파일러 입장에서 생각을 하다보니, 이렇게 됐다.
    어찌보면 불합리할수도 있다.
    코딩을 하는 사람 입장에서는 껍데기는 뭔지, 안에 뭐가 들었는지 다 알고 있는데 말이다..
    하지만, 결국 이런 문법의 구성은 프로그래머를 위한것이니까 좋게좋게 생각하자. -_=;
     
     
    2. 객체 레퍼런스
     
    앞서 얘기한 것에서 '포인터'란 말을 '레퍼런스'로 바꾸기만 하면 된다.
    결국, 포인터나 레퍼런스나 뭘써도 기능은 똑같다.
     
    ========================================================================================
     
    overriding
     
    base 클래스에서 선언된 함수를 derived 클래스에서 재정의하는 것을 말한다.
     
    이때 overriding은 이전에 정의된 함수를 가리는(hide) 특성을 지닌다.
     
    즉, main에서 어떤 함수를 부르면 가장 최근에 재정의된 함수를 호출하게 된다.
     
    그럼 original 함수를 호출할수는 없나?
     
    아니다, 방법이 있다.
     
    첫번째는, 포인터를 이용하는 것이다. 정확히는 포인터의 시야를 이용하는 것이다.
     
    class AAA{
           func();
    };
     
    class BBB : public AAA{
           func(){
                  .....;
           }
    };
     
    main(){
           BBB* b = new BBB;
           b->func();             // BBB의 func()
     
           AAA* a = new AAA;
           a->func();             // AAA의 func()
    }
     
    두번째는, 범위지정연산자(::)를 이용하는 것이다.
     
    virtual를 배운뒤 자세히 알아보자.
     
     
    ========================================================================================
     
    virtual 함수
     
    class AAA{
           virtual void fct();
    };
     
    class BBB : public AAA{
           void fct();    // virtual void fct();
    };
     
    class AAA : public BBB{
           void fct();   // virtual void fct();
    };
     
    main(){
           BBB* b = new CCC;
           b->fct();
     
           AAA* a = b;
           a->fct();
    }
     
    메모리 상태를 그림으로 나타내면 다음과 같다.
     
     



    후에 또 얘기하겠지만, virtual의 특성을 대해 다시 한번 새겨두자.
     
    첫째, 호출하고자 하는 함수가 virtual일 경우,
    객체 자신내에서 그것을 override하는 함수가 있을때는, 그 함수를 호출한다.
    (객체포인터의 권한을 넘어설수 있다.)
     
    둘째, virtual 함수를 override하는 함수는 virtual 키워드를 붙이지 않아도 자동으로 virtual로
    override한다.
     
    셋째, 클래스의 선언과 구현을 따로 두는경우,
    virtual키워드는 선언할때만 붙여야 한다. 구현할때도 똑같이 virtual을 붙여두면 컴파일오류난다.
     
     
    ========================================================================================
     
    static binding & dynamic binding
     
    class AAA{
           virtual void fct();
    };
     
    class BBB : public AAA{
           void fct();
    };
     
    main(){
           BBB b;
           b.fct();         // static binding
     
           AAA* a = new BBB;
           a->fct();      // dynamic binding
    }
     
    a는 동적 할당이 되기때문에, 컴파일 time에 컴파일러는 a안에 뭐가 들어갈지 모른다.
    (바보가? -_-;)
     
    따라서 a->fct()에서 fct()는 runtime때 어떤 fct()를 수행할것인가를 결정한다.
    이를 dynamic binding이라 한다.
     
    ========================================================================================
     
    virtual로 overriding된 함수 호출하기
     
    original 함수는 가려지기 때문에 그냥은 호출할수 없다고 했다.
     
    위에서 포인터를 이용하는것 말고, 범위지정연산자를 이용한다고 했는데, 알아보자.
     
    class AAA{
           virtual func(){
                  cout << "AAA" << endl;
           }
    };
     
    class BBB : public AAA{
           func(){
                  AAA::func();        // 방법 1.
                  cout << "BBB" << endl;
           }
    };
     
    main(){
           AAA* a = new BBB;  
     
           //첫번째 시도     
           a->func();  
              
           //두번째 시도
           a->AAA::func();     // 방법 2.
    }
     
    --실--행--결--과-------
       //첫번째 시도
       AAA
       BBB
     
       //두번째 시도
       AAA
    -----------------------
     
    a의 func()를 호출하려고 별 방법을 다 쓴다.
     
    첫번째 시도를 보면, 실행 결과가 AAA가 먼저 불려진다음 BBB가 불려진다.
     
    과정을 살펴보면, AAA타입의 포인터 이기 때문에 자식은 볼수가 없다. 그래서 자기가 갖고있는
     
    func()로 가는데, virtual이기 때문에, 이를 overriding하고 있는 BBB의 func()로 간다.
     
    갔더니 AAA::func()를 우선 수행하라고 되어 있기때문에, AAA를 출력하고,
     
    그리고나서 BBB를 출력하게 된다.
     
    두번째 방법은... -_-; 이런 문장이 들어간 플그램은 디자인 자체를 의심해 봐야 한다고
     
    책에 적혀있구나. ㅎㅅㅎ
     
    ========================================================================================

     

    순수 가상 함수와 추상 클래스

     

    class ABC{

           int a;

           char b;

           virtual void func() = 0;

    };

     

    함수 끝에 = 0 이라고 붙여주면, 그 함수는 껍데기만 선언하겠다는 말이다.

     

    상속과 virtual을 쓰다보면, 어쩔수없이 내용은 없더라도 껍데기라도 그자리에 있어야 하는

    경우가 있다. 그런 경우에 쓰는거다.

    여기선 virtual 이 붙었고, body가 없으므로 순수가상함수라고 이름붙여준다.

     

    컴파일러에게 "func()함수는 호출될 일이 없거든. 그래서 일부러 선언만 하고 정의는 하지 않은

    거야. 실수가 아니야. 그러니까 컴파일 오류를 발생시키면 안돼!" 라고 말하는것과 같다.

     

    이렇게 클래스의 멤버 함수 중 하나 이상이 순수가상함수인 경우,

    이 클래스를 추상(abstract)클래스라고 한다.

     

    !! 추상 클래스는 객체화 하지 못한다.

    함수의 body(정의)가 생략되었기때문에 완전한 클래스가 아니다.

    따라서 객체를 생성하지 못하는것은 당연하다.

     

    프로젝트에서 Employee클래스가 이런 클래스인데, 문제 될것은 없다.

    객체화될 필요가 없는 클래스이기 때문이다.

    (객체화 되었다면 프로그래머의 실수일 가능성이 높다.)

     
    ========================================================================================
     
    virtual 소멸자의 필요성
     
    1. 상속하고 있는 클래스의 객체 소멸 시 문제점
     
    class AAA{
           AAA();
           ~AAA();   
    };
     
    class BBB : public AAA{
           BBB();
           ~BBB();
    };
     
    main(){
           AAA * a = new BBB;
           BBB * b = new BBB;
     
           delete a;
           delete b;
    }
     
    --결과-------------------------------
    a의 AAA소멸자 호출
    b의 BBB소멸자 호출 -> AAA소멸자 호출
    -------------------------------------
     
    살펴보자.
    a는 실제로는 BBB를 가리키고 있는 AAA타입의 포인터이다.
    a를 해제할때는 BBB객체를 삭제하는 방식으로 해야 하는데,
    컴파일러는 a가 AAA타입이기때문에 안에 들어있는게 AAA객체라고 보고,
    AAA객체를 삭제하는 방식만 취하기 때문에 문제가 생겼다!!
     
    BBB소멸자가 호출되어야 하는데 어떻게 하면 될까?
     
    2. virtual 소멸자
     
    답은 바로 AAA(base)의 소멸자를 virtual로 선언하면 된다.
     
    그러고 나면 어떻게 될까?
     


    일단 AAA의 소멸자를 호출하려고 하는데,
     
    ~AAA가 virtual로 선언되어 있기때문에, derived클래스인 BBB의 소멸자를 호출한다.
     
    (* 이름은 다르지만, 둘다 소멸자이기 때문에 virtual함수를 overriding했을때와같이 반응한다.)
     
     
    ~BBB가 호출된 후 BBB는 AAA를 상속하고 있기때문에 base클래스인 AAA의 소멸자를
     
    호출한다. 이로써 AAA와 BBB의 소멸자가 모두 호출되었다.

    '플밍 > C++ (overview)' 카테고리의 다른 글

    연산자 오버로딩  (0) 2012.01.03
    virtual 응용(원리, 다중상속)  (0) 2012.01.03
    상속  (0) 2012.01.03
    modifier 종류 : const, static, explicit, mutable  (0) 2012.01.03
    복사 생성자  (0) 2012.01.03
Designed by Tistory.