성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold' next="member_func,pointer_size">C++ 클래스의 상속에 따른 메모리 구조</h1> <p> <a name='1'></a> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>1. 비어 있는 단일 클래스</div> <br /> 우선, 빈 클래스를 하나 생성해 볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>class Empty { };</span> void main() { Empty inst; printf("sizeof(inst) == %d\n", sizeof(inst)); } /* 출력 결과 <span style='color: blue; font-weight: bold'>sizeof(inst) == 1</span> */ </pre> <br /> 놀랍게도 출력 결과는 1입니다. 검색을 해보니, 표준에 의해 객체의 크기를 0으로 반환하지 못하고 최소 1을 반환한다고 합니다. "<a target='tab' href='http://stackoverflow.com/questions/2362097/why-is-the-size-of-an-empty-class-in-c-not-zero'>http://stackoverflow.com/questions/2362097/why-is-the-size-of-an-empty-class-in-c-not-zero</a>" 글을 보면 이에 대한 2가지 이유가 나옵니다. 첫 번째로는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Empty *pa = new Empty(); Empty *pb = new Empty(); </pre> <br /> 위와 같은 경우, 만약 빈 클래스가 0 바이트 메모리를 할당한다면 heap의 사용 증가가 없기 때문에 pa, pb 모두 같은 주소를 반환하는 부작용이 있습니다.<br /> <br /> 두 번째 이유는 다음과 같은 식으로 배열의 요소 수를 알아내는 코드가 있는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Empty arr[10]; int elemCount = sizeof(arr) / <span style='color: blue; font-weight: bold'>sizeof(Empty)</span>; // elemCount == 10 </pre> <br /> sizeof(Empty)가 0을 반환하면 DivideByZero 예외 상황이 발생하기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>2. 필드를 가진 단일 클래스</div> <br /> 클래스에 필드 하나를 추가해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FieldSample { <span style='color: blue; font-weight: bold'>char ch1;</span> }; FieldSample inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 1</span> </pre> <br /> 자료형에 따라(위에서는 char 1바이트) 크기가 출력됩니다. char 필드 2개를 추가하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FieldSample { <span style='color: blue; font-weight: bold'>char ch1; char ch2;</span> }; FieldSample inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 2</span> </pre> <br /> 2바이트가 됩니다. 그런데, ch1과 ch2 사이에 int를 넣으면 상황이 달라집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FieldSample { char ch1; <span style='color: blue; font-weight: bold'>int n1;</span> char ch2; }; </pre> <br /> 그럼, 4바이트 정렬이 되어 다음과 같은 layout을 갖게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [오프셋 8: ch2] - 4바이트 [오프셋 4: n1] - 4바이트 [오프셋 0: ch1] - 4바이트 </pre> <br /> 따라서, sizeof(FieldSample)을 하게 되면 12가 나옵니다.<br /> <br /> 낭비되는 메모리가 좀 심하다고 생각되면 (정렬에 따른 성능 향상을 희생하는) pragma pack 옵션을 줄 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>#pragma pack(1) // 1바이트 정렬</span> class FieldSample { public: char ch1; int n1; char ch2; }; </pre> <br /> 그럼 메모리 layout이 다음과 같이 바뀝니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [오프셋 5: ch2] - 1바이트 [오프셋 1: n1] - 4바이트 [오프셋 0: ch1] - 1바이트 </pre> <br /> 따라서 sizeof(FieldSample) == 6이 나옵니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>3. 멤버 함수를 가진 단일 클래스</div> <br /> int 필드 하나에 함수 하나를 추가해볼까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FieldFuncSample { public: int n1 = 1; <span style='color: blue; font-weight: bold'>void func1() { }</span> }; FieldFuncSample inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // sizeof(inst) == 4 </pre> <br /> 보는 바와 같이 함수의 추가는 메모리 변화와 상관없습니다. (당연한 결과입니다.)<br /> <br /> 그런데, 가상 함수(virtual function)를 추가하면 상황이 달라집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class FieldFuncSample { public: int n1 = 1; void func1() { } <span style='color: blue; font-weight: bold'>virtual void vfunc1() { }</span> }; FieldFuncSample inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // sizeof(inst) == 8 </pre> <br /> 왜냐하면 virtual 함수의 추가는, 이에 대한 동적 바인딩을 관리하기 위한 virtual function table(vtable)이 생성되면서 객체는 vtable에 대한 포인터(vptr) 하나를 소유하기 때문입니다. 즉, 다음과 같은 layout을 갖게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [오프셋 4: 필드 n1] - 4바이트 [오프셋 0: vtable 포인터] - 4바이트 </pre> <br /> 물론, 가상 함수의 수와는 상관이 없습니다. 추가되는 가상 함수는 vtable에만 영향을 줄 뿐 객체는 여전히 vtable 포인터 하나만을 유지하기 때문입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>4. 단일 상속 클래스</div> <br /> 그런데, 상속을 받으면 어떻게 될까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>class A</span> { public: int n1 = 1; void func1() { n1 ++; } virtual void vfunc1() { } }; <span style='color: blue; font-weight: bold'>class BonA : A</span> { public: int n2 = 2; void func2() { n2 ++; } virtual void vfunc2() { } }; <span style='color: blue; font-weight: bold'>class ConBonA : BonA</span> { public: int n3 = 2; void func3() { n3 ++; } virtual void vfunc3() { } }; <span style='color: blue; font-weight: bold'>BonA inst;</span> printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 12</span> <span style='color: blue; font-weight: bold'>ConBonA inst2;</span> printf("sizeof(inst2) == %d\n", sizeof(inst2)); // <span style='color: blue; font-weight: bold'>sizeof(inst2) == 16</span> </pre> <br /> 할당된 메모리를 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > BonA inst 객체 (12바이트) [오프셋 8: 필드 BonA::n2] - 4바이트 [오프셋 4: 필드 A::n1] - 4바이트 [오프셋 0: vtable 포인터] - 4바이트 ConBonA inst2 객체 (16바이트) [오프셋12: 필드 ConBonA::n3] - 4바이트 [오프셋 8: 필드 BonA::n2] - 4바이트 [오프셋 4: 필드 A::n1] - 4바이트 [오프셋 0: vtable 포인터] - 4바이트 </pre> <br /> 정직하게 누적된 객체들이 가진 필드의 수에 따라 메모리가 늘어나는 것을 볼 수 있습니다. (참고로, vfunc1, vfunc2, vfunc3 가상 함수들은 어차피 별도로 마련된 vtable에만 기록되기 때문에 할당받은 객체와는 무관합니다.)<br /> <br /> 가만 보면, 규칙이 있습니다. vtable이 가장 하단에 있고, 그 위에 가장 처음의 기반 클래스에 있는 필드들이 먼저 누적되어 쌓입니다. 물론, 이것에는 나름의 이유가 있습니다. 가령 A::func1 함수를,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void func1() { n1 ++; } </pre> <br /> 컴파일된 기계어 코드로 살펴보면 다음과 같이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 00041000 inc dword ptr [ecx+4] // ecx == this 포인터 00041003 ret </pre> <br /> 즉, n1 필드를 접근하는데 this 포인터에 +4 위치로 지정합니다. BonA::func2의 컴파일 결과는 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void func2() { n2 ++; } 00041000 inc dword ptr [ecx+8] 00041003 ret </pre> <br /> 위의 코드를 기반으로 다음과 같은 함수 호출을 할 때,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > BonA inst; inst.func1(); inst.func2(); </pre> <br /> C++ 컴파일러는 다음과 같이 기계어 코드를 간편하게 생성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > mov ecx, [inst] call [func1] call [func2] </pre> <br /> 즉, ecx에 담기는 this 포인터 값을 아무런 변경 없이 func1과 func2에 전달해서 사용할 수 있는 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>5. 단일 상속 클래스들의 형 변환</div> <br /> 상속받은 객체들의 메모리 구조가 저렇게 지정되었기 때문에 this 포인터를 형 변환할 때도 쉬워졌습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ConBonA *pInst = new ConBonA(); BonA *pbInst = (BonA *)pInst; A *paInst = (A *)pInst; </pre> <br /> 간략하게 기계어로 다음과 같이 표현됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>00B71B8E mov eax, [new로 할당받은 힙 주소]</span> 00B71B9B mov dword ptr [ebp-34h],<span style='color: blue; font-weight: bold'>eax</span> // pInst 변수에 ConBonA* 타입의 this 포인터 보관 BonA *pbInst = (BonA *)pInst; 00B71BB1 mov dword ptr [ebp-40h],<span style='color: blue; font-weight: bold'>eax</span> // pbInst 변수에 BonA* 타입의 this 포인터 보관 A *paInst = (A *)pInst; 00B71BB7 mov dword ptr [ebp-4Ch],<span style='color: blue; font-weight: bold'>eax</span> // paInst 변수에 A* 타입의 this 포인터 보관 </pre> <br /> 여기서도 별다르게 this 포인터의 값을 아무런 변경 없이 기반 클래스들의 타입으로 지정해도 무방합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>6. 다중 상속 클래스</div> <br /> 그런데, 이번에는 상속을 다음과 같이 다중으로 받도록 변경해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>class A</span> { public: int n1 = 1; void func1() { n1++; } virtual void vfunc1() { } }; <span style='color: blue; font-weight: bold'>class B</span> { public: int n2 = 2; void func2() { n2++; } virtual void vfunc2() { } }; <span style='color: blue; font-weight: bold'>class C</span> { public: int n3 = 3; void func3() { n3++; } virtual void vfunc3() { } }; <span style='color: blue; font-weight: bold'>class ConBandA : public B, public A</span> { public: int c = 10; void func_c() { c++; } virtual void vfunc_c() { } }; <span style='color: blue; font-weight: bold'>class DonCandBandA : public C, public B, public A</span> { public: int d = 11; void func_d() { d++; } virtual void vfunc_d() { } }; <span style='color: blue; font-weight: bold'>ConBandA</span> inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 20</span> <span style='color: blue; font-weight: bold'>DonCandBandA</span> inst2; printf("sizeof(inst2) == %d\n", sizeof(inst2)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 28</span> </pre> <br /> 객체 크기부터 달라졌는데, "C -> B -> A" 순으로 상속받았을 때는 16바이트가 필요했던 반면 "C -> B, A"와 같이 다중으로 상속받으니 20바이트가 필요해졌습니다. 실제로 이때의 메모리 할당은 다음과 같이 배열됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ConBandA inst 객체 (20바이트) [오프셋16: 필드 ConBandA::c] - 4바이트 [오프셋12: 필드 A::n1] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 8: 필드 ConBandA::vtable 포인터]</span> - 4바이트 [오프셋 4: 필드 B::n2] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 0: B::vtable 포인터]</span> - 4바이트 </pre> <br /> 3개의 상속을 받은 DonCandBandA는 더 복잡해집니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DonCandBandA inst 객체 (28바이트) [오프셋24: 필드 DonCandBandA::d] - 4바이트 [오프셋20: 필드 A::n1] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋16: DonCandBandA::vtable 포인터]</span> - 4바이트 [오프셋12: 필드 B::n2] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 8: B::vtable 포인터]</span> - 4바이트 [오프셋 4: 필드 C::n3] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 0: C::vtable 포인터]</span> - 4바이트 </pre> <br /> 그래도 나름의 규칙이 있습니다. 직접 상속받는 것으로 처리할 클래스 한 개를 제외하고는 전부 하위에 vtable 포인터와 필드를 배치시키고 있습니다.<br /> <br /> 문제는 지금부터입니다. 기존의 직렬로 상속한 구조와는 달리 이제부터는 C/C++ 소스 코드에서 동일한 객체를 사용함에도 불구하고 어떤 메서드를 호출하느냐에 따라 컴파일러가 적극적으로 개입해서 this 포인터의 주소를 조정해 주어야 합니다.<br /> <br /> 우선, 처음 DonCandBandA 타입의 객체를 생성했을 때의 this 포인터는 "오프셋 0" 지점이 있는 메모리 주소를 가리키게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DonCandBandA *pInst = new DonCandBandA(); // pInst 주솟값의 위치는 "오프셋 0"을 가리킴 (이하, 그 주소를 0x1000이라고 가정하고 설명) </pre> <br /> 이 상태에서 DonCandBandA 클래스의 func_d 함수와 C 클래스의 func3 함수를 호출하는 것은 아무런 변경 없이 그대로 사용할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > pInst->func3(); 00C64F6C lea <span style='color: blue; font-weight: bold'>ecx,[0x1000]</span> // this 포인터가 오프셋 0을 가리킴. (할당된 주소가 0x1000으로 가정) 00C64F6F call C::func3 (0C613ACh) pInst->func_d(); 00C64F74 lea <span style='color: blue; font-weight: bold'>ecx,[0x1000]</span> 00C64F77 call DonCandBandA::func_d (0C613B1h) </pre> <br /> C 클래스의 경우 어차피 단독 생성했을 때의 메모리 구조와 다를 바가 없으므로 func3의 코드가,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void func3() { n3 ++; } 00041000 inc dword ptr [<span style='color: blue; font-weight: bold'>ecx+4</span>] // this 포인터에 4를 더했으므로 "[오프셋 4: 필드 C::n3]" 위치를 지정 00041003 ret </pre> <br /> 정상적으로 동작합니다. 또한, DonCandBandA 클래스의 func_d는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void func_d() { d++; } 00C61886 inc dword ptr [<span style='color: blue; font-weight: bold'>ecx+18h</span>] 00041003 ret </pre> <br /> ecx + 0x18(24)의 오프셋 연산을 하는데, 어차피 DonCandBandA 클래스는 C++ 컴파일러가 모든 메모리 레이아웃을 계산할 수 있기 때문에 this 포인터로부터 해당 클래스의 멤버 필드들이 어떤 위치에 알 수 있으므로 저렇게 ecx 변경 없이 곧바로 필드 접근을 할 수 있게 됩니다.<br /> <br /> 그런데, A::func1과 B::func2를 호출할 때는 어떻게 될까요? 이미 A::func1은 n1 필드를 접근하는데 0x04 오프셋을 사용했고, B::func2는 0x04 오프셋을 사용하고 있는 중입니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > void func1() { n1 ++; } 00041000 inc dword ptr <span style='color: blue; font-weight: bold'>[ecx+4]</span> 00041003 ret void func2() { n2 ++; } 00041000 inc dword ptr <span style='color: blue; font-weight: bold'>[ecx+4]</span> 00041003 ret </pre> <br /> 따라서, func_3과 func_d를 호출했던 것과 동일한 this 포인터를 넘겨주면 정상적인 필드 접근을 할 수 없게 됩니다. 이 때문에 C/C++ 컴파일러는 func1, func2 함수를 호출 시에 다음과 같이 this 포인터 주소를 조정해서 넘겨주게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > pInst->func1(); 00C64F7C lea ecx,[<span style='color: blue; font-weight: bold'>0x1000 + 0x10</span>] 00C64F7F call A::func1 (0C613B6h) pInst->func2(); 00C64F84 lea ecx,[<span style='color: blue; font-weight: bold'>0x1000 + 0x8</span>] 00C64F87 call B::func2 (0C613BBh) </pre> <br /> 보는 바와 같이, this 포인터를 넘겨주는 ecx의 값을 A와 B가 마치 상속된 적이 없었다는 듯이 단일 클래스로 new 할당했을 때와 동일한 환경으로 변경해 줍니다. (개발자 입장에서 아무렇지도 않게 호출하던 멤버 함수의 이면에는 C/C++ 컴파일러의 이런 눈물겨운 숨은 노력이 있었던 것입니다. ^^;)<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>7. this 포인터 처리 관점에서의 static_cast, reinterpret_cast, dynamic_cast 형 변환 차이점</div> <br /> 결국, 컴파일러 입장에서는 this 포인터 값에 대해 짝을 이뤄 호출되는 함수가 어떤 클래스에 정의되었느냐에 따라 오프셋 값을 자동화해서 맞춰주는 코드를 산출해 주어야 합니다.<br /> <br /> 이로 인해, 우리가 익히 알고 있던 형 변환 연산자가 static_cast, reinterpret_cast, dynamic_cast로 다양하게 나눠진 것입니다.<br /> <br /> 이전의, A, B, C를 병렬로 다중 상속받았던 DonCandBandA 객체의 메모리 layout을 다시 한번 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DonCandBandA inst 객체 (28바이트) [오프셋24: 필드 DonCandBandA::d] - 4바이트 [오프셋20: 필드 A::n1] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋16: DonCandBandA::vtable 포인터]</span> - 4바이트 [오프셋12: 필드 B::n2] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 8: B::vtable 포인터]</span> - 4바이트 [오프셋 4: 필드 C::n3] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 0: C::vtable 포인터]</span> - 4바이트 </pre> <br /> 형 변환이 얼마나 복잡해지는지 유추해 볼 수 있습니다. 가령, 다음과 같은 코드에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DonCandBandA *pInst = new DonCandBandA(); A *paInst = (A *)pInst; </pre> <br /> C/C++ 컴파일러는 pInst가 오프셋 0 지점을 가리키고 있으므로 클래스 A 타입으로 형 변환했을 때, 16 바이트(0x10)를 더한 값을 반환해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > DonCandBandA *pInst2 = ...; 00D31D95 mov eax, [<span style='color: blue; font-weight: bold'>inst2</span>] A *paInst = (A *)pInst2; 00D31D9E mov [paInst], [<span style='color: blue; font-weight: bold'>eax + 10h</span>] </pre> <br /> 그럼, paInst 변수는 오프셋 16 위치를 자신의 this 포인터 위치로 알고 동작하게 됩니다. 그런데 이 상황에서 해당 포인터를 B 클래스로 바꾸는 경우 어떻게 될까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > B *pbFromPa1 = (B *)paInst; pbFromPa1->func2(); </pre> <br /> 아시는 것처럼, 캐스팅 연산자는 '강제 형 변환'을 하게 되므로, paInst와 pbFromPa의 주솟값이 같습니다. 즉, func2가 실행될 때의 ecx에 들어 있는 this 포인터 주소는 오프셋 8이 아닌 오프셋 16의 위치를 가리키고 있으므로 func2 실행은 B::n2 필드의 값을 접근하지 못하고 A::n1 필드의 값을 접근하는 결과를 낳게 됩니다. (현실적인 경우, 보통 이렇게 되면 비정상 종료를 하는 등의 예측할 수 없는 결과를 보게 됩니다.)<br /> <br /> 이 문제는 강제 형 변환을 하는 reinterpret_cast에서도 동일하게 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > B *pbFromPa2 = reinterpret_cast<B *>(paInst); // 주솟값이 같음: pbFromPa2 == paInst </pre> <br /> 반면, static_cast를 쓰게 되면 컴파일 시에 아예 에러를 내며 쓸 수 없게 해줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > A *paFromPbError = static_cast<B *>(paInst); // Error C2440 'static_cast': cannot convert from 'A *' to 'B *' </pre> <br /> 마지막으로 dynamic_cast를 쓰면 RTTI 정보를 참조해 결국 DonCandBandA 객체의 포인터라는 것을 알 수 있어 정상적으로 B 포인터 값을 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > B *pbFromPa3 = dynamic_cast<B *>(paInst); // pbFromPa3 = paInst + offset(8) </pre> <br /> dynamic_cast는 RTTI 정보를 참조한다는 것에 유의해야 합니다. 만약 (Visual C++의 경우) C/C++의 Langauge 범주에서 "Enable Run-Time Type Information" 옵션을 "No(/GR-)"로 설정하면 RTTI 정보가 없어지는데 이 상황에서 dynamic_cast를 실행하면 (컴파일 타임이 아닌) 런타임 시에 다음과 같은 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > A *paFromPb3 = dynamic_cast<A *>(pbInst); // RTTI 정보가 없으면 Runtime에 다음과 같은 예외 발생 // Exception thrown at 0x0F1A813F (vcruntime140.dll) in ConsoleApplication1.exe: 0xC0000005: Access violation reading location 0x00000004. occurred </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>8. 마름모 꼴의 상속 클래스</div> <br /> 그런데, 문제는 더욱 복잡해질 수 있습니다. 이른바 마름모 꼴 상속을 받는 구조가 바로 그것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>class A</span> { public: int n1 = 1; void func1() { n1++; } virtual void vfunc1() { } }; <span style='color: blue; font-weight: bold'>class BonA : public A</span> { public: int n2 = 2; void func2() { n2++; } virtual void vfunc2() { } }; <span style='color: blue; font-weight: bold'>class ConA : public A</span> { public: int n3 = 3; void func3() { n3++; } virtual void vfunc3() { } }; <span style='color: blue; font-weight: bold'>class Don_BonA_and_ConA : public BonA, public ConA</span> { public: int d = 11; void func_d() { d++; } virtual void vfunc_d() { } }; <span style='color: blue; font-weight: bold'>Don_BonA_and_ConA</span> inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 28</span> </pre> <br /> 객체가 할당된 메모리 layout을 보면 2번 상속받게 된 A 클래스의 필드 멤버들이 2중으로 존재한다는 것을 알 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Don_BonA_and_ConA inst 객체 (28바이트) [오프셋24: 필드 Don_BonA_and_ConA::d] - 4바이트 [오프셋20: 필드 ConA::n3] - 4바이트 [<span style='color: blue; font-weight: bold'>오프셋16: 필드 A::n1</span>] - 4바이트 [<span style='color: blue; font-weight: bold'>오프셋12: Don_BonA_and_ConA::vtable 포인터</span>] - 4바이트 [오프셋 8: 필드 BonA::n2] - 4바이트 [<span style='color: blue; font-weight: bold'>오프셋 4: 필드 A::n1</span>] - 4바이트 [<span style='color: blue; font-weight: bold'>오프셋 0: BonA::vtable 포인터</span>] - 4바이트 </pre> <br /> 이 때문에 다음과 같이 BonA, ConA에 대해 형 변환을 한 다음 호출하면 각각의 n1 필드 값을 변경하게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Don_BonA_and_ConA *pInst = new Don_BonA_and_ConA(); BonA *pbaInst = (BonA *)pInst; ConA *pcaInst = (ConA *)pInst; pbaInst->func1(); // (BonA *)로 호출한 func1은 오프셋 4의 A::n1 필드를 변경하고, pcaInst->func1(); // (ConA *)로 호출한 func1은 오프셋 16의 A::n1 필드를 변경 </pre> <br /> 문제는, pInst 변수로 func1을 호출할 때 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > pInst->func1(); // Error C2385 ambiguous access of 'func1' </pre> <br /> DonCandBandA 클래스 입장에서는 func1의 호출로 인해 (결국) 2중으로 존재하는 객체를 접근하게 되므로 이에 대한 선택을 자동화할 수 없기 때문에 개발자에게 명시할 것을 요구합니다. 따라서, 다음과 같이 원하는 측의 상속을 지정해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > pInst->BonA::func1(); </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>9. 마름모 꼴의 가상 상속 클래스</div> <br /> 마름모 꼴로 상속되는 경우의 중복 데이터 문제를 해결하기 위해, 중복되는 클래스를 virtual로 상속할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>class A</span> { public: int n1 = 1; void func1() { n1++; } virtual void vfunc1() { } }; class BonVirtualA : <span style='color: blue; font-weight: bold'>virtual public A</span> { public: int n2 = 2; void func2() { n2++; } virtual void vfunc2() { } }; class ConVirtualA : <span style='color: blue; font-weight: bold'>virtual public A</span> { public: int n3 = 3; void func3() { n3++; } virtual void vfunc3() { } }; <span style='color: blue; font-weight: bold'>class D_virtual_on_BonA_and_ConA : public BonVirtualA, public ConVirtualA</span> { public: int d = 11; void func_d() { d++; } virtual void vfunc_d() { } }; <span style='color: blue; font-weight: bold'>D_virtual_on_BonA_and_ConA</span> inst; printf("sizeof(inst) == %d\n", sizeof(inst)); // <span style='color: blue; font-weight: bold'>sizeof(inst) == 36</span> </pre> <br /> 중복된 데이터가 없어졌는데도 불구하고 객체의 크기는 오히려 기존 28에서 virtual로 바꾼 경우 36으로 늘어납니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > D_virtual_on_BonA_and_ConA inst 객체 (36바이트) [오프셋32: 필드 A::n1] - 4바이트 [오프셋28: A::vtable 포인터] - 4바이트 [오프셋24: 필드 D_virtual_on_BonA_and_ConA::d] - 4바이트 [오프셋20: 필드 ConVirtualA::n3] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋16: ConVirtualA의 virtual 기반 클래스 정보 포인터] - 4바이트</span> [오프셋12: D_virtual_on_BonA_and_ConA::vtable 포인터] - 4바이트 [오프셋 8: 필드 B::n2] - 4바이트 <span style='color: blue; font-weight: bold'>[오프셋 4: BonVirtualA의 virtual 기반 클래스 정보 포인터] - 4바이트</span> [오프셋 0: BonVirtualA::vtable 포인터] - 4바이트 </pre> <br /> 가만 보면, BonVirtualA의 vtable 포인터와 D_virtual_on_BonA_and_ConA::vtable 포인터의 다음 오프셋 위치에 virtual base 클래스인 A::vtable 포인터에 대한 위치 변위를 갖고 있는 정보의 주소가 기록됩니다. (이 값을 "Effective C++ Digital Collection: 140 Ways to Improve Your Programming" 책에서는 "Pointer to virtual base class"라고 지칭)<br /> <br /> 이 값이 어떻게 활용되는지 역어셈블을 해보면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > D_virtual_on_BonA_and_ConA *pInst2 = new D_virtual_on_BonA_and_ConA(); 00B220AE mov dword ptr [pInst2],[this pointer address] // pInst2에는 D_virtual_on_BonA_and_ConA 객체의 오프셋 0 위치를 가리킴. int nback = pInst2->n1; 00B220B4 <span style='color: blue; font-weight: bold'>mov eax,dword ptr [pInst2]</span> 00B220BA mov ecx,dword ptr [<span style='color: blue; font-weight: bold'>eax+4</span>] // 오프셋 0 위치의 바로 위에 있는 "Pointer to virtual base class" 주소를 구함 // 가령, 구해진 ecx 값이 0x00b27bf0라고 할때, // 0x00b27bf0 == fx ff ff ff // <span style='color: blue; font-weight: bold'>0x00b27bf4 == 18 00 00 00</span> // 0x00b27bf8 == 00 00 00 00 00B220BD mov edx,dword ptr [<span style='color: blue; font-weight: bold'>ecx+4</span>] // 변위값 0x18 값이 edx로 들어감 00B220C6 mov ecx,dword ptr [<span style='color: blue; font-weight: bold'>eax+edx+8</span>] // [this pointer + 0x18 + 8]이 되어 // 결국 pInst2 주소로부터 0x20(32) 위치의 값을 가리키고 그것이 필드 A::n1 임. </pre> <br /> 그럼, 이번에는 ConVirtualA 타입으로 형 변환 후 구하면 어떻게 될까요?<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 249: ConVirtualA *vba = (ConVirtualA *)pInst2; 250: nback = vba->n1; 00B2211B mov eax,dword ptr [<span style='color: blue; font-weight: bold'>pInst2 주소로부터 오프셋 12 위치의 vtable</span>] 00B22121 mov ecx,dword ptr [<span style='color: blue; font-weight: bold'>eax+4</span>] // 오프셋 16 위치의 또 다른 "Pointer to virtual base class" // 가령, 구해진 ecx 값이 0x00b27bfc라고 할때, // 0x00b27bfc == fx ff ff ff // <span style='color: blue; font-weight: bold'>0x00b27c00 == 0c 00 00 00</span> // 0x00b27c04 == 00 00 00 00 00B22124 mov edx,dword ptr [<span style='color: blue; font-weight: bold'>ecx+4</span>] // 변위값 0x0c 값이 edx로 들어감 00B2212D mov ecx,dword ptr [<span style='color: blue; font-weight: bold'>eax+edx+8</span>] // [this pointer + 0x0c + 8]이 되어 // 결국 vba 주소로부터 0x14(20) 위치의 값을 가리키고 그것이 필드 A::n1 임. </pre> <br /> 보는 바와 같이 코드가 참 복잡해지는데, 이러한 모든 복잡성을 C/C++ 컴파일러가 책임지기 때문에 그나마 개발자가 '상속'이라는 기능을 사용할 수 있게 된 것입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 휴~~~ 이것으로 C++ 클래스의 메모리 layout이 대충 정리된 것 같습니다. <br /> <br /> 보는 바와 같이, C/C++은 다중 상속을 허용함으로 인해 복잡한 컴파일러가 되었습니다. 어찌 보면, C#이나 Java에서 단일 상속만을 허용한 것은 이런 면에서 봤을 때 당연한 것이 아니었을까 ... 라고 당연하게 생각할 정도입니다. ^^;<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1123&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
8076
(왼쪽의 숫자를 입력해야 합니다.)