C++ 가상함수(Virtual Function)


서론

이 장은 가상함수에 대해 설명한다.

목차

  1. 가상함수란
  2. 바인딩이란?
    1. 정적 바인딩(Static binding)
    2. 동적 바인딩(Dynamic binding)
  3. 가상함수를 사용하는 이유
  4. 가상함수 테이블

가상함수란

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

아래는 가상함수를 설명하기 위해 바인딩이라는 개념을 먼저 설명한다.

바인딩이란?

사전적 의미로는 컴퓨터 프로그래밍에서 각종 값들이 확정되어 더 이상 변경할 수 없는 구속(bind)상태가 되는 것으로 프로그래머가 코딩을 해서 컴파일을 하게 되면 가각의 코드가 메모리 어딘가에 저장되고, 함수를 호출하는 부분에는 그 함수가 저장된 메모리의 주소값이 저장되며 프로그래머가 값을 변경할 수 없는 상태

정적 바인딩(Static binding)

실행 이전에 값이 확정되면 정적 바인딩이라 한다.

컴파일 타임에 호출될 함수가 결정되는 것으로 기본적으로 정적 바인딩이 된다.

동적 바인딩(Dynamic binding)

실행 이후에 값이 확정되면 동적 바인딩이라 한다.

런타임에 호출될 함수가 결정되는 것으로, virtual 키워드를 통해 동적 바인딩하는 함수를 가상 함수라고 한다.

가상함수를 사용하는 이유

컴파일러는 컴파일 타임에 함수를 호출하는 코드를 고정된 메모리 주소로 변환시킵니다. 이것을 정적바인딩이라 하는데 일반 함수의 경우 모두 정적바인딩을 하게 됩니다. 하지만 일반 함수를 오버 로딩하게 되면 정적 바인딩으로 인해 문제가 될 수 있다. 예시를 봐보자.

부모 클래스 타입의 포인터인 p를 자식 클래스 타입의 포인터인 c 를 할당한 이후에 print라는 함수를 호출했을 때 부모 클래스의 print를 호출하는 것을 볼 수 있다. 정적바인딩으로 인해(컴파일 당시 호출될 함수의 번지가 이미 결정나버렸기 때문에) 부모의 함수가 호출되는 것이다.

이를 해결하려면 정적바인딩이 아닌 동적바인딩을 해야 하는데 동적바인딩을 하려면 일반 함수들을 가상함수로 바꾸어주면 된다.

가상함수로 선언하면 포인터의 타입이 아닌 포인터가 가리키는 객체의 타입에 따라 멤버 함수를 선택하게 된다.

따라서 다음과 같이 변경하면된다.

그럼 출력결과가 아래와 같이 달라지는 것을 볼 수 있다.

그럼 실제로 virtual이라는 키워드를 가진 함수가 어떻게 주소가 변하는지 봐보자

가상함수 테이블

가상함수 테이블은 가상함수에 대한 함수 포인터 배열이다.

실행결과

위 가상함수 테이블은 다음과 같이 구성되어있다. _vfptr는 가상함수 테이블을 의미한다.

Parent 클래스에서 2개의 가상함수가 선언되었으니 2개의 가상함수가 들어가 있다. [0]번 index의 경우 Child클래스에서 재정의를 했기 때문에 주소 값이 0x00461500 -> 0x00461505로 바뀌었다. 반면 [1]번 index는 재정의를 안 했기에 Child클래스의 함수는 여전히 부모 클래스의 함수의 주소와 같음을 볼 수 있다.

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

[referece]

https://coding-factory.tistory.com/699