C, C++

가상함수가 있는 클래스의 이해와 멤버변수의 일괄 초기화 기법

Binceline 2013. 1. 13. 23:12

출처 : http://d00d00.egloos.com/93008

--------------------------

구조체나 클래스나 C++에서는 같이 취급됩니다. 다만 default 영역이 구조체는 private 이고 클래스는 public이라는 점만 다를 뿐입니다. 
그래서 클래스라고 하면 그것은 union까지도 포함하는 구조체를 포괄하는 개념입니다. 

클래스에는 흔히 가상함수가 존재합니다. 
하나의 클래스 내에서 가상함수는 상속관계에 의해 그 함수주소가 변해야 하므로 가상함수 테이블 일명 VTable을 유지하고, 클래스 내에서는 이 VTable(Virtual Table)을 가리키는 4바이트 포인트를 가집니다. 
C++빌더의 경우는 인스턴스를 생성하면 그 인스턴스의 최초 4바이트가 바로 VTable을 가르키는 포인트입니다. 
VC++의 경우도 마찬가지로 인스턴스의 최초 4바이트가 VTable을 가리키는 포인트입니다. 

우선 가상함수가 없는 클래스를 보겠습니다. 

// 가상함수가 없는 구조체 또는 클래스 

class TT1 
{ 
public: 
    int        t1; 
    int        t2; 
    String    str;    // AnsiString클래스는 문자열을 가르키는 4바이트포인트만을 가지는 클래스로, 처음선언시 0(NULL)으로 초기화된다. 그러므로 문자열 대입전에는 0으로 재설정해도 상관없다. 어차피 0 이므로. 

    TT1() 
    { 
        // 가상함수가 없는 순수 클래스/구조체는 이렇게 한줄로 모두 0으로 간단히 초기화 할수 있다. 
        ZeroMemory(this, sizeof(*this)); 
    } 
}; 

생성자에서  ZeroMemory(this, sizeof(*this)); 로 
한방에 멤버변수를 0으로 초기화하는데 아무런 문제가 없습니다. 
가상함수가 없는 간단한 클래스는 이런식으로 초기화하는 것은 실무에서도 많이 쓰이는 기법입니다. 
MS 플머들도 많이 애용하죠. 

하지만 멤버함수중에 가상함수가 존재할때 이렇게 초기화하면 
인스턴스 첫 4바이트인 VTable을 가리키는 포인트값이 0으로 되어 가상함수는 기능을 하지 못하게 됩니다. 
엑세스바이얼레이션 예외를 일으키게 됩니다. 
그러므로 가상함수를 포함하고 있을때는 VTable을 가리키는 포인트를 피해서 초기화를 하면 되겠죠. 

다음은 가상함수를 포함하는 클래스를 보겠습니다. 

// 가상함수를 포함하는 클래스/구조체 

class TT 
{ 
public: 
    int        temp; 
    virtual ~TT()    {    }        // VTable 0번 
    virtual void func1()       { add("run func1");    } // VTable 1번 
    virtual void func2()       { add("run func2");    } // VTable 2번 
    virtual void func3(int a)      { add(String("run func3 ") + a); } // VTable 3번 

    TT() 
    { 
        //add(String().sprintf("&t:%08X &t.temp:%08X", this, &temp)); 
        ZeroMemory(&temp, sizeof(*this) - 4);    // VTable의 포인트를 피해서. 
        add(temp); 
    } 
}; 

생성자에 VTable을 피해서 초기화하는 기법이 보입니다. 
앞 4바이트를 피하겠다고, ZeroMemory(this + 4, sizeof(*this) - 4); 식으로 했다가는 
엉뚱한 곳의 메모리를 0으로 채우게 됩니다. this 는 TT 형이므로 this + 4는 곧 this 메모리번저 + 4 * sizoef(TT); 
와 같기 때문입니다. 위처럼 첫 멤버변수의 메모리 번지를 취하는 것이 안전합니다. 

사이즈에 있어서도 sizeof(*this) - 4 표현보다 
sizeof(*this) - ((int)&temp - (int)this) 가 더 낫습니다만, win32 어플인 경우는 바뀌지 않는 사항이기 때문에, 
그냥 위처럼 그냥 간편하게 사용해도 됩니다. 

위에서는 4개의 가상함수가 쓰였는데, 4개의 함수포인트 배열인 VTable은 클래스에 가상함수가 쓰여진 순서대로 
존재하게 됩니다. 그러므로 위에서 0번부터 순서를 붙여놓은 순서대로 VTable이 구성됩니다.

여기서 주의해야할 것은 VTable의 구성방법에 대해서는 C++표준이 정해진바가 없어 컴파일러 제작사마다 
구현방법이 틀릴수 있다는 사실입니다. C++빌더와 VC++의 구성방법이 틀리며, 지금 설명하고 있는 내용은 
전부  C++빌더 중심적인 설명입니다. 

다음은 가상함수를 포함하는 클래스를 합성한 경우입니다. 

// 가상합수가 있는 클래스를 포함하는 클래스. 

class TT2 
{ 
public: 
    int    temp2; 
    int temp3; 
    TT  t;   // 가상함수가 있는 클래스죠. 

    TT2() 
    { 
        add(String().sprintf("this:%08X temp2:%08X t:%08X t.temp:%08X", this, &temp2, &t, &t.temp)); 
        // 여기서는 ZeroMemory 전체를 초기화해서는 안된다. 
        // 가상테이블이 있는 클래스를 피해서, 
        // 원하는 부분만 한번에 0으로 초기화하는 기법은 아래와 같다. 
        ZeroMemory(&temp2, offsetof(TT2, t) -    offsetof(TT2, temp2)); 
        add(String().sprintf("TT2 특정 블럭 크기:%d", offsetof(TT2, t) - offsetof(TT2, temp2))); 
    } 
    virtual ~TT2() { } 
}; 

예제에서 add(...)함수가 자꾸 나오는데 add(...)는 결과를 메모장에 찍어보기 위한 간단함수입니다. 아래와 같습니다. 

void    add(String msg) 
{ 
    Form1->Memo1->Lines->Add(msg); 
} 

TT2 클래스의 경우를 보면 TT2의 인스턴스의 첫 4바이트는 TT2에 대한 VTable을 가르킵니다. 
안에 포함한 TT의 인스턴스 t 도 VTalbe을 가지고 있는데 마찬가지로 t 의 인스턴스 첫 4바이트가 VTable을 
가르킵니다. 
temp3 변수 다음번지에 t의 인스턴스가 위치하며, t의 인스턴스 첫 4바이트는 TT의 VTable을 가르킨다는 것입니다. 

그러면 TT2 클래스를 한방에 초기화 할수 있는 방법은 없을까요? 
당연히 t 가 VTable에 대한 포인트를 포함하고 있으므로 t 인스턴스를 피해서 초기화를 해야 합니다. 
즉 temp2 변수부터 t 바로 위인 temp3 까지만 0으로 초기화 하면 되죠. 

ZeroMemory(&temp2, offsetof(TT2, t) -    offsetof(TT2, temp2)); 

offsetof를 이용한 한번에 초기화하는 기법입니다. 
temp2부터 t 인스턴스 이전 까지의 크기를 구하는 기법이 보이는데, int 변수가 2개이므로 짐작한대로 8바이트의 
크기가 됩니다. 

지금까지 VTable과 한방에 멤버변수를 0으로 만드는 기법을 설명했는데, 
정확하게 알지 못한다면 그냥 초기화 리스트 즉 생성자에서 일일이 멤버변수에 값을 할당하는 기법으로 
초기화하시기 바랍니다. 
그것이 보다 좋은 프로그래밍 습관입니다. 
여기서의 설명은 C++의 구현된 내부구조를 살펴보는데 목적이 있습니다. 

VTable과 VTable에 대한 포인트의 성질을 이용해서 다음과 같은 실험을 해 볼수 있습니다. 


typedef void (*Tfunc)(void *pClass); 
typedef void (*Tfunc3)(void *pClass, int a); 

void __fastcall TForm1::Button4Click(TObject *Sender) 
{ 
    TT t; 

    int        *pClass = (int *)&t; 
    void    **pVTable = (void **)(*pClass); 
    Tfunc   *func = (Tfunc *)pVTable; 
    add(String().sprintf("VTable:%08X", &func)); 
    func[1](pClass); 
    func[2](pClass); 
    Tfunc3     func3 = (Tfunc3)func[3]; 
    func3(pClass, 4); 
    add(""); 

    TT2 t2; 

} 
//--------------------------------------------------------------------------- 

TT 클래스는 4개의 가상함수를 가지는 클래스입니다. 
그러므로 그 인스턴스의 번지를 받으면 곧 그 클래스에 대한 포인트가 되죠. 
그 포인트의 첫 4바이트는 VTable에 대한 포인트이므로 
그 값을 받아서 VTable를 함수포인트배열로 인식시킵니다. 
    void    **pVTable = (void **)(*pClass); 
이렇게 되면 
pVTable[0] => ~TT() 
pVTable[1] => func1() 
pVTable[2] => func2() 
pVTable[3] => func3(int a) 
를 가르키게 됩니다. 
하지만 pVTable은 함수포인트의 포인트가 아닌 void * 형으로 선언되어 있으므로, 
함수포인트로 캐스팅을 해 줍니다. 
    Tfunc   *func = (Tfunc *)pVTable; 
    Tfunc3     func3 = (Tfunc3)func[3]; 
그리고 원하는 가상함수를 호출할 수 있습니다. 
    func[1](pClass); 
    func[2](pClass); 
    func3(pClass, 4); 

인자로 꼭 pClass를 붙인 이유는 
모든 클래스의 멤버함수는 그 첫 인자로 자신의 인스턴스의 주소 값을 가져야 하기 때문입니다. 
그래야, 그 값을 가지고 인스턴스의 멤버변수를 엑세스 할수있을 테닌까요. 
물론, func1, func2 멤버 함수처럼 인자가 없고, 안에서 멤버변수를 엑세스하지 않는다면 
pClass 가 아닌 NULL을 넘기거나 아예 인자 없이 호출해도 됩니다. 
이 경우 스택의 인자를 호출 받은 함수에서 제거하는 파스칼식 호출 규약이 아니어야 겠죠. 

//--------------------------------------------------------------------------- 

이상 간략한 설명을 했는데 
프로그램할때 거의 신경을 안쓰는 사항이기는 해도, 
아무래도 고급기법을 구사하려면 내부 구조를 아는 것이 좋습니다. 
어차피 COM을 제대로 이해하려면 VTable 에 대한 지식은 필수입니다. 
멤버 변수 초기화 기법은 그냥 참고적으로 이해해도 좋은데, 
제가 프로그램할 때는 사실 자주 이용하는 기법이기도 합니다. 

그림도 없이 설명해서 미안한데, 다음에 기회되면 보충하죠.

-> 볼랜드포럼, 김태선(jsdkts)님 ->http://cbuilder.borlandforum.com/impboard/impboard.dll?action=read&db=bcb_tutorial&no=97

반응형