본문 바로가기

Language/C++

동적 바인딩(Dynaminc Binding)과 가상함수


아..................................

우선 글을 읽기 전에...이글을 올리기 5분전(담배 피고왔음..)에 중반까지 정리하다가 글을 날렸다가 다시썼다는 걸 알아두길 바란다.

ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

자..다시 하자.ㅠㅠㅠ;;;

 

그리고 이 글을 읽기 전에 이전 study인 Derived class point to Base class point Variable 글을 읽고 오길 바란다. 않그러면 왜 동적바인딩이 필요한지를 이해하기가 좀 거시기 하다.

 

이전 study와 연결을 위해서 이전 study를 한번더 복습하면, 상속된 class 끼리의 포인터 는 서로 참조가 가능하다.

하지만 포인터의 자료형이 실 객체의 자료형보다 존중 되기 때문에 실 객체가 무엇이든 간에 포인터의 자료형에 의해서 해당 멤버 함수가 호출이 된다.

바로 모순이 일어나는 것이다.

이런 이유는 바로 멤버 함수와 연결이 되는 시점 즉, 바인딩의 시점때문이라는 것 또한 이전에 배웠다.

그럼 여기서 바인딩의 정의를 보자.

바인딩(Binding)이란?

함수 호출을 해당 함수의 정의와 결합하는(묶어 놓는) 것을 말한다.

즉, 어떠한 class의 객체로 해당 class의 멤버함수를 호출하고자 할때 멤버함수와 객체는 결합하여 호출을 하는데 이때를 바인딩 된다 라고 하는것이다.

때문에 어떤 클레스의 어떤 멤버 함수인지를 알고 호출이 되는 것이다. (This Point로 리턴까지 이해했을꺼라 생각한다)

하지만 C++에서는 오버라이딩과 오버로딩이 제공이 되는데 오버로딩은 상관이 없다 쳐도 오버라이딩된 함수라면 원하는 class의 함수가 호출이 되도록 만들어야 한다는 것이다.

즉,  실객체를 찾아서 함수를 호출해주기 위하여 동적 바인딩이라는 기능을 지원한다.

자, 그러면 왜 이런모순이 발생하는지를 알아보자

기본적으로 C++은 정적바인딩(Static Binding)을 디폴트로 이루어진다.

이 시점을 Compile Time 이라고 하며, Compile은 내부 값은 중요한것이 아니라 선언되어져 있는 Type을 보고 바인딩을 하기 때문에

실제 객체가 무엇이든 해당 포인터 객체의 함수를 호출하는 것이다.

즉, 문법상으로 함수 호출을 해당 함수의 정의와 결합하는 것을 정적 바인딩(Static Binding or Eraly Binding)이라 한다.

그럼 이 글의 주제인 동적 바인딩은?

동적 바인딩은 문법이야 어떻든 내부 실제의 객체 위주로 함수를 호출하게 되는데 이 시점을 Run Time 이라하며, 프로그램이 실행되고

있는 동안에 멤버함수의 결합이 일어난다.

동적 바인딩(Dynaminc Binding or Late Binding)은 프로그램이 실제로 실행이 되면서 이루어 지기 때문에 정확히 실 객체의 Type을 찾아서 결합해주기 때문에 위에서 얘기한 모순이 발생하지 않는다는 것이다.

그럼 내가 원하는 값을 얻기 위하여 Compile에게 이 "구문은 동적 바인딩이야~" 라는 것을 알려줘야 하는데 이때 사용되는 키워드가

바로 그 중요한 Virutual 이라는 키워드이다.

만약 오버라이딩이 된 함수를 동적 바인딩으로 호출하고자 한다면, 다음과 같이 사용하면 된다.

<Type>

virtual void prn();

이렇게 동적 바인딩이 된 함수를 가상 함수라고 한다.

참고로 가상 함수는 멤버 함수일 경우에만 가능하며(당연한것이 이런 기능은 상속이 아니면 필요가 없어..) 가상 함수도 상속외 된다.

소스 하나 보자..

class base{
protected:
 int a, b;
public:
 base():a(0),b(0){}
 ~base(){}
 base(int aa,int bb):a(aa),b(bb){}

 void prn(){cout<<"\nBase Print\n";cout<<a<<"\t"<<b<<endl;}
 void sum(){}
 void addOne(){a++;b++;}
 void addOne(int dd){a+=dd;b+=dd;}

};
class add:public base{
protected:
 int c;
public:
 add(int aa, int bb):base(){a=aa;b=bb;}
 ~add(){}
 void sum(){c = a+b;}
 void prn(){cout<<"\nAdd Print\n";cout<<a<<"+"<<b<<"="<<c<<endl;}
 void addOne(){a++;b++;c = a+b;c++;}

};

void main()
{
 base base_obj(10,20);
 add add_obj(10,20);

 base *pBase;
 add *pAdd;

 pBase = &add_obj;//실 객체가 아닌 Point객체의 멤버가 호출
 
 pAdd = (add *)&base_obj;

 pBase->prn();

 pAdd->sum();
 pAdd->prn();
}

이 소스는 모순을 이르킨다. 바로 정적 바인딩을 디폴트로 하여 실행이 되기 때문이다.

하지만 이 소스를 다음과 같이 변경 하면...

class base{
protected:
 int a, b;
public:
 base():a(0),b(0){}
 ~base(){}
 base(int aa,int bb):a(aa),b(bb){}
 //가상 함수 선언.
 virtual void prn(){cout<<"\nBase Print\n";cout<<a<<"\t"<<b<<endl;}
 virtual void sum(){}
 void addOne(){a++;b++;}
 void addOne(int dd){a+=dd;b+=dd;}

};
class add:public base{
protected:
 int c;
public:
 add(int aa, int bb):base(){a=aa;b=bb;}
 ~add(){}
 void sum(){c = a+b;}
 void prn(){cout<<"\nAdd Print\n";cout<<a<<"+"<<b<<"="<<c<<endl;}
 void addOne(){a++;b++;c = a+b;c++;}

};

void main()
{
 base base_obj(10,20);
 add add_obj(10,20);

 base *pBase;
 add *pAdd;

 //동적 바인딩으로 저장된 객체 우선 으로 호출
 pBase = &add_obj;

 
 pAdd = (add *)&base_obj;

 pBase->prn();

 pAdd->sum();
 pAdd->prn();
}

포인터 객체가 무엇이든 간에 실제 객체 우선으로 멤버가 호출이 되는것을 확인 할 수 있다.

참고로 처음 소스는 에러를 이르킨다.(실행은 될것이다.) 하지만 수정된 소스는 에러없이 자~알 돌아간다.

바로 이를 동적 바인딩 이라고 하는것이다.

자...정리하면,

동적 바인딩은 Run Time에 일어나며, 포인터 객체Type이 아닌 저장된 값 즉, 실제 객체의 Type을 우선적으로 한다.

동적 바인딩을 함수가 아닌 변수에도 사용하는데 이때 사용하는것이 바로 new연산자로 Point 배열을 사용할 때이다.

new 연산자로 배열을 할당하면, 배열의 인덱스 수를 숫자가 아닌 일반 변수로 할당이 가능한데 컴파일은 변수안의 값을 모르지만

프로그램이 실행될때는 그 값이 정해지기 때문에 이런 것이 가능한것이다.

자...하지만 부작용 역시 존재한다.

모든것은 인과율이라는 법칙에서 어긋나지 않는다는 것이다.

기반 클래스의 포인터가 객체의 종류를 기억하는 방법이 필요하기 때문에 처리에 오버해드가 생기며, 시간이나 메모리 사용에 있어서 동적바인딩은 정적 바인딩 보다 능률적이지 못하다.

때문에 이렇게 좋은 기능을 가지고도 정적 바인딩이 디폴트인 이유가 여기에 있다.

기반 클래스의 포인터가 객체의 종류를 기억하는 방법이 필요하다. 라는 구문을 설명하자면,

가상함수가 실행시점에서 해당 함수가 호출되려면 어떤 함수가 호출될지 모르므로 각 객체의 가상함수를 기억공간에 따로 저장을 하게

된다. 이를 가상함수 테이블(Virtual Function table)이라고 하며,  이는 각 객체 단위로 저장되며, RAM의 특정 위치에 저장되고 각 객체

들은 가상 함수 테이블의 주소를 참조하여 실행하게 된다.

또한, 가상 함수 테이블은 객체 단위로 만들어진다. 하나의 객체에 여러 개의 가상 함수들이 존재 하므로 가상 테이블은 함수들의 주소를

배열 형태로 저장하게 된다.

가상 함수 테이블의 주소를 저장하는 포인터가 따로 존재하게 되며, 이를 가상 포인터(Virtural Point 이하 vptr)이라고 한다. 가상 함수 포인터는 가상 함수 테이블을 가리킨다.

이를 그림으로 나타 내보자(PPT의 내용을 스샷한 사진이다.)


상속 관계에 있는 두 클레스 의 객체를 선언하면 위와 같이 메모리에 로드된다.

그리고 Base class에 가상 함수가 만들어지면 객체에 의해 재생된(Overridin) 함수들 중 어떤 것을 호출하여야 하는지를 알아야 하기때문에 가상 함수 테이블을 아래와 같이 생성한다.



이때 가상 함수 테이블은의 주소가 부여된다. (1000번지와 1500번지)

그후, 각 객체의 가상 함수 테이블의 주소를 저장하고 있는 Vptr(가상 포인터) 가 다음과 같은 메커니즘으로 저장 된다.



각 객체마다 vptr이란 가상 포인터가 가상 함수 테이블의 위치를 가리키며, 이로 인하여 정확한 하수를 찾아서 호출을 하게 된다. vptr은 

은닉된 포인터 멤버이다.

위에서 언급 하였지만 부작용은 이런 메커니즘을 가지고 있기 때문에 발생된다.

가상함수를 사용하기 위해서 가상 테이블을 필요로 하며, 가상 함수 만큼 주소 배열을 만들어 줘야하고, 가상 포인터를 매 객체 마다 만들어야 하기 때문에 메모리에 오버해드를 초래한다.

또한, 각 테이블을 이용하여 주소를 조사하여야 하므로 처리속도도 지연된다.

하지만, 이런 부작용보다는 장점이 매우 크기 때문에 프로그램시 많이 사용을 하는것이다.

동적 바인딩을 사용하면, 실행시간에 자유로이 성격이 변할 수 있기 때문에 적응성이 뛰어나며, 유연성있는 프로그램이 된다.

 

-C++ 언어 30일 완성 참조, A-Shell Study team 유창 PPT참조-