E D R S I H C RSS
ID
Password
Join
필요가 발명의 어머니라면, 불만은 진보의 아버지. ―데이비드 록펠러


Contents

1 시작하기전에
2 부모 클래스를 가리키는 포인터 변수
3 가상(Virtual) 맴버 메소드
4 추상 클래스 (Abstract base class)
5 마치며

1 시작하기전에 #

이 튜토리얼을 완벽하게 이해하려면, 포인터와 상속에 대한 지식이 필요함을 밝힙니다. 만약 아래 표현이 무엇을 의미하는지 긴가민가할 경우(^^), 부족한 부분을 공부하시고 읽어야할 것으로 생각합니다:
int a::b(c) {};    // 클래스 맴버함수의 선언
a->b = ...;        // 객체 포인터 변수에 대한 맴버참조
class a: public b; // 상속 선언

2 부모 클래스를 가리키는 포인터 변수 #

상속된 클래스들의 잇점중 하나는 자식클래스 타입의 포인터는 부모클래스 타입의 포인터와 데이터 타입의시점에서 서로 호환된다는 점이다. (다시말하면, 부모클래스 포인터는 자식클래스 포인터로 캐스팅이 가능하다) 이 튜토리얼에서는 이 강력한 C++의 특성의 잇점을 활용하는 것을 설명한다. 실제 예를 들어, 사각형과 삼각형에 대한 프로그램을 재작성함으로써 이 특성을 설명할 예정이다. :

// 부모 클래스를 가리키는 포인터 변수 
#include <iostream.h>

class CPolygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
  };

class CRectangle: public CPolygon {
  public:
    int area (void)
      { return (width * height); }
  };

class CTriangle: public CPolygon {
  public:
    int area (void)
      { return (width * height / 2); }
  };

int main () {
  CRectangle rect;
  CTriangle trgl;
  CPolygon * ppoly1 = &rect;
  CPolygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << rect.area() << endl;
  cout << trgl.area() << endl;
  return 0;
}
main 함수는 CPolygon 객체를 가리키는 2개의 포인터 변수들을 생성한다. (ppoly1과 ppoly2) 이들은 각각 rect와 trgl 변수들의 주소를 담고 있고, 이들은 둘다 CPolygon으로 부터 상속받은 상태이므로 적법한 대입(assign)이 가능하다.

rect와 trgl 대신에 ppoly1과 ppoly2를 사용하는데 있어 한가지 한계가 있다면, ppoly1과 ppoly2는 CPolygon* 타입으로 선언되어있는 상태이고 단지 CRectangleCTriangleCPolygon으로 부터 상속받은 맴버들만 참조가 가능하다는 것이다. 이것은 ppoly1과 ppoly2을 사용할 때 area()를 호출할 수 없는 이유가 된다.

CPolygon 클래스의 포인터 변수로 하여금 area()를 적법한 맴버 메소드로서 사용가능하게 하기 위해서는, 이 메소드를 부모 클래스인 CPolygon에서도 선언을 해야만 한다. 다음 섹션을 읽어보도록 하자.

3 가상(Virtual) 맴버 메소드 #

상속된 클래스 내에서 재정의되려고 하는 클래스 맴버를 부모클래스에 기술할 때에는, virtual 키워드를 앞에 붙여주어야한다. 이것은 상속된 자식의 메소드가 부모의 메소드에 겹쳐지면서, 부모 클래스의 메소드가 실행될 때, 같은 선언을 가진 자식 메소드가 실행되게 끔 만든다.

다음 예제를 보도록 하자:

// 가상(virtual) 맴버들
#include <iostream.h>

class CPolygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void)
      { return (0); }
  };

class CRectangle: public CPolygon {
  public:
    int area (void)
      { return (width * height); }
  };

class CTriangle: public CPolygon {
  public:
    int area (void)
      { return (width * height / 2); }
  };

int main () {
  CRectangle rect;
  CTriangle trgl;
  CPolygon poly;
  CPolygon * ppoly1 = &rect;
  CPolygon * ppoly2 = &trgl;
  CPolygon * ppoly3 = &poly;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  cout << ppoly1->area() << endl;
  cout << ppoly2->area() << endl;
  cout << ppoly3->area() << endl;
  return 0;
}

이제 3 클래스 모두 (CPolygon, CRectangle, CTriangle) 같은 맴버들(width, height, set_values(), area())을 가지게 되었다.

area()매소드는 차후에 자식 클래스에서 재정의되므로 virtual 키워드를 붙여 정의되었다. 한번 virtual 키워드를 삭제하고 컴파일 한 후 실행보면, 세가지 polygon들이 모두 0으로 출력되는 것을 볼 수 있다. (원래 virtual이 적혀져 있는 상태에서는 20,10,0으로 출력되었을 것이다.) 이것은 자식 클래스에 해당하는 area() 메소드가 아닌 부모 클래스의 area() 메소드가 호출되기 때문이다. (이것이 원래 정상이다. 실제 포인터의 주소에 해당하는 함수를 호출하는 것이기 때문이다.)

그러므로 virtual 키워드는 부모클래스가 포인터 변수로 사용될 때, 부모클래스에 virtual로 선언된 맴버함수와 이름이 같은 상속받은 자식클래스의 메소드를 호출하도록 지정한다. In fact what happens is that that base class member is placed virtually in it, since in derived classes can be replaced, as it is the case in our example.

CPolygon 타입의 객체를 선언하고 그 자체의 area() 메소드를 호출하는 것이 또한 가능하다는 것에 주의하라. (이때에는 실행결과로 0을 반환할 것이다.)

4 추상 클래스 (Abstract base class) #

기본적인 추상 클래스들은 이전의 예제인 CPolygon과 비슷한 것이라고 할 수 있다. 단지 차이점이라면 바로 전 예제에서는 CPolygon 객체들을 위해서 적법한 area() 메소드를 선언했지만, 추상 클래스에서는 단지 이 area() 메소드의 내용을 선언하지않고 =0으로 기입하므로서 공백으로 남겨둔다는 것이다.

따라서 CPolygon 클래스는 다음과 같이 선언될 수 있다:

// abstract class CPoligon
class CPolygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
  };

virtual int area() 부분에 함수의 내용을 구현하는 대신 =0을 덧붙이는 것에 주목하라. 이런 형태의 함수들은 순수 가상 함수라고 칭하며, 1개 이상의 순수가상함수를 포함하는 모든 클래스는 추상 클래스라고 칭한다.

추상 클래스의 두드러진 특징은 인스턴스(객체)를 만들 수 없다는 것이다. 그러나 포인터 변수로는 생성이 가능하다. 그러므로 다음과 같이 선언했을 경우에는:
CPolygon poly;
컴파일시 에러가 발생하게 된다. 반면에 다음과 같이 포인터로 선언한다면:
CPolygon * ppoly1;
CPolygon * ppoly2;
완벽하게 적법한 문장이 된다. 이것은 순수 가상함수는 그자체의 내용이 정의되지않았기 때문이며, 그러므로 객체로 생성될 수 없기 때문이다. 포인터로 선언하는 것은 실제로 객체를 생성하는 것은 아니므로 적법하게 취급된다.

여기에 완벽한 예제가 있다:

// virtual members
#include <iostream.h>

class CPolygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
  };

class CRectangle: public CPolygon {
  public:
    int area (void)
      { return (width * height); }
  };

class CTriangle: public CPolygon {
  public:
    int area (void)
      { return (width * height / 2); }
  };

int main () {
  CRectangle rect;
  CTriangle trgl;
  CPolygon * ppoly1 = &rect;
  CPolygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << ppoly1->area() << endl;
  cout << ppoly2->area() << endl;
  return 0;
}

만약 프로그램을 다시 훑어 본다면, 서로다른 (자식)클래스들이 단 하나의 포인터 타입인 CPolygon* 를 사용하여 참조된다는 것을 알아챌수 있을 것이다. 이것은 엄청나게 유용한 방법이다. 아래 예제를 보면, 상속된 클래스와는 독립적으로 area() 함수의 결과에 따라 화면에 출력을 수행하는 CPolygon의 메소드(아래 예에서는 printare()함수이다)를 생성할 수 있다는 것을 알 수 있다.

// virtual members
#include <iostream.h>

class CPolygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
    void printarea (void)
      { cout << this->area() << endl; }
  };

class CRectangle: public CPolygon {
  public:
    int area (void)
      { return (width * height); }
  };

class CTriangle: public CPolygon {
  public:
    int area (void)
      { return (width * height / 2); }
  };

int main () {
  CRectangle rect;
  CTriangle trgl;
  CPolygon * ppoly1 = &rect;
  CPolygon * ppoly2 = &trgl;
  ppoly1->set_values(4, 5);
  ppoly2->set_values(4, 5);
  ppoly1->printarea();
  ppoly2->printarea();
  return 0;
}

객체의 포인터가 가리키는 "실제 객체"의 area() 메소드가 실행되는 것을 명심하도록 하자.

추상 클래스와 가상 맴버 함수는 C++이 다형성을 가지도록 함으로써 객체지향적 프로그래밍을 만드는 유용한 도구로 활용됨을 볼 수 있다. 물론 이런 기능을 구현하는데 있어 더 가장 간단한 예를 본 것이며, 좀더 복잡한 예는 동적 메모리상에 적용되는 객체들이나 또는 객체의 배열에 적용될 경우를 들 수 있겠다.

5 마치며 #

virtual 키워드를 가장 흔하게 볼 수 있는 예제는 VC에서 자동으로 클래스를 생성했을 때의 소멸자의 선언에서이다. (소멸자 앞에 virtual이 선언되어있음을 볼 수 있다.) 일반적인 클래스에서는 상속받은 자식 클래스는 부모 클래스의 생성자를 실행한 후에 자신의 생성자를 실행한다. 마찬가지로 소멸자도 그렇게 수행되는데, 이러면 안 좋을 경우가 간혹 있다. (부모 소멸자에서 메모리를 해제한 후에 자식 소멸자에서 같은 주소를 또 해제하려할 경우를 상상해봐라.) 이런 경우, 부모 소멸자가 수행되는 것을 방지해야 할 경우는 부모 소멸자 선언 앞에 virtual을 붙여주는 것이다.

간략하게 C++ 상속의 백미인 virtual에 대해서 알아보았다. C++을 주무르는데 도움되기를 바란다.

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2010-10-28 12:42:52
Processing time 0.5052 sec