설명 -

virtual 키워드는 함수에 붙일 수 있다. 이 키워드를 붙이는 의미는 가상 테이블(virtual table)을 작성하기 위해서다. 이를 통해서 오버라이딩이 가능해진다.


기능 및 특징 -

오버라이딩이 가능해진다. 앞뒤 다 빼먹고 이게 뜬금없이 무슨 소린가 하겠지만, 상속관계도에서 오버라이딩을 하기 위해서는 가상 함수여야한다.


먼저 객체는 생성되었을 때 자기 자신의 멤버를 가리키고 있다. 만약 부모의 객체를 자식을 가리킨다면 어떨까? 부모 포인터가 자식을 가리켜도 객체 자체는 자식이니까 우리는 자식을 원활하게 쓸 수 있을까?

 

정답은 불가능 이다. 

 

이런 것이 뭐가 중요할까 싶지만... RTS 게임을 구현하여서 유닛이 100종류 정도 있다. 물론 각각의 공격 방식이나 형태 등은 전부 다를 것이므로, 우리가 '공격' 명령을 내렸을 때 유닛이 취할 행동은 제각각 일 것이다.

 

그렇다면 우리는 100 종류의 유닛에 대하여 100종류의 호출을 해주어야할까? 단순히 사용자가 어택 을 누르면 전부 다?

 

 

위의 질문으로 돌아가보자, 부모가 자식을 가리켜 '자식에서 새롭게 정의된 기능'을 쓸 수 있다면 저런 바보같은 행동은 필요하지 않다. 모든 유닛이 부모로부터 '공격'을 상속받게 하고 부모 타입으로 죄다 받아서 공격 명령을 내리면 각각이 자기 자신에 맞는 공격을 행하게 되는 것이다.

 

이를 다형성이라고 하며, virtual 키워드는 오버라이딩을 성립할 수 있게 해주어 다형성을 구현하는 핵심 역할을 한다. 이 과정에서 가상 함수 포인터(virtual function pointer)가 생겨난다.


 

사용 방법 예시 및 이해도 -

 

 

이처럼 접근하면...

 

 

 

 

 

 

 

 

 

 전부 조상이 나와버린다.

 

 

 

그러나 virtual 키워드를 붙이면 결과가 다르다.

 

 

 

 


 

 

 

 


 

그렇다면 왜 이러한 결과가 나오는 것일까?

 

 

 

이 상황을 보다 명시적으로 보기 위해서 내부의 소스(자신의 주소를 표기하도록)를 아래와 같이 수정하였다. 그리고 돌려보았다.

 

 

 

 

 

분명 this의 주소는 계속해서 변하고 있다. 즉 객체 자신을 의미하는 this 포인터는 전부 다르다는 의미인데? 문제는 3번을 타는 동안 계속해서 super를 타고 있다.

 

결과 :

 

 

이럴수가. 전부 함수의 주소가 같다.

 

 

우리는 여기서 '정적 바인딩'과 '동적 바인딩'의 개념을 상기해야한다.

 

 

컴파일러는 함수가 어떤 주소에 있는지 알고 있으며 그래서 호출문을 이 함수의 주소로 점프하는 코드로 번역할 것이다.

 

컴파일하는 시점(정확하게는 링크 시점)에 호출 주소가 결정되는 것을 정적 결합[정적 바인딩](Static Binding) 또는 이른 결합(Early Binding)이라고 한다. 결합(Binding)이란 함수 호출문에 대해 실제 호출될 함수의 번지를 결정하는 것. 일반적인 함수들은 모두 정적 결합이다.

 

바인딩은 다른 함수만이 아니라 컴파일 전반 사용되는데 이에 대해서는 본 블로그의 게시글 '바인딩'에 기록하였.

 

모두 정적 결합이다.. 따라서 지금의 상황이 일어난 것이다. super의 print는 처음에 '정적바인딩 되어 기록된 002510E1'이다. 그렇기 때문에 super의 입장에서는 print == 002510E1 이라고 생각한다. 객체가 무엇이건 print는 002510E1 주소로만 가면 되기 때문에.. 부모 , 자식의 print가 호출될 하등의 이유가 없는 것이다!

 

 

 

그럼 이번에는 virtual을 사용한 경우를 보도록 하자.

 

바로 결론부터 보자.

 

 

그런데.. 결론은 다르지만 주소는 같지 않은가?

뭔가 잘못 된 것이 아닌가 싶지만.. 잘못되지 않았다. 위의 주소는 정적 바인딩된 주소일 뿐이다.

 

이게 무슨 소리냐면.. 가상 함수는 '동적 바인딩' 된다.

 

가상 함수를 소유한 클래스는 가상 함수 테이블(vftable)을 가리키는 4바이트의 가상 함수 포인터(vfptr)을 가지게 된다. 이 것의 존재 여부에 대해서는 함수를 하나 만들어둔 클래스와, 가상 함수를 하나 만들어둔 클래스 간의 size 비교를 통해서도 알 수 있다.

(그냥 함수인 곳은 1바이트 , 가상 함수 측은 4바이트가 나올 것이다. 자세한 내용은 EBCO를 참조.)

 

동적 바인딩이란, 동적 시간대(Runtime)에 결합 되는 것을 말하는 데. 현재의 상황의 경우, 처음부터 함수의 주소를 결정해놓고 호출하는 것이 아니라, 호출 시에 함수의 주소를 알려주고 있는 상황이 된다. 아래의 예제를 보자.

 

 

 

 

 

 

 

 

 

각 vfptr이 가리키고 있는 곳이 다르지 않은가? 그렇다. 동적 바인딩은 이처럼 함수에 대해서 저장된 포인터를 써서 가리키는 방법인 것이다.

 

그로 인해 우리는 각 객체의 가상 함수 포인터(vfptr)에 맞는 함수를 호출 하는 것이 가능해졌다!

 

 

덧붙여 memset을 배운 사람이라면.. 가상 함수가 있는 클래스 / 구조체에는 memset을 사용하지 마라.. 라는 이야기를 들어보았을 것이다. 이유는 아래와 같다.

 

 

 

 

이 때문에 memset은 가상 함수를 보유한 클래스에 사용하면 안된다.

 

 

덧붙여 업 캐스팅 후 해제할 때의 문제를 해결할 수도 있다.

부모가 자식을 가리키고 있는 경우, 그대로 해제하면 부모의 범위만 해제되는 문제가 있다.

virtual 키워드를 사용하여 이 문제를 해결할 수 있다.

 

 

 

 

 

 


 

장단점 및 비교 -

단점

1. 클래스 크기에 4바이트가 더 들어간다. (가상 함수 포인터)

2. 내부에서 체크를 해야하므로 조금이라도 더 느려지긴 느려진다.

3. memset등의 전체 메모리 초기화 함수를 사용할 수 없다.
장점
오버라이딩을 가능하게 한다.

 

첨부 자료 -

 

 

오버라이딩.cpp

 


 

참고한 주소 및 정보 -

// 정적 바인딩 , 동적 바인딩에 대하여

soen.kr
http://secretroute.tistory.com/entry/140819

Posted by GENESIS8

댓글을 달아 주세요