2 분 소요

가상 함수의 개념

다음 예제는 가상 함수의 필요성을 설명하기 위한 잘못된 예제이다.
이 예자가 어떤 문제점을 가지고 있는지 분석해 보고 해결 방법을 생각해 보자.

예제1

class Base
{
public:
    void OutMessage() { printf("Base Class\n"); }
};

class Derived : public Base
{
public:
    void OutMessage() { printf("Derived Class\n"); }
};

 

void main()
{
    Base B,*pB;
    Derived D;

    pB=&B;
    pB->OutMessage();

    pB=&D;
    pB->OutMessage();

}

Base 클래스에 OutMessage라는 멤버 함수가 작성되어 있으며 이 함수는 자신의 소속을 화면으로 출력하기만 한다. Base로부터 파생된 Derived는 OutMessage 멤버 함수를 재정의하여 원래의 함수와 다른 문자열을 출력하도록 했다. main에서는 Base형의 B와 Derived형의 D를 선언하고 Base형의 포인터 pB로 이 두 객체의 번지를 차례대로 대입받은 후 포인터로 OutMessage를 호출했다.

pB가 B를 가리키는 상황에서 pB->OutMessage는 Base의 OutMessage를 호출할 것이다. 그렇다면 pB가 D를 가리킬 때는 Derived의 OutMessage를 호출할 것처럼 보인다. 이 예제를 만든 사람의 의도는 바로 이런 동작이었다. 그러나 실행해 보면 예상과는 다른 결과가 나온다.

결과:
Base Class
Base Class

앞 항에서 알아 봤다시피 부모 클래스 타입의 포인터 pB가 자식 객체 D를 가리키는 것은 문법적으로 합당하다. 그런데 pB가 D를 가리키는 상황에서 멤버 함수 호출은 왜 Base의 것이 호출되는가? 그 이유는 컴파일러가 포인터의 정적 타입을 보고 이 타입에 맞는 멤버 함수를 호출하기 때문이다. pB가 Base * 타입으로 선언되어 있이므로 Base의 멤버 함수를 호출하는 것이다.

이것은 원하는 결과가 아니다. 이 예제가 의도하는 바는 pB가 선언된 포인터 타입(정적 타입)에 따라 멤버 함수를 선택하는 것이 아니라 pB가 가리키고 있는 객체의 타입(동적 타입)에 따라 멤버 함수가 선택되도록 하는 것이다. pB가 Base *로 선언되었지만 Derived의 객체를 가리키고 있을 때는 Derived의 멤버 함수가 호출되도록 하고 싶다. 이렇게 하고 싶다면 원하는 함수의 선언문에 virtual 키워드를 붙여 이 함수를 가상 함수로 선언한다. 예제를 다음과 같이 수정해 보자.

예제2

class Base
{
public:
    virtual void OutMessage() { printf("Base Class\n"); }
};

class Derived : public Base
{
public:
    virtual void OutMessage() { printf("Derived Class\n"); }
};

부모의 멤버 함수가 가상 함수이면 자식의 멤버 함수도 자동으로 가상 함수가 되므로 Derived의 OutMessage에는 굳이 virtual 키워드를 쓰지 않아도 되지만 이 함수가 가상 함수라는 것을 분명히 표시하기 위해 양쪽에 모두 붙이는 것이 더 좋다. virtual 키워드는 클래스 선언문 내에서만 쓸 수 있으며 함수 정의부에서는 쓸 수 없다. 정의부에 virtual을 쓰면 에러 처리되므로 함수를 외부 정의할 때는 virtual 키워드없이 함수의 본체만 기술해야 한다.

이렇게 가상 함수로 선언하면 포인터의 타입이 아닌 포인터가 가리키는 객체의 타입에 따라 멤버 함수를 선택하므로 원하는 결과가 나온다. 즉, 가상 함수란 포인터의 정적 타입이 아닌 동적 타입을 따르는 함수이다. 수정 후의 출력 결과는 다음과 같다.

Base Class
Derived Class

예제3

class Base
{
public:
    virtual void OutMessage() { printf("Base Class\n"); }
};


class Derived : public Base
{
public:
    virtual void OutMessage() { printf("Derived Class\n"); }
};

void Message(Base *pB)
{
    pB->OutMessage();
}

void main()
{
    Base B;
    Derived D;

    Message(&B);
    Message(&D);
}

Message 함수는 Base *형의 포인터 pB를 받아들여 이 포인터가 가리키는 객체의 OutMessage 함수를 호출한다. main에서 &B에 대해 그리고 &D에 대해 Message 함수를 두 번 호출했는데 전달되는 객체 타입에 따라 실제 호출될 OutMessage 함수가 달라진다. 만약 OutMessage가 가상 함수가 아니라면 Message 함수는 Base의 멤버 함수만 호출하므로 결과는 항상 “Base Class”가 될 것이다.

OutMessage 함수가 가상으로 선언되어 있으므로 형식 인수 pB가 전달받는 객체의 타입에 따라 호출될 함수가 결정된다. Message 함수의 본체 코드는 완전히 똑같은데 전달되는 객체에 따라 실제 동작은 달라진다. pB->OutMessage() 라는 코드가 경우에 따라 다른 동작을 할 수 있는 능력, 이것이 바로 다형성의 개념이다.

부모 클래스형의 포인터로부터 멤버 함수를 호출할 때 비가상 함수는 포인터가 어떤 객체를 가리키는가에 상관없이 항상 포인터 타입 클래스의 멤버 함수를 호출한다. 반면 가상 함수는 포인터가 가리키는 실제 객체의 함수를 호출한다는 점이 다르다. 그래서 파생클래스에서 재정의하는 멤버 함수, 또는 앞으로라도 재정의할 가능성이 있는 멤버 함수는 가상으로 선언하는 것이 좋다. 그래야 부모 클래스의 포인터 타입으로 자식 객체의 멤버 함수를 호출해도 정확하게 호출된다.