[C++] 가상함수(virtual) 사용법 총정리

    가상함수란?

    가상함수는 부모 클래스에서 상속받을 클래스에서 재정의할 것으로 기대하고 정의해놓은 함수입니다. virtual이라는 예약어를 함수 앞에 붙여서 생성할 수 있으며 이렇게 생성된 가상함수는 파생 클래스에서 재정의하면 이전에 정의되었던 내용들은 모두 새롭게 정의된 내용들로 교체됩니다.

     

    가상함수를 사용해야 하는 이유

    컴파일러는 함수를 호출할 때 매우 복잡한 과정을 거치게 됩니다. 그렇기에 컴파일러는 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환시킵니다. 이것을 정적바인딩이라고 합니다. 일반 함수의 경우 모두 이러한 정적바인딩을 하게 됩니다. 하지만 일반 함수를 오버 로딩하게 되면 정적바인딩으로 인해 문제가 될 수 있습니다. 가상함수가 아닌 보통의 경우 부모 클래스형 포인터로 멤버 함수를 호출할때, 컴파일러는 정적타입을 보고 이 타입에 맞는 멤버함수를 호출하기 때문입니다.

    #include <iostream>
    using namespace std;
    
    class Parent{
    public:
        void print() {
            cout << "이곳은 Parent입니다." << endl;
        }
    };
    
    class Child : public Parent{
    public:
        void print() {
            cout << "이곳은 Child입니다." << endl;
        }
    };
    
    void main() {
        Parent* p = new Parent;
        Child* c = new Child;
        
        p->print();
        p = c;
        p->print();
    }
    

    가상함수 예제1

    위의 예제를 보시면 Parent타입으로 선언된 포인터 p에 Child객체의 주소를 넣고 함수를 호출시켰는데 Parent클래스의 함수가 호출되었습니다. p포인터의 주소를 child로 바꾸어주었음에도 불구하고 정적바인딩으로 인해(컴파일 당시 호출될 함수의 번지가 이미 결정나버렸기 때문에) 부모의 함수가 호출되는 것입니다. 이를 해결하려면 정적바인딩이 아닌 동적바인딩을 해야 합니다. 동적바인딩을 하려면 일반 함수들을 가상함수로 바꾸어주시면 됩니다. 가상함수로 선언하면 포인터의 타입이 아닌 포인터가 가리키는 객체의 타입에 따라 멤버 함수를 선택하게 됩니다.

     

     가상함수 virtual 사용법 

    virtual 반환형식 메서드명 //문법
    virtual void PrintName(); //ex
    
    #include <iostream>
    using namespace std;
    
    class Parent{
    public:
        virtual void print() { //virtual함수 선언
            cout << "이곳은 Parent입니다." << endl;
        }
    };
    
    class Child : public Parent{
    public:
        virtual void print() {
            cout << "이곳은 Child입니다." << endl;
        }
    };
    
    void main() {
        Parent* p = new Parent;
        Child* c = new Child;
        
        p->print();
        p = c;
        p->print();
    }
    

    가상함수 예제2

    부모의 print() 함수를 가상함수로 선언하여 동적바인딩을 하여 아까와 같은 문제를 해결할 수 있습니다. 참고로 한번 가상함수로 선언된 함수는 따로 virtual 키워드를 앞에 써주지 않아도 가상함수로 인식합니다. 

     

    가상함수 테이블

    가상함수 테이블을 영어로 한다면 Virtual Function Table입니다. 여기서 테이블은 배열을 뜻합니다. 즉 가상함수 테이블은 함수 포인터 배열이라고 생각하시면 됩니다. 

     

    함수 포인터를 모르신다면 아래 글을 참고해주세요.

    [C언어/C++] 함수 포인터 사용법 & 예제 총정리

    #include <iostream>
    using namespace std;
    
    class Parent{
    public:
        virtual void func1() {
            cout << "이곳은 Parent의 func1입니다." << endl;
        }
        virtual void func2() {
            cout << "이곳은 Parent의 func2입니다." << endl;
        }
        void func3() { //가상함수 X
            cout << "이곳은 Parent의 func3입니다." << endl;
        }
    };
    
    class Child : public Parent{
    public:
        virtual void func1() {
            cout << "이곳은 Child의 func1입니다." << endl;
        }
    };
    
    void main() {
        Parent* p = new Parent;
        Child* c = new Child;
        
        p->func1();
        c->func1();
        p->func2();
        c->func2();
        p->func3();
        c->func3();
    }
    

    가상함수 예제3

     

    위 예제의 가상함수 테이블은 어떤 식으로 구성되어 있을까요?

     

    가상함수 테이블 구조

    위의 예제에서 조사식으로 p포인터와 c포인터를 찍어보면 위와 같이 __vfptr이라는 테이블이 나오게 됩니다. 이 테이블이 바로 가상함수 테이블입니다. Parent 클래스에서 2개의 가상함수가 선언되었으니 2개의 가상함수가 잘 들어가 있는 것을 보실 수 있고 [0]번 index의 경우 Child클래스에서 재정의를 했기에 주소 값이 0x00461500 -> 0x00461505로 바뀐 것을 보실 수 있습니다. [1]번 index는 재정의를 안 했기에 그대로죠. 

     

    위와 같이 가상함수를 가지는 클래스의 경우 가상 함수 주소들이 배열형태로 존재하는 가상함수 테이블을 가지고 있고  클래스 안에서 이 테이블을 지시할수있는 포인터를 가지고 있습니다. 동작시 호출할 함수의 목록을 가상함수 테이블에 미리 작성해놓고 실행 중에 객체와 그 객체의 함수 주소를 찾는 방법으로 동작하게 됩니다.

    댓글

    Designed by JB FACTORY