가상 함수

가상 키워드는 함수 지정자로, 기본 및 파생 클래스의 함수 중 적절한 함수 멤버를 런타임에 동적으로 선택할 수 있는 메커니즘을 제공합니다. 구조에는 가상 함수를 사용할 수 없습니다. 함수 멤버에 대해서만 선언을 변경할 수 있습니다.

가상 함수는 일반 함수와 마찬가지로 실행가능한 바디를 가져야 합니다. 호출 시 시맨틱은 다른 함수와 동일합니다.

파생 클래스에서 가상 함수를 재정의할 수 있습니다. 가상 함수에 대해 호출해야 하는 함수정의를 동적으로 선택합니다(실시간). 기준 클래스에 가상 함수가 포함되어 있고 파생 클래스에 이 함수의 자체 버전이 있는 경우가 대표적입니다.

기준 클래스에 대한 포인터는 기준 클래스 개체 또는 파생 클래스의 개체를 나타낼 수 있습니다. 호출할 멤버 함수의 선택은 런타임에 수행되며 포인터 유형이 아닌 개체 유형에 따라 달라집니다. 파생 형식의 멤버가 없는 경우 기본적으로 기준 클래스의 가상 함수가 사용됩니다.

소멸자가상 키워드로 선언되었는지 여부에 관계없이 항상 가상입니다.

MT5_Tetris.mq5의 예에서는 가상 함수의 사용을 살펴보겠습니다. 포함된 파일 MT5_TetrisShape.mqh에 그리기 기능을 가진 기본 클래스 CTetrisShape가 정의되어 있습니다.

//+------------------------------------------------------------------+
class CTetrisShape
  {
protected:
   int               m_type;
   int               m_xpos;
   int               m_ypos;
   int               m_xsize;
   int               m_ysize;
   int               m_prev_turn;
   int               m_turn;
   int               m_right_border;
public:
   void              CTetrisShape();
   void              SetRightBorder(int border) { m_right_border=border; }
   void              SetYPos(int ypos)          { m_ypos=ypos;           }
   void              SetXPos(int xpos)          { m_xpos=xpos;           }
   int               GetYPos()                  { return(m_ypos);        }
   int               GetXPos()                  { return(m_xpos);        }
   int               GetYSize()                 { return(m_ysize);       }
   int               GetXSize()                 { return(m_xsize);       }
   int               GetType()                  { return(m_type);        }
   void              Left()                     { m_xpos-=SHAPE_SIZE;    }
   void              Right()                    { m_xpos+=SHAPE_SIZE;    }
   void              Rotate()                   { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
   virtual void      Draw()                     { return;                }
   virtual bool      CheckDown(int& pad_array[]);
   virtual bool      CheckLeft(int& side_row[]);
   virtual bool      CheckRight(int& side_row[]);
  };

또한 각 파생 클래스에 대해 이 기능은 하위 세분류의 특성에 따라 구현됩니다. 예를 들어 첫 번째 형상 CTetrisShape1에는 Draw() 함수가 구현되어 있습니다.

class CTetrisShape1 : public CTetrisShape
  {
public:
   //--- 쉐이프 그리기
   virtual void      Draw()
     {
      int    i;
      string name;
      //---
      if(m_turn==0 || m_turn==2)
        {
         //--- 수평적
         for(i=0; i<4; i++)
           {
            name=SHAPE_NAME+(string)i;
            ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
            ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
          }
        }
      else
        {
         //--- 수직적
         for(i=0; i<4; i++)
           {
            name=SHAPE_NAME+(string)i;
            ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos);
            ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+i*SHAPE_SIZE);
          }
        }
    }
  }

사각형 쉐이프는 클래스 CTetrisShape6에 의해 설명되며 자체적인 Draw() 메서드가 있습니다:

class CTetrisShape6 : public CTetrisShape
  {
public:
   //--- 쉐이프 그리기
   virtual void      Draw()
     {
      int    i;
      string name;
      //---
      for(i=0; i<2; i++)
        {
         name=SHAPE_NAME+(string)i;
         ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+i*SHAPE_SIZE);
         ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos);
        }
      for(i=2; i<4; i++)
        {
         name=SHAPE_NAME+(string)i;
         ObjectSetInteger(0,name,OBJPROP_XDISTANCE,m_xpos+(i-2)*SHAPE_SIZE);
         ObjectSetInteger(0,name,OBJPROP_YDISTANCE,m_ypos+SHAPE_SIZE);
        }
    }
  };

생성된 개체가 속한 클래스에 따라 이 클래스 또는 저 파생 클래스의 가상 함수를 호출합니다.

void CTetrisField::NewShape()
  {
//--- 7개의 가능한 도형 중 하나를 임의로 만듭니다
   int nshape=rand()%7;
   switch(nshape)
     {
      case 0: m_shape=new CTetrisShape1; break;
      case 1: m_shape=new CTetrisShape2; break;
      case 2: m_shape=new CTetrisShape3; break;
      case 3: m_shape=new CTetrisShape4; break;
      case 4: m_shape=new CTetrisShape5; break;
      case 5: m_shape=new CTetrisShape6; break;
      case 6: m_shape=new CTetrisShape7; break;
    }
//--- 그리기
   m_shape.Draw();
//---
  }

한정자 '오버라이드' #

'override' 한정자는 선언된 함수가 부모 클래스의 메서드를 재정의해야 함을 의미합니다. 이 방법을 사용하면 오류 재정의가 발생하지 않도록 할 수 있습니다. 예를 들어 메서드 서명이 실수로 수정되는 것을 방지할 수 있습니다. 기본 클래스에 'func' 메서드가 정의되어 있다고 가정합니다. 메서드에서는 int 변수를 인수로 받아들입니다:

class CFoo
  {
   void virtual func(int x) const { }
  };

그런 다음 자식 클래스에서 메서드를 재정의합니다:

class CBar : public CFoo
  {
   void func(short x) { }
  };

그러나 인수 유형이 int에서 short로 잘못 변경되었습니다. 사실 이것은 메서드를 재정의하는 것이 아니라 메서드 오버로드입니다. 오버로드된 함수 알고리즘 정의에 따라 작동하는 컴파일러는 특정 상황에서 재정의된 메서드 대신 기본 클래스에 정의된 메서드를 선택할 수 있습니다.

이러한 오류를 방지하려면 재정의할 메서드에 'override' 한정자를 명시적으로 추가해야 합니다.

class CBar : public CFoo
  {
   void func(short x) override { }
  };

재정의하는 동안 메서드 서명이 변경되면 컴파일러는 상위 클래스에서 동일한 서명을 가진 메서드를 찾을 수 없으며 컴파일 오류를 반환합니다:

'CBar::func' 메서드가 'override' 지정자로 선언되었지만 기준 클래스 메서드는 재정의하지 않습니다

한정자 'final' #

'final' 한정자는 이와 반대입니다. 자식 클래스에서 메서드를 재정의할 수 없습니다. 메서드 구현이 충분하고 완전히 완료된 경우 나중에 수정되지 않도록 'final' 한정자를 사용하여 이 메서드를 선언하십시오.

class CFoo
  {
   void virtual func(int x) final { }
  };
 
class CBar : public CFoo
  {
   void func(int) { }
  };
 

위의 예와 같이 메서드를 'final' 한정자로 재정의하려고 하면 컴파일러에서 오류를 반환합니다:

'CFoo::func' 메서드를 'CBar::func'로 재정의할 수 없습니다. 
'CFoo::func' 선언을 참조하십시오.

더 보기

표준 라이브러리