[C++] 다형성을 구현하기 위한 가상 함수( virtual function )

반응형

가상 함수( virtual function )

가상 함수( virtual function )는 기본( base ) 클래스에서 virtual 키워드를 사용하여 선언되고, 파생 클래스에서 재정의( overriding )되는 멤버 함수를 말합니다.

 

이러한 가상 함수가, 파생 클래스에서 재정의 되는, 일반 멤버 함수와 다른 점은, 파생 클래스가 기본( base ) 클래스 타입의 포인터나 참조를 통해 접근될 때, 가상 함수를 호출함으로써, 기본 클래스의 함수가 아닌 파생 클래스 버전의 함수를 사용할 수 있다는 점에 있습니다.

코드를 보는 것이 이해하는데 더 도움이 될 것입니다.

#include <iostream>
using namespace std;

class CBase{	// 기본 클래스

public:
    virtual void VFunc(){	// 가상 함수
        cout << "Base Virtual Func called\n";
    }

    void Func(){	// 일반 멤버 함수
        cout << "Base Func called\n";
    }
};

class CDerived : public CBase {	// 파생 클래스

public:
    virtual void VFunc(){   // 가상 함수 재정의
        cout << "CDerived Virtual Func called\n";
    }

    void Func(){    // 일반 멤버 함수 재정의
        cout << "CDerived Func called\n";
    }
};

int main(){

    CDerived cObj;

    CBase& rObj = cObj; // Base 클래스로 참조

    rObj.Func();	// Base 클래스의 함수 호출
    rObj.VFunc();	// CDerived 클래스의 함수 호출
}

▼출력

Base Func called
CDerived Virtual Func called

위 예제를 보면, 기본 클래스 CBase의 가상함수 VFunc와 일반 함수 Func를 파생 클래스 CDerived에서 재정의( overriding ) 하고 있습니다.
그리고 main 함수에서 파생 클래스 타입의 객체를 기본(base) 클래스 타입의 참조( reference ) 변수 rObj로 참조하고 있습니다.

이때, rObj.Func() 코드는 CBase 클래스의 Func 함수를 호출하지만, rObj.VFunc() 코드는 CDerived 클래스의 VFunc 함수를 호출하게 됩니다.

이것이 가상 함수의 기능이며, 이렇게 기본 클래스( CBase ) 타입의 참조를 통해 파생 클래스( CDerived )의 기능을 사용하는 것을 다형성(polymorphism)이라고 합니다.


이러한 가상 함수는 클래스 형식의 개체에 대해서만 호출되므로, 전역( global ) 또는 정적( static ) 함수를 가상 함수로 선언할 수 없습니다.
그리고, 기본 클래스에서 가상 함수로 선언한 함수는, 재정의( overriding ) 시에도 항상 가상 함수입니다.
따라서, 파생 클래스에서 반드시 virtual 키워드를 사용해야 하는 것은 아닙니다.

class CDerived : public CBase {

public:
	
    // 가상 함수 재정의, virtual 키워드를 사용하지 않아도 됩니다.
    void VFunc(){   
        cout << "CDerived Virtual Func called\n";
    }
};

 

 

가상 함수( virtual function )를 사용하는 이유

가상 함수를 사용하는 이유는, 위에서 말한, 다형성( polymorphism )을 실현하고자 하기 때문입니다.

 

이 다형성( polymorphism )은 객체 지향 프로그래밍의 특징 중 하나이고, "여러 형태"를 의미하는 그리스어 단어로, 가상 함수를 사용하여, 어떤 식으로 객체가 다양한 형태를 띨 수 있는지를 예문을 통해 알아보겠습니다.

 

아래의 CShape 클래스는 CRectangle, CCircle, CTriangle 클래스들의 기본 클래스입니다.

#include <iostream>
using namespace std;

class CShape{   // base 클래스

public:
    virtual float CalcArea(){   // 가상 함수
    	cout << "Shape Area: " << 0 << endl;
        return 0;
    }
};

class CRectangle : public CShape {
    float width, height;
public:
    CRectangle(float w, float h) : width(w), height(h) {}
    
    virtual float CalcArea(){   // 가상 함수
        float fArea = width * height;
        cout << "Rectangle Area: " << fArea << endl;
        return fArea;
    }
};

class CCircle : public CShape {
    float radius;
public:
    CCircle(float r) : radius(r){}
    
    virtual float CalcArea(){   // 가상 함수
        float fArea = radius*radius*3.14;
        cout << "Circle Area: " << fArea << endl;
        return fArea;
    }
};

class CTriangle : public CShape {
    float width, height;
public:
    CTriangle(float w, float h) : width(w), height(h) {}
    
    virtual float CalcArea(){   // 가상 함수
        float fArea = width * height * 1/2;
        cout << "Triangle Area: " << fArea << endl;
        return fArea;
    }
};

int main(){

    CShape* pObj;

    CRectangle cRect(20, 20);	// 다양한 도형 객체
    CCircle cCircle(10);
    CTriangle cTri(20, 20);

    pObj = &cRect;
    float fArea = pObj->CalcArea(); // 사각형 역할

    pObj = &cCircle;
    fArea = pObj->CalcArea();   // 원의 역할

    pObj = &cTri;
    fArea = pObj->CalcArea(); // 삼각형 역할
}

▼출력

Rectangle Area: 400
Circle Area: 314
Triangle Area: 200

위에 예제에서 CShape 타입의 포인터 pObj가 어떤 객체를 가리키는 가에 따라, 가상 함수를 통해 그 객체의 기능을 사용할 수 있음을 알 수 있습니다.

예를 들어, pObjCRectangle 타입의 객체를 가리키면, pObj->CalcAreaCRectangle 클래스의 CalcArea 가상 함수를 호출할 것입니다.

이것이 바로 다형성입니다.

만약, 가상 함수가 없다면 도형들의 면적을 구하는 함수 구현은 다음처럼 될 것입니다.

float CalcArea( CRectangle& cObj){
    return cObj.CalcArea();
}

float CalcArea( CCircle& cObj){
    return cObj.CalcArea();
}

float CalcArea( CTriangle& cObj){
    return cObj.CalcArea();
}

그리고, 도형이 늘어날수록, 이 함수의 리스트도 같이 늘어나게 될 것입니다.
그러나, 가상 함수를 사용하면 다음과 같이 구현할 수 있습니다.

float CalcArea( CShape& cObj){
    return cObj.CalcArea();
}

cObjCShape 파생 타입의 어떤 객체를 대입하더라도, 파생 클래스에서 CalcArea 가상 함수를 재정의 했다면, 그 객체의 형태로 기능을 수행해 원하는 목표를 달성하게 될 것입니다.

 

참고로, 위의 CRectangle 클래스에서 기본 클래스 CShape의 가상 함수를 호출하고 싶다면 어떻게 해야 할까요?

이 때는 범위 지정 연산자( scope resolution operator ) ::을 사용해 기본 클래스의 함수 호출을 명시함으로써, 가상 함수 호출 메커니즘을 억제할 수 있습니다.

int main(){

    CRectangle cRect(20, 20);
    
    cRect.CalcArea();   // CRectangle 함수 호출
    cRect.CShape::CalcArea();   // 명시적 CShape 함수 호출
    
    CShape* pObj = &cRect;
    pObj->CalcArea();   // CRectangle의 가상 함수 호출
    pObj->CShape::CalcArea();   // 명시적 CShape 함수 호출
}

▼출력

Rectangle Area: 400
Shape Area: 0
Rectangle Area: 400
Shape Area: 0

 

 

정리

  • 가상 함수를 통해서 기본(base) 클래스 타입의 포인터나 참조( reference )로 파생 클래스의 함수를 호출할 수 있습니다.
  • 가상 함수를 쓰는 이유는 다형성( polymorphism )을 구현하기 위해서입니다.

 

 

이 글과 관련 있는 글들

가상 함수를 호출하기 위한 동적 바인딩( dynamic binding )

추상 클래스( abstract class )와 순수 가상 소멸자

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유