본문 바로가기

C/C++

가상 함수 / 순수 가상 함수/ 비가상 함수

이 글은 'Effective C++'의 항목34의 내용 입니다.

(항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자)

글쓰는 스타일도 베껴 왔습니다.


 

가상함수 / 순수 가상 함수 / 비가상 함수

 

(public) 상속이란 개념은 두가지로 나뉩니다.

함수 인터페이스 상속과 함수 구현의 상속입니다

 

클래스 설계할때다음의 경우가 있을 거에요

 - 멤버 함수의 인터페이스(선언)만 파생 클래스에서 상속받고 싶을때

 - 함수의 인터페이스 및 구현 모두 상속 받고그 상속받은 구현이 오버라이드 가능하게 하고 싶을때

 - 인터페이스와 구현을 상속받되 어떤 것도 오버라이드할 수 없도록 막고 싶을 때

 

예제를 보면서 몸으로 느껴는게 중요합니다.

그래픽 응용프로그램에 쓰이는 도형을 나타내는 클래스 구조를 생각해 봅시다.

 

Shape.h

class Shape {

public :

        virtual void draw () const = 0                   // 순수가상함수

        virtual void error (const std::stringmsg);     // (단순가상함수

        int objectID() const;                            // 비가상함수

}

 

      

Shape는 추상 클래스 입니다

순수 가상 함수인 draw가 바로 추상 클래스 딱지를 붙인 장본인이죠.

이 추상 클래스는 인스턴스를 만들려고 하면 안되고이 클래스의 파생 클래스만 인스턴스화가 가능합니다.

아무 힘도 없을 것 같은 이 Shape가 파생 클래스에 미치는 영향은 가히 절대군주와 맞먹습니다.

 

위의 세 함수에 대체 어떤 속뜻이 있을까요?

 

 

수 가상 함수인 draw부터 생각해 봅시다.

        virtual void draw () const = 0                        // 순수 가상 함수

 

순수 가상 함수의 가장 두드러진 특징은  두가지 입니다.

 - 순수 가상 함수를 물려받은 파생 클래스가 해당 순수 가상 함수를 다시 선언해야 합니다.

 - 순수 가상 함수는 전형적으로 추상 클래스 안에서 정의 하지 않습니다.

위 두가지를 하나로 모아보면 다음과 같은 결론이 나오죠

=> 순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스만을 물려주려는 것입니다.

 

Shape::draw 함수에 딱 맞죠?

"Shape를 상속받는 모든 객체는 그리기(draw)가 가능해야 한다"라는 요구사항은 그리 이상할게 없습니다.

게다가 그리기에 대해 생각도 없이 객체를 만들었을 때도 '그리기'가 되도록 해주는 draw 함수의 구현은

Shape 클래스 차원에서 어떻게 할수가 없으니 말입니다

작사각형과 타원을 그리는 알고리즘이 같을래야 같을 수 없자나요

 "draw 함수는 여러분이 직접 제공하도록 하시우하지만 당신이 어떻게 구현할지에 대해선 난 아무 생각 없소"

라고 말하는 것과 같습니다.

 

말 나온 김에사실 순수 가상 함수에도 정의를 제공할 수 있습니다.

다시 말해, Shape::draw 함수에 구현을 붙일 수 있다는 이야기에요.

C++도 이에 대해 툴툴거리지 않습니다만구현이 붙은 순수가상함수를 호출할려면 반드시

클래스 이름을 한정자로 붙여 주어야 한다는 점이에요

 

Shape.cpp

Shape *ps = new Shape;            //에러, Shape는추상클래스에요

 

Shape *ps1 = new Rectangle;       // 좋아요

ps1->draw();                      // Rectangle::draw 호출하겠지요

 

ps1->Shape::draw();               // Shape::draw를호출합니다.

 

 

이렇게 사용할 수 있다는 거에요.

이 부분은 이후에도 잊어버리지 않는게 인생에 도움이 될 겁니다 ㅋㅋ

뒤에서 확인하시겠지만이 부분은 단순(비순수가상 함수에 대한 기본 구현을 보다 안전하게 제공하는 매커니즘으로도 활용할 수 있습니다.

 

 

(그냥비순수-_-) 가상 함수입니다.

       virtual void error (const std::string& msg);     // (단순가상 함수

 

 단순 가상 함수는

 - 파생 클래스로 하여금 함수의 인터페이스를 상속하게 한다는 점은 똑같아요

 - 파생 클래스 쪽에서 오버라이드할 수 있는 함수 구현부도 제공한다는 점이 다른 거죠

 단순 가상 함수를 선언하는 목적은

=> 파생 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려받게 하자는 거죠 

 

에러가 발생했을때 함수를 제공하는 것은 모든 클래스가 해야 하는 일이지만,

각 클래스마다 그때그때 꼭 맞는 방법으로 에러를 처리할 필요가 없다는 겁니다.

그냥 Shape 클래스의 기본 제공 에러 처리 함수를 쓰라는 거죠

"error 함수는 여러분이 지원해야 한다구그러나 굳이 새로 만들 생각이 없다면 Shape의 기본 버전을 쓰라구"

라는 겁니다.

 

그런데 알고 보면

단순 가상 함수에서 함수 인터페이스와 기본 구현을 한꺼번에 지정하도록 내버려 두는 것은 위험할 수가 있어요.

예를 들어 볼게요.

XYZ이라는 항공사가 있고비행기는 A 모델, B모델 두가지가 있다고 쳐요

게다가 이 두 모델은 비행 방식이 같아요그니까 다음처럼 설계하겠죠.

 

Airplane.h

class Airplane {

public :

   virtual void fly (const Airportdestination);          //가상함수

};

 

void Airplane::fly (const Airportdestination) { 구현;}

 

class ModelApublic Airplane {...};                      // Airplane 상속

class ModelBpublic Airplane {...};                      // Airplane 상속

 

 

fly는 가상함수 입니다.

모든 비행기는 fly를 지원해야 하며다른 모델의 비행기는 원칙상 fly함수를 다르게 구현 할 수도 있다는 뜻이죠.

그런데 ModelA  ModelB 클래스는 코드 중복을 피해기본 비행을 Airplane::fly 본문으로 보냈습니다.

좋습니다이게 객체지향이니까요

 

but, 아직 좋아하긴 이릅니다.

XYZ 항공사 매출이 늘어서 C 모델을 도입했습니다 C모델은 비행 방식이 완전 다릅니다.

프로그래머들은 서둘러 C모델을 위한 클래스를 만들었습니다물론 Airplane을 상속 받아서요

서두르다보니 그만... fly 함수를 재정의하는 것을 잊어버렸네요

 

ModelC.h

class ModelC :: publc Airplane {

        ...                                   // fly가없어요

};

 

Airplane *ps = new ModelC             

pa->fly (서울);                             // Airplane::fly 함수가호출됩니다!!

 

비행방식이 완전히 다르다니까요날 수가 없어요...

 

지금 이 문제는, Airplane::fly 함수가 기본 동작을 구현해서 생긴 문제가 아니라,

ModelC 클래스는 이 기본 동작을 원한다고 명시적으로 밝히지 않았는데아무 걸림돌 없이 물려 받았다는 겁니다.

이 문제 역시 쉽게 구현 가능할 수 있습니다일종의 팁인 셈인데요,

가상 함수의 인터페이스와 그 가상 함수의 기본 구현을 잇는 연결 관계를 끊어 버리는 거에요

무슨 말이냐구요... 다음을 보시죠

 

Airplane.h

class Airplane {

public :

        virtual void fly (const Airportdestination0;          //순수가상함수

protected:

        void defaultFly (const Airportdestination) {구현;}

};

 

  

Airplane::fly 함수가 순수 가상 함수로 바뀌었어요 .

그리고 defaultFly라는 별도의 함수가 기본 비행을 구현했습니다.

기본 동작을 쓰고 싶은 클래스(ModelA, ModelB) fly 함수 구현시 defaultFly를 인라인 호출하면 되요

 

ModelA.h

class ModelA : publc Airplane {

public :

   virtual void fly (const Airportdestination)

   {  defaultFly (destinateion); };

};

 

 

 

 이렇게 하면 ModelC가 기본 구현을 우연히 물려받을 가능성은 없어졌습니다.

fly가 순수가상함수이니까 자신 만의 버전을 스스로 제공해야겠죠서둘러 구현을 못하면 에러를 내니까요

이제 좀 쓸만해진 설계가 됐습니다.

잠깐,

Airplane::defaultFly 함수가 protected 멤버인것도 보시죠

Airplane 및 파생 클래스만 내부적으로 사용하는 함수기 때문이죠

 

또 다른 중요사항은 Airplane::defaultFly 함수가 비가상 함수라는 점입니다

파생 클래스에서 재정의해선 안되기 때문이에요.

 

fly  defaultFly 같은 함수를 별도로 마련하는 아이디어를 좋아하지 않는 사람들이 있습니다.

중요하지도 않은 관계로 얽힌 비슷한 함수 이름들이 많아지면서 코드가 지저분해진대요.

그 분들은 순수 가상 함수가 파생 클래스에서 재선언되어야 하는걸 활용하되자체적으로 순수 가상 함수의 구현을 구비해 두는 거라고 합니다.

말보다 코드를 보죠

 

Airplane.h

 

class  Airplane {

public :

        virtual void fly (const Airportdestination0;

};

 

void Airplane::fly (const Airportdestination)       //순수가상함수의구현

{

        구현;

}

 

class ModelA : publc Airplane {

public :

        virtual void fly (const Airportdestination)

        {  Airplane::fly (destinateion); };

};

 

class ModelCpublc Airplane {

public :

        virtual void fly (const Airportdestination){ModelC fly 구현;}

};

 

 

Airplane::defaultFly의 자리에 순수 가상 함수인 Airplane::fly의 본문이 와 있습니다.

(위에서 말햇던거 기억나시죠안전하게 제공하는 매커니즘 어쩌고 저쩌고 한거)

 

파생 클래스가 사용해도 되나명시적으로 원할 경우에만 사용 가능 합니다.  원하는 거죠

그러나 함수 양쪽에 각기 다른 보호 수준을 부여할 수 있는 융통성은 날아갔습니다.

defaultFly 에 들어 있음으로써 protected 영역의 코드가 fly로 옮겨지면서 public 되었으니까요.

 

 

가상 함수입니다(그냥 함수죠;)

     int objectID() const;                                     // 비가상 함수

 

비가상 함수로 선언된 것은 파생 클래스에서 다른 행동이 일어나지 않는다는 겁니다.

공통된 변하지 않는 동작이죠

비가상 함수를 선언하는 목적은

파생 클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 상속받게 합니다.

"Shape 및 파생 객체의 objectID 함수 동작은 항상 똑같겠군파생 클래스는 이것을 바꿀수 없을걸"

 

 

론입니다.

순수 가상 함수단순 가상 함수비가상 함수 선언문이 가진 이런 차이점 덕택에,

파생 클래스가 물려받았으면 하는 것들을 정밀하게 지정할 수 있습니다.

 

판단에 따라 인터페이스만 상속인터페이스와 기본 구현을 함께 상속인터페이스와 필수 구현을 상속 시킬수 있어요

클래스 설계에 중요합니다.

 

가상 함수의 비용 때문에 모든 멤버 함수를 비가상 함수로 선언하거나,

파생 클래스에서 재정의가 안되야 하는 함수도 있음에도 불구하고모든 멤버 함수를 가상 함수로 선언하는 실수를 하지 마세요.

 

<출처 : Effective C++, 스콧 마이어스>