..

Search

60) 가상 함수

60) 가상 함수

가상 함수


가상 함수(virtual function)

C++에서 가상 함수(virtual function)는 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미합니다.

이러한 가상 함수는 자신을 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정됩니다.

 

C++에서 가상 함수는 virtual 키워드를 사용하여 선언합니다.

문법

virtual 멤버함수의원형;

 

기초 클래스에서 virtual 키워드를 사용해 가상 함수를 선언하면, 파생 클래스에서 재정의된 멤버 함수도 자동으로 가상 함수가 됩니다.

 

파생 클래스의 멤버 함수 쪽에도 virtual 키워드를 사용하여 가상 함수라는 것을 명확히 하는 것도 나쁘지 않습니다.

동적 바인딩(dynamic binding)

C++ 컴파일러는 함수를 호출할 때, 어느 블록에 있는 함수를 호출해야 하고, 해당 함수가 저장된 정확한 메모리 위치까지도 알아야 합니다.

이처럼 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행하라는 의미로 해석하는 것을 바인딩(binding)이라고 합니다.

하지만 C++에서는 함수가 오버로딩될 수 있으므로 이 작업이 조금 복잡해집니다.

 

대부분 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환됩니다.

이것을 정적 바인딩(static binding) 또는 초기 바인딩(early binding)이라고 합니다.

C++에서는 가상 함수가 아닌 멤버 함수는 모두 이러한 정적 바인딩을 하게 됩니다.

 

하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없습니다.

왜냐하면, 가상 함수는 프로그램이 실행될 때 객체를 결정하므로, 컴파일 타임에 해당 객체를 특정할 수 없기 때문입니다.

따라서 가상 함수의 경우에는 런 타임에 올바른 함수가 실행될 수 있도록 해야 합니다.

이것을 동적 바인딩(dynamic binding) 또는 지연 바인딩(late binding)이라고 합니다.

 

하지만 가상 함수도 결합하는 타입이 분명할 때에는 일반 함수와 같이 정적 바인딩을 합니다.

이러한 가상 함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩을 하게 됩니다.

 

다음 예제는 가상 함수를 사용한 동적 바인딩을 간략하게 표현한 예제입니다.

예제

class A

{

public:

    virtual void Print() { cout << "A 클래스의 Print() 함수" << endl; }

};

 

class B : public A

{

    virtual void Print() { cout << "B 클래스의 Print() 함수" << endl; }

};

 

int main(void)

{

    A* ptr;

    A obj_a;

    B obj_b;

 

    ptr = &obj_a;

    ptr->Print();

    ptr = &obj_b;

    ptr->Print();

    return 0;

}

코딩연습 ▶

실행 결과

A 클래스의 Print() 함수

B 클래스의 Print() 함수


가상 함수 테이블(virtual function table, vtbl)

C++에서는 가상 함수의 정의와 동작 방식만을 규정하고 있으며, 그에 따른 구현은 컴파일러마다 다릅니다.

하지만 컴파일러가 가상 함수를 다루는 가장 일반적인 방식은 가상 함수 테이블(virtual function table)을 이용하는 것입니다.

 

C++ 컴파일러는 각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 숨겨진 멤버를 하나씩 추가합니다.

이와 함께 가상 함수를 단 하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성합니다.

이렇게 작성된 가상 함수 테이블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되게 됩니다.

 

가상 함수를 호출하면, C++ 프로그램은 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출하게 됩니다.

가상 함수를 사용하면 이처럼 함수의 호출 과정이 복잡해지므로, 메모리와 실행 속도 측면에서 약간의 부담을 가지게 됩니다.

따라서 C++에서 기본 바인딩은 정적 바인딩이며, 필요한 경우에만 가상 함수로 선언하도록 하고 있습니다.

 

하지만 파생 클래스가 재정의할 가능성이 있는 함수는 모두 가상 함수로 선언하는 편이 좋습니다.

가상 소멸자

C++에서 기초 클래스의 소멸자는 반드시 가상으로 선언해야 합니다.

 

우선 앞서 살펴본 Person 클래스와 파생 클래스인 Student 클래스가 있다고 가정합니다.

예제

Person* hong = new Student;

...

delete hong;

 

위의 예제에서 Person 클래스는 Student 클래스의 기초 클래스이므로, hong이라는 Student 객체가 동적으로 할당됩니다.

하지만 마지막 구문의 delete 키워드는 ~Student() 소멸자를 호출하지 않고, ~Person() 소멸자를 호출할 것입니다.

그러므로 Student 객체에 동적으로 할당된 메모리는 정상적으로 해제되지 않을 것입니다.

 

하지만 Person 클래스의 소멸자를 가상으로 선언한다면, 위의 구문은 정상적으로 ~Student() 소멸자를 호출할 것입니다.

따라서 기초 클래스는 명시적으로 소멸자를 선언할 필요가 없더라도, 아무 일도 하지 않는 가상 소멸자를 선언해야 합니다.


연습문제