有时我们会将对象作为参数传入函数,其中有不少所谓的“陷阱”,大部分都与对象中申请的内存释放有关,到底是什么原因导致的这些问题呢?所谓的默认的拷贝构造函数又是什么呢?它又做了怎么样的工作,编译器又是怎么样调用它的呢?让我们一起走进C++的内部世界
当类所占用的空间很小时,比如只有几个int数据成员,编译器会选择直接将几个数据成员做压栈操作(先声明的数据后压栈,后声明的数据先压栈),以供调用函数使用
测试代码:
class UnitTest
{
public:
int m_One;
int m_Two;
};
void ShowUnitTest(UnitTest unit)
{
printf("%d %d %s\n", unit.m_One, unit.m_Two);
}
int main(int argc, char *argv[])
{
UnitTest test;
test.m_One = 1;
test.m_Two = 2;
ShowUnitTest(test);
system("pause");
return 0;
}
看其反汇编的代码:
int main(int argc, char *argv[])
{
00BB1420 push ebp
00BB1421 mov ebp,esp
00BB1423 sub esp,0D0h
00BB1429 push ebx
00BB142A push esi
00BB142B push edi
00BB142C lea edi,[ebp-0D0h]
00BB1432 mov ecx,34h
00BB1437 mov eax,0CCCCCCCCh
00BB143C rep stos dword ptr es:[edi]
UnitTest test;
test.m_One = 1;
00BB143E mov dword ptr [test],1
test.m_Two = 2;
00BB1445 mov dword ptr [ebp-8],2
ShowUnitTest(test);
00BB144C mov eax,dword ptr [ebp-8]
00BB144F push eax
00BB1450 mov ecx,dword ptr [test]
00BB1453 push ecx
00BB1454 call ShowUnitTest (0BB11E5h)
可以看到00BB144C 的压栈顺序确实是先声明后压栈,后声明先压栈,直接使用push解决问题。
再下面我们看一下类的体积较大时编译器的处理过程
先看一段代码:
class UnitTest
{
public:
int m_One;
int m_Two;
char m_sName[32];
};
void ShowUnitTest(UnitTest unit)
{
printf("%d %d %s\n", unit.m_One, unit.m_Two, unit.m_sName);
}
int main(int argc, char *argv[])
{
UnitTest test;
test.m_One = 1;
test.m_Two = 2;
char tmp[] = "name";
strncpy_s(test.m_sName, tmp, sizeof(tmp));
ShowUnitTest(test);
system("pause");
return 0;
}
反汇编代码为:
int main(int argc, char *argv[])
{
00053D00 push ebp
00053D01 mov ebp,esp
00053D03 sub esp,104h
00053D09 push ebx
00053D0A push esi
00053D0B push edi
00053D0C lea edi,[ebp-104h]
00053D12 mov ecx,41h
00053D17 mov eax,0CCCCCCCCh
00053D1C rep stos dword ptr es:[edi]
00053D1E mov eax,dword ptr ds:[00058004h]
00053D23 xor eax,ebp
00053D25 mov dword ptr [ebp-4],eax
UnitTest test;
test.m_One = 1;
00053D28 mov dword ptr [test],1
test.m_Two = 2;
00053D2F mov dword ptr [ebp-2Ch],2
char tmp[] = "name";
00053D36 mov eax,dword ptr ds:[00055864h]
00053D3B mov dword ptr [tmp],eax
00053D3E mov cl,byte ptr ds:[55868h]
00053D44 mov byte ptr [ebp-3Ch],cl
strncpy_s(test.m_sName, tmp, sizeof(tmp));
00053D47 push 5
00053D49 lea eax,[tmp]
00053D4C push eax
00053D4D lea ecx,[ebp-28h]
00053D50 push ecx
00053D51 call strncpy_s<32> (0511EFh)
00053D56 add esp,0Ch
ShowUnitTest(test);
00053D59 sub esp,28h
00053D5C mov ecx,0Ah
00053D61 lea esi,[test]
00053D64 mov edi,esp
00053D66 rep movs dword ptr es:[edi],dword ptr [esi]
00053D68 call ShowUnitTest (0511E5h)
我们直接观察0x00053D59 开始调用ShowUnitTest处的汇编代码:
我们发现首先通过栈指着esp 开辟了0x28H(40)大小的空间,正好是UnitTest对象的大小。
然后通过rep movs 指令将对象test拷贝了一份到此处。
而这里的操作即是《C++ Primer》中所说的调用默认拷贝构造函数并创建的临时变量。
编译器在这里插入了申请栈空间及直接复制对象在栈上内存的内存数据的汇编指令(即浅拷贝)
然后我们观察ShowUnitTest的内部实现:
ShowUnitTest:
00281420 push ebp
00281421 mov ebp,esp
00281423 sub esp,0C0h
00281429 push ebx
0028142A push esi
0028142B push edi
0028142C lea edi,[ebp-0C0h]
00281432 mov ecx,30h
00281437 mov eax,0CCCCCCCCh
0028143C rep stos dword ptr es:[edi]
printf("%d %d %s\n", unit.m_One, unit.m_Two, unit.m_sName);
0028143E mov esi,esp
00281440 lea eax,[ebp+10h]
00281443 push eax
00281444 mov ecx,dword ptr [ebp+0Ch]
00281447 push ecx
00281448 mov edx,dword ptr [unit]
0028144B push edx
0028144C push 2859DCh
00281451 call dword ptr ds:[289118h]
00281457 add esp,10h
0028145A cmp esi,esp
0028145C call __RTC_CheckEsp (028113Bh)
}
从00281440这里可以看出,程序在向ebp的下方(即更高的地址位置)进行寻址操作,而操作的位置就是该函数的调用函数的栈帧,而 ebp + 10h 对应的就是临时变量的m_sName的首地址,ebp+0Ch 即临时对象m_Two的首地址, [unit] 即ebp+08 ,即临时对象的首地址,也就是对象m_One的位置。