C, C++/C++ 언어 / / 2024. 5. 23.

[C++] 가상 함수( Virtual Function )를 사용하는 이유

가상 함수란

가상 함수(Virtual Function)는 기본(base) 클래스에서 선언되고, 파생 클래스에서 재정의(overriding) 되는 멤버 함수를 말합니다.

가상 함수를 일반 함수와 구분하기 위해 virtual 키워드를 사용합니다.

 

파생 클래스에서 재정의 되는 일반 멤버 함수와 다른 점은,

파생 클래스가 기본(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 클래스의 함수 호출

    return 0;
}

▼결과

Base Func called
CDerived Virtual Func called

 

위 예제를 보면, 기본 클래스 CBase의 가상함수 VFunc와 일반 함수 Func를 파생 클래스 CDerived에서 재정의 하고 있습니다.

그리고 main 함수에서 파생 클래스를 기본(base) 클래스 타입인 rObj으로 참조하고 있습니다.

 

이때, 일반 멤버 함수를 호출하면 CBase클래스의 Func 함수를 호출하지만, 가상 함수를 호출하면 CDerived 클래스의 VFunc 함수를 호출하게 됩니다.

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

 

이러한 가상 함수는 클래스 형식의 개체에 대해서만 호출되므로, 전역 또는 정적 함수를 가상 함수로 선언할 수 없습니다.

그리고, 기본 클래스에서 가상 함수로 선언한 함수는, 재정의 시에도 항상 가상 함수입니다.

따라서, 파생 클래스에서 반드시 virtual 키워드를 사용해야 하는 것은 아닙니다.

class CDerived : public CBase {

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

 

 

가상 함수를 사용하는 이유

가상 함수를 사용하는 이유는 위에서 말한 다형성(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(); // 삼각형 역할

    return 0;
}

▼결과

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

 

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

이렇게 하나의 변수로 다양한 객체의 기능을 사용하는 것을 다형성이라 합니다.

 

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

 

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();
}

cObj에 어떤 도형을 대입하더라도, 그 객체의 형태로 기능을 수행해 원하는 목표를 달성하게 될 것입니다.

 

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

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

int main(){

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

▼결과

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

 

정리

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

 

이 글과 관련된 글들

동적 바인딩과 가상 함수 테이블 ( V-Table )

추상 클래스와 순수 가상 소멸자

 

 

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