2015. 2. 6.

[C++]Virtual

 상속과 함수 오버라이딩에 이어서 드디어 virtual에 대해서 설명할 차례가 왔다. virtual을 이해하고 있다는 것은 C++의 상속, 함수 오버라이딩, 생성자, 소멸자, 객체 포인터의 참조 관계 등에 대해 종합적으로 이해하고 있음을 의미한다. 그만큼 중요하고 핵심적인 개념이다. 주의깊게 살펴보도록 하자.

객체 포인터의 참조 관계
 코드를 보자. Third 클래스의 객체가 하나가 생성되었다. First, Second, Third 클래스의 포인터로 이 객체를 참조하고 있다. 저번에도 설명했듯이 이런 경우 포인터의 자료형을 기준으로 함수 호출 여부를 판단한다고 그랬다. 그런데 각 클래스의 멤버함수는 함수 오버라이딩이 되어있다. 함수 오버라이딩을 한 이유를 떠올려보자. 객체에 맞게 오버라이딩한 멤버함수를 호출하고자 함수를 오버라이딩하지 않았던가. '객체에 맞는' 멤버함수를 호출하고자 하였지만 객체 포인터의 참조 관계에 따라서 '객체에 맞지 않는' 멤버함수가 호출되었다. 객체가 아닌 포인터의 자료형을 기준으로 멤버함수가 호출되었다. 이런 요구가 생길만 하다. 객체 포인터의 자료형이 아니라 실제 참조하는 객체를 기준으로 멤버함수를 호출할 수는 없을까?


virtual

 위의 요구를 충족시켜주는 키워드가 바로 virtual이다. 객체 포인터의 참조 관계는 클래스의 자료형을 따르지만 오버라이딩된 멤버함수를 virtual로 선언하게 되면 그 멤버함수는 실제 객체의 자료형에 따라 해당하는 멤버함수를 호출하게 된다.

 virtual을 적용하기 전에 virtual를 설명하기 위해 정적 바인딩(static binding), 동적 바인딩(dynamic binding)을 먼저 살펴보자. 바인딩이란 프로그램 구성 요소의 성격을 결정한다는 의미이다. 예를 들면 변수의 자료형이 무엇인지를 정하는 것도 바인딩에 속한다. 지금은 변수의 자료형을 정하는 의미정도 바인딩을 생각하자. 그럼 정적, 동적 바인딩은 무엇인가?
  • 정적 바인딩: 컴파일 단계에서 변수의 자료형을 정한다.
  • 동적 바인딩: 실행 단계에서 변수의 자료형을 정한다.
 fptr->MyFunc() 코드가 컴파일 단계에서는 포인터의 자료형에 따른다고 했다. 그러면 실행 단계에는 어떻게 될까? virtual이 선언되지 않으면 그대로 포인터의 자료형에 따른다. 하지만 virtual을 선언한 멤버함수는 실행 단계에서 포인터의 자료형을 따르지 않고, 포인터가 가리키는 실제 객체의 자료형에 따라 호출된다. 이처럼 실행 단계에서 변수의 자료형을 정하는 것이 바로 동적 바인딩이다. 결국 virtual 키워드로 멤버함수를 선언하면 해당 멤버함수는 동적 바인딩되어 실행된다. 객체 포인터의 참조 관계를 따르지 않고 객체에 따라 멤버함수가 호출된다.

 위의 경우는 MyFunc() 함수를 virtual 선언하면 이 멤버함수 호출은 이제 객체의 실제 자료형에 따르게 된다. 포인터의 자료형이 First인 fptr이 MyFunc()을 호출하면 First의 MyFunc()을 호출하는 것이 아니라 fptr이 참조하는 Third 클래스의 MyFunc()을 호출한다. 
 이제 MyFunc() 함수에 virtual을 추가하였다. 이 멤버함수는 동적 바인딩되어 호출된다. 위 코드의 실행 결과는 이제 쉽게 예상할 수 있을 것이다. virtual 선언으로 동적 바인딩되어 멤버함수가 호출된다. 위 코드에서 생성되는 객체는 모두 Third 클래스의 객체이다. 그러므로 Third 클래스의 MyFunc()만 호출된다. 참고로 virtual 선언은 부모 클래스에 멤버함수에 하게 되면, 자식 클래스에서 오버라이딩된 함수는 모두 자동으로 virtual 속성을 지니게 된다. 자동으로 virtual 속성이 부여된다고 하더라도 자식 클래스의 오버라이딩 함수에도 virtual을 추가하여 명시적으로 표시해주도록 하자. 그렇게 하지 않으면 부모 클래스에 virtual 선언 여부를 확인해야만 한다.


정리하면

  • 오버라이딩한 멤버함수를 virtual 선언하면 그 함수은 해당 객체가 무엇인지에 따라 동적 바인딩되어 호출된다.

댓글 없음:

댓글 쓰기