본문 바로가기
Dev Language/C++

정적 결합과 동적 결합 & 가상함수

by 미티치 2016. 12. 23.

정적 결합과 동적 결합

 

 소스 코드에 있는 함수 호출을 특정 블록에 있는 함수 코드를 실행하라는 뜻으로 해석하는 것을 함수 이름을 결합한다.”라고 말한다. C언어에서는 각각의 함수가 별개의 함수에 해당하기 때문에 이 작업이 간단했지만, C++에서는 함수 오버로딩 때문에 이 작업이 조금 복잡하다. 어떤 함수를 사용하라는지 알아내기 위해, 컴파일러는 함수 이름뿐만 아니라 함수의 매개변수도 수행할 수 있다. 컴파일 하는 동안에 일어나는 결합을 정적 결합(static binding) 또는 초기 결합이라 한다. 하지만 프로그램을 실행할 때 사용자가 객체를 결정하는 경우, 컴파일 하는 동안에 그 객체가 어떤 종류인지 알 수 없는 경우에는 어떤 함수를 사용할 것인지 컴파일 시에 알 수 없다. 따라서 프로그램을 실행할 때 올바른 가상 메서드가 선택되도록 하는 작업을 동적 결합(dynamic binding) 또는 말기결합 이라고 한다.

 컴파일러는 가상 함수(virtual)가 아닌 함수들에 대해서 정적 결합을 사용하고, 가상 함수에 대해서는 동적 결합을 사용한다. 동적 결합은 클래스 메서드들을 다시 정의하는 것을 허용하지만, 정적 결합은 그것을 허용하지 않는다. 그렇다면 왜 정적 결합을 사용하는가?

-       프로그램이 무언가를 실행 시간에 결정할 수 있도록 하려면, 기초 클래스 포인터나 참조가 지시하는 객체의 종류가 무엇인지 추적하는 방법이 필요하다. 그러려면 가외의 처리 부담이 생긴다. , 메서드를 다시 정의하지 않는 파생 클래스의 경우에 동적 결합이 필요 없다. 이러한 경우 정적 결합을 사용하는 것이 이치에도 맞고 효율적이기 때문에 디폴트는 정적 결합인 것이다.

-       또한 클래스를 설계할 때, 파생 클래스에서 다시 정의되는 것을 원하지 않는 멤버 함수가 있을 수 있다. 이 함수를 가상이 아닌 함수로 만듦으로써, 첫 번째로는 위에서 말했듯이 효율성이 좋아지고, 두 번째로는 다시 정의되면 안 된다는 의도를 드러낼 수 있다.

 

 

가상 함수는 어떻게 동작하는가?

 컴파일러가 가상 함수를 다루는 일반적인 방법은, 각각의 객체에 숨겨진 멤버를 하나씩 추가하는 것이다. 숨겨진 멤버는 함수의 주소들로 이루어진 배열을 지시하는 포인터를 저장한다. 이 배열을 가상 함수 테이블( virtual function table; vtbl )이라고 한다. vtbl에는 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어 있다.

 예를 들어, 기초 클래스의 한 객체는 그 클래스를 위한 모든 가상 함수들의 주소로 이루어진 테이블을 지시하는 포인터를 가진다. 파생 클래스의 한 객체는, 가상 함수들의 주소로 이루어진 별개의 테이블을 지시하는 포인터를 가진다. 파생 클래스가 가상 함수에 대해 새로운 정의를 제공하면, vtbl은 새로 정의된 함수의 주소를 저장한다. 여기서 파생 클래스가 가상 함수를 재정의 하지 않으면 vtbl은 그 함수의 오리지널 버전의 주소를 저장한다.

 가상 함수를 호출하면, 프로그램은 객체에 vtbl 주소가 저장되어 있다는 것을 알게 되고, 함수 주소들로 이루어진 해당 테이블에 접근한다. 사용하는 함수가 클래스 선언에 정의된 첫 번째 가상 함수라면, 프로그램은 그 배열에 있는 첫 번째 주소를 사용하고, 그 주소에 있는 함수를 실행시킨다. 다시 말해서, 가상 함수를 사용하면 메모리와 실행 속도 면에서 약간의 부담이 따르는데,

-       각 객체의 크기가 주소 하나를 저장하는 데 필요한 양만큼 커진다

-       각각의 클래스에 대해, 컴파일러는 가상 함수들의 주소로 이루어진 하나의 테이블(배열)을 만든다.

-       각각의 함수 호출에 대해, 실행할 함수의 주소를 얻기 위해 테이블에 접근하는 가외의 단계가 더 필요하다.

 

 

 

가상함수 규칙

 

-       생성자는 가상으로 선언할 수 없다.

-       클래스가 기초 클래스로 사용된다면, 파괴자는 가상으로 선언해야 한다. 예를 들어, Employee 클래스가 기초 클래스이고, Singer 클래스가 파생 클래스라고 가정하자. Singer에는 new에 의해 대입된 메모리를 지시하는 char * 멤버가 추가되었다. 그렇다면 Singer 객체의 수명이 다했을 때, ~Singer() 파괴자가 호출되어 그 메모리를 해제하는 것이 중요해진다.
           Employee * pe = new Singer;
           delete pe;
이런 코드가 선언되어있다면, 여기서 delete pe~Employee()일까? ~Singer()일까? 여기에 디폴트 정적 결합이 적용된다면, ~Employee() 파괴자를 호출하다. 그러면 이 코드는 Singer 객체에 새로 추가된 클래스 멤버들이 지시하는 메모리는 해제하지 않고, Singer 객체의 Employee 성분들이 지시하는 메모리만 해제한다
.
그러나 파괴자가 가상이라면, 동일한 코드가 ~Singer() 파괴자를 호출한다. 이 파괴자는 Singer 성분이 지시하는 메모리를 해제하고 나서, ~Employee() 파괴자를 호출하여 Employee 성분이 지시하는 메모리를 해제한다
.
(
이것은 기초 클래스가 명시적 파괴자의 서비스를 요구하지 않더라도 디폴트 파괴자에 의존하면 안 된다는 것을 의미한다. 그 대신 아무 일도 하지 않는 가상 파괴자를 제공해야 한다.  , 클래스를 기초 클래스로 사용할 예정이라면, 그 클래스가 파괴자를 요구하지 않더라도 가상 파괴자를 제공해야 한다. )

-       프렌드는 가상 함수가 될 수 없다. 프렌드는 클래스 멤버가 아니기 때문이다.

-       파생 클래스가 함수를 다시 정의하지 않으면, 파생 클래스는 그 함수의 기초 클래스 버전을 사용한다. 파생 클래스가 길게 이어진 파생 사슬의 일부라면, 가장 최근에 정의된 버전을 사용한다.

-       상속된 메서드를 재정의할 경우에는 오리지널 원형과 정확히 일치시킬 필요가 있다.

-       또한 기초 클래스 선언이 오버로딩 되어있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의 해야 한다.

 

순수 가상 함수

 추상화 기초 클래스에 메서드를 구현할 수 없기 때문에 구현되지 않는 함수를 위해 순수 가상 함수(pure virtual function)을 사용한다. 순수 가상 함수는 함수 선언 뒤에 = 0 을 가진다. 클래스 선언에 순수 가상 함수가 들어있으면, 그 클래스의 객체를 생성할 수 없다.

           void Move( int nx, int ny ) = 0;

è Java에서 추상 클래스에 선언하는 추상 메서드와 같은 개념인 것 같다.

 추상화 기초 클래스로 만들려면, 적어도 하나의 가상 함수가 순수 가상 함수가 되어야 한다.

 

'Dev Language > C++' 카테고리의 다른 글

Thread 생성 시 지역변수 유효 시점  (0) 2018.04.11
C++로 DLL 만들기  (0) 2017.03.08
Is a, Has a 관계 & 상속  (0) 2016.12.23
참조 변수 & 복사 생성자  (1) 2016.12.06
if문과 switch문  (0) 2016.12.06