Basic/C/C++2012.07.05 23:50


 

 

8강에 이어서 상속과 다형성에 대하여 계속 살펴보겠습니다.

 

[Static Binding 과 Dynamic Binding]

 

 *오버라이딩*

 

#include <iostream>

using namespace std;

 

class AAA

{

   public:

     void fct()

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

    BBB b;

    b.fct();

    return 0;

 }

 

 위 예제를 보시면 Base 클래스인 AAA의 맴버 함수로 fct()가 존재하고 AAA를 상속하는 Derived 클래스 BBB도 맴버 함수로 fct()가 정의되어 있습니다. 그러나 위 예제를 실행시켜보면 컴파일 오류없이 정상적으로 "BBB"가 출력되는걸 알 수 있습니다.

 

 이처럼 Base 클래스에 선언된 형태의 함수를 Derived 클래스에서 다시 선언하는 현상을 가리켜 오버라이딩(overriding)이라고 합니다. 즉 오버라이딩은 이전에 정의된 함수를 숨기는 특성을 지닌다고 이야기할 수 있습니다. 아 그리고 오버로딩과 오버라이딩을 혼돈해서는 안됩니다! 기억이 나지 않으시면 복습하세요~

 

 그렇다면 상속되면서 오버라이딩된 함수는 호출이 불가능해진걸까요? 또 그건 아닙니다. 다음예제를 보겠습니다.

 

class AAA

{

   public:

     void fct()

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new BBB;

   b->fct();

 

   AAA* a = b;

   a->tct();

 

   delete b;

   return 0;

}

 

--->결과값

BBB

AAA

 

 볼드처리된 부분에서는 BBB객체를 생성하고 있고, 포인터 b를 이용해서 fct 함수를 호출하고 있습니다. BBB 객체를 BBB 포인터로 바라볼 경우 BBB 클래스 내에 선언된 맴버 함수 fct만 보이게됩니다. 때문에 첫 번째 함수 호출에서는 "BBB"가 출력이 되겠죠? 그리고나서 포인터 b가 지니고 있는 주소값을 포인터 a에 대입해줍니다. 포인터형이 서로다른대 가능하냐구요? 당연히 가능하겠죠? 이유는 8강에서 설명했을겁니다. BBB 클래스는 AAA 클래스를 상속하기 때문에 AAA* 는 AAA와 BBB 객체의 주소값 모두를 표현할 수 있습니다. 그리고 마지막으로 포인터 a를 가지고 fct()함수를 호출하고 있습니다.

 

 이와 같이 오버라이딩되어 가려진 함수지만 AAA의 포인터로 접근할 경우에는 AAA 클래스의 fct함수가, BBB의 포인터로 접근할 경우에는 BBB 클래스의 fct함수가 호출됩니다. 호출되지 않는 함수가 가려지는거겠구요~

 

*맴버함수의 가상선언*

 

 virtual 키워드를 통해 가상으로 선언하는 방법을 살펴보겠습니다.

 

class AAA

{

   public:

     virtual void fct() // 가상함수

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new BBB;

   b->fct();

 

   AAA* a = b;

   a->tct();

 

   delete b;

   return 0;

}

 

 virtual 키워드를 사용함으로써 fct()함수를 가상 함수로 선언해주었습니다. 그러나 실행결과는

 

BBB

BBB

 

 가 출력이 됩니다. 아까 전과는 조금 다른 결과값을 나타내죠? AAA 타입의 포인터로 객체를 가리키건, BBB 타입의 포인터로 객체를 가리키건 오버라이딩된 함수만 호출되고 있습니다. 위 예제에서는 AAA 클래스에 선언되어 있는 fct 함수를 가상의 함수, 즉 존재하지 않는 함수로 선언하고 있습니다.

 

class AAA

{

   public:

     virtual void fct() 

     {

          cout<<"AAA"<<endl;

     }

 };

 

class BBB : public AAA

{

  public:

     void fct() //virtual void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

class CCC : public BBB

{

  public:

     void fct() //virtual void fct()

    {

        cout<<"BBB"<<endl;

    }

 };

 

int main(void)

{

   BBB* b = new CCC;

   b->fct();

 

   AAA* a = b;

   a->fct();

 

   delete b;

   return 0;

}

 

---->결과값

CCC

CCC

 

 AAA 클래스의 fct 함수는 virtual 선언되어 있고, BBB 클래스의 fct 함수가 오버라이딩 하고있습니다. 이런 경우 BBB 클래스의 fct 함수도 가상함수가 됩니다. 즉 BBB 클래스 선언에서의 void fct() 가 virtual void fct()가 되는겁니다. 마찬가지로 CCC 클래스의 fct 함수가 BBB 클래스의 fct 함수를 오버라이딩 하고 있습니다. 왜냐하면 BBB 클래스의 fct 함수가 가상함수이기 때문이죠!

 

 이렇게되면 최종적으로 오버라이딩한 함수를 제외한 나머지 함수는 가려지게 됩니다. AAA, BBB에 있는 함수는 가려지게되고 CCC 클래스에 선언된 fct 함수만 보이게 됩니다. b->fct() 에서는 BBB 타입의 포인터를 가지고 fct 함수를 호출하고 있습니다. 따라서 BBB 클래스의 맴버 함수 fct 를 호출하는 데서부터 시작은 이뤄집니다. 다만 함수가 가상함수이므로 오버라이딩한 함수가 호출이 되겠죠? 

 

 자 다시한번 정리해봅시다. 만약 BBB 클래스의 포인터형(BBB*)으로 b를 선언하고 b->fct() 를 호출하면 BBB 에있는 fct()가 호출이 되어야 하지만 virtual 함수이므로 BBB의 fct() 대신 CCC의 fct() 함수가 호출됩니다. 마찬가지로 a->fct() 를 호출하면 AAA의 fct() 대신 BBB의 fct()가 호출되는대 이 함수도 virtual 함수이므로 다시 CCC 클래스의 fct()함수가 호출됩니다.

 

 [static binding 과 dynamic binding]

 

 스태틱 바인딩과 다이나믹 바인딩이 무엇인지 우리는 이미 알고 있습니다.

 

class AAA

{

  public:

    virtual void fct()

    {

      cout<<"AAA"<<endl;

    }

};

 

class BBB : public AAA

{

  public:

    void fct()

    {

       cout<<"BBB"<<endl;

    }

};

 

int main(void)

{

  BBB b;

  b.fct(); //static binding

 

  AAA* a = new BBB;

  a->fct(); //dynamic binding

  return 0;

}

 

 b.fct() 의 경우 BBB 클래스에 선언되어 있는 fct 함수가 호출됩니다. 하지만 a->fct() 함수는 상황에 따라서 달라집니다. 포인터 a가 가리키는 것이 AAA 객체라면 AAA클래스의 fct 함수가 호출되겠지만, BBB 객체라면 BBB 클래스의 fct 함수가 호출이 될것입니다. 왜냐하면 AAA클래스의 맴버 함수 fct가 virtual로 가상함수 선언이 되었기 때문이죠. 이러한경우 static binding, dynamic binding 이라고 정의합니다. 이러한 dynamic binding은 다형성의 하나의 예 입니다. 쉽게 얘기하자면 모습은 같은데 형태는 다른겁니다. a라는 이름의 포인터가 가리키는 대상에 따라서 기능의 형태가 다르게 나타나는거죠. 이러한 다형성의 예를 하나 더 들어보자면 ' * ' 연산자가 있을겁니다. 상황에 따라서 곱셈 연산자로 또는 포인터 연산자로 사용하기 때문입니다.

 

 

 이번에는 범위 지정 연산자(::)를 통해서 오버라이딩된 함수를 호출해보는 예제를 살펴봅시다.

 

class AAA

{

   pulbic:

      virtual void fct()

       {  cout <<"AAA"<<endl; }

};

 

class BBB : public AAA

{

    public:

       void fct()

          {

             AAA::fct();

             cout<<"BBB"<<endl;

           }

};

 

int main(void)

{

   AAA* a = new BBB;

   a->fct();

 

   cout<<endl;

 

   a->AAA::fct();

   return 0;

}

 

 결과값 --->

AAA

BBB

 

AAA

 

 첫 번째 볼드처리된 부분에서의 함수 호출은 AAA 클래스에 선언되어 있는 fct 함수를 호출하라는 의미입니다. 따라서 BBB 클래스의 fct() 함수가 호출되면 AAA 클래스의 fct() 함수도 호출이 될겁니다.

 두 번째 볼드처리된 부분의 형태로도 Base 클래스에 존재하는 오버라이딩된 함수의 호출이 가능합니다. 결과값을 확인해보시길 바랍니다~

 

 자 우리는 앞서 얘기한 Employee Problem 문제를 virtual 함수를 이용하여 해결할 수 있습니다. 수정된 부분은 다음과 같습니다.

 

class Employee

{

  protected:

    char name[20];

  public:

    Employee(char* _name);

    const char* GetName();

    virtual int GetPay()

    {  return 0;  }

};

Employee::Employee(char* _name)

{

  strcpy(name,_name);

}

const char* Employee::GetName()

{

   return name;

}

 

 단지 GetPay 함수가 가상 함수로 선언되어 추가로 들어간 것 밖에 없습니다. 그러나 우리는 급여 리스트 출력 부분에서 주석처리된 부분 ( cout<<"salary: "<<empList[i]->GetPay()<<endl; ) 을 이러한 방법으로 해결 할 수 있습니다. Department 클래스에 선언되어 있는 empList 는 Employee 포인터 배열이지만 정작 가리키고 있는 대상들은 Permanent 객체이거나 Temporary 객체 였습니다. 문제는 Permanent 객체와 Temporary 객체는 GetPay 함수를 지니고 있지만, Employee 클래스에는  GetPay 함수가 선언되어 있지 않다는 문제 때문에 결국 컴파일 에러를 발생시킨 것 이었죠.

 

 즉, 가상 함수로 선언했기 때문에 Employee 클래스의 GetPay 함수가 호출되는 것이 아니라, 오버라이딩한 Derived 클래스의 GetPay 함수가 호출됩니다.

 

class Employee

{

  protected:

    char name[20];

  public:

    Employee(char* _name);

    const char* GetName();

    virtual int GetPay()=0;

};

Employee::Employee(char* _name)

{

  strcpy(name,_name);

}

const char* Employee::GetName()

{

   return name;

}

 

 위와 같이 선언되어 있는 형태를 순수가상함수라고 하는데 별건 아닙니다. 단지 컴파일러에게 GetPay 함수는 호출될 일이 없고, 일부러 선언만하고 정의는 하지 않는 함수라고 알려주는 겁니다.

 

[virtual 소멸자]

 

 가상함수 말고도 소멸자에 virtual 키워드를 사용할 수 있습니다. 과연 어떠한 상황에서 소멸자에 virtual 키워드를 사용해야 할까요? 다음 예제를 살펴보도록 하겠습니다.

 

class AAA

{

  char* str1;

public:

  AAA(char* _str1)

  {

     str1 = new char[strlen(_str1)+1];

     strcpy(str1, _str1);

   }

   ~AAA()

  {

    cout<<"~AAA() call "<<endl;

    delete []str1;

   }

   virtual void ShowString()

   {

     cout<<str1<<' ';

    }

};

 

class BBB : public AAA

{

   char* str2;

public:

   BBB(char* _str1, char* _str2) : AAA(_str1)

   {

      str2 = new char[strlen(_str2)+1];

      strcpy(str2, _str2);

   }

   ~BBB()

   {

     cout<<"~BBB() call"<<endl;

     delete []str2;

   }

   virtual void ShowString()

   {

       AAA::ShowString();

       cout<<str2<<endl;

    }

};

 

int main()

{

   AAA* a = new BBB("Good", " evening");

   BBB* b = new BBB("Good", "morning");

 

   a->ShowString();

   b->ShowString();

 

   delete a;

   delete b;

   return 0;

}

 

 결과값---->

Good evening

Good morning

~AAA() call

~BBB() call

~AAA() call

 

 조금만 주의깊게 살펴보시면 위 코드를 이해하는데 별다른 무리는 없을겁니다. 결과값을 보시면 BBB 클래스의 소멸자가 한번 덜 호출되었다는 것을 알 수 있습니다. 볼드 처리된 부분이 문제입니다. AAA 타입 포인터 a로 BBB 객체를 가리키고 있습니다. 그리고 포인터 a를 통한 BBB 객체의 소멸을 시도하고 있습니다. 이러한 과정에서 문제가 생긴겁니다. 왜냐하면 포인터 a가 가리키는 객체는 BBB 객체이지만 AAA타입의 포인터로 가리키고 있기 때문에 컴파일러는 BBB 객체를 AAA 객체로 인식하여 AAA 클래스의 소멸자만이 호출된겁니다.

 

 이러한 문제의 해결법은 간단합니다.

 

virtual ~AAA()

{

  cout<<"~AAA() call"<<endl;

  delete []str1;

}

 

으로 변경해주면 됩니다.

 

변경 후의 결과값은

 

Good evening

Good morning

~BBB() call

~AAA() call

~BBB() call

~AAA() call

 

 일단 AAA 클래스의 소멸자를 호출하려 할겁니다. 그러나 소멸자가 가상 함수로 지정되었으므로 Derived 클래스의 소멸자를 우선 호출하게 됩니다. 그 다음에는 BBB 클래스의 소멸자는 AAA 클래스를 상속하고 있기 때문에 다시 AAA 클래스의 소멸자가 호출됩니다. 이해되셨죠?

 

 이번 강은 여기까지 입니다. 다음 강에서는 virtual 의 추가적인 내용과 다중 상속을 공부해보도록 하겠습니다~!

 

 

 

 

 

Posted by boxbop@gmail.com boxbop