上上篇我们介绍了类,上一篇介绍了汇编,有了汇编的知识,我们这次就来深入分析类和对象。看看类是怎么构造的,对象又是什么?
6.1 对象是如何构造的
有人问程序员:“有没有对象?”,程序员都是回复:“new 一个”。那作为程序员,知道对象时怎么new出来的么?不知道的话,下次就别说了,哈哈哈。
6.1.1 无继承对象构造步骤
c++语言中,创建一个对象,其实是有几种的,比如new一个堆上的对象,栈上申请的对象,还有全局对象,还有可能是静态对象,这些对象,我们下一节课再分析。
我们先挑栈上的对象来分析,new我们会在后面分析。
class A
{
public:
A(int i) //: m_i(i)
{
m_i = i;
}
int m_i;
};
int main()
{
std::cout << "Hello World!\n";
// 我们创建对象
A a(1); // 这样子就构造起来了
// 那我们的
}
要想了解对象是怎么构造的,我们反汇编查看。
// 我们创建对象
A a(1); // 在c++层面,对象是这么构造的
// 下面我们来看看汇编层面
00007FF7EA542260 BA 01 00 00 00 mov edx,1 // 把1存入到 edx中
00007FF7EA542265 48 8D 4D 04 lea rcx,[a] // 如果是按寄存器来写是:[rbp+4] // 把a的地址存入到rcx中
// rcx = 0000000A60AFFCF4
00007FF7EA542269 E8 CA F1 FF FF call A::A (07FF7EA541438h)
// 准备跳转到 类A的构造函数的地址
// 我们记录一下RSP = 0000001CDB6FF690 RBP = 0000001CDB6FF6B0
// a地址 = 0000001CDB6FF6B4
就是调用我们写的构造函数。继续深入
class A
{
public:
A(int i) //: m_i(i)
// 这时候的RSP = 0000001CDB6FF688看着只是把下一步的地址入栈
00007FF6E8D41D80 89 54 24 10 mov dword ptr [rsp+10h],edx
// edx=1 地址 = 0000001CDB6FF698
00007FF6E8D41D84 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
// rcx=&a 地址 0000001CDB6FF690
00007FF6E8D41D89 55 push rbp
// 这个就不说了,入栈
00007FF6E8D41D8A 57 push rdi
// 这个也是入栈
00007FF6E8D41D8B 48 81 EC E8 00 00 00 sub rsp,0E8h
// 奢侈的window RSP=0000001CDB6FF590
00007FF6E8D41D92 48 8D 6C 24 20 lea rbp,[rsp+20h]
// rbp=0000001CDB6FF5B0
00007FF6E8D41D97 48 8D 0D C9 12 01 00 lea rcx,[__9B3DB3DC_07@1 深入分析类和对象@cpp (07FF6E8D53067h)]
00007FF6E8D41D9E E8 2C F6 FF FF call __CheckForDebuggerJustMyCode (07FF6E8D413CFh)
{
m_i = i;
// 这个偏移这么大,就知道是取传参的值,0000001CDB6FF5B0+E0 = 0000001CDB6FF690,刚好就是a的地址,这个在windos显示符号,就是this。传说中的this指针
00007FF6E8D41DA3 48 8B 85 E0 00 00 00 mov rax,qword ptr [rbp+00000000000000E0h]
// 这个偏移一算,就是我们传参的i
00007FF6E8D41DAA 8B 8D E8 00 00 00 mov ecx,dword ptr [rbp+00000000000000E8h]
// 最后一步,就是把ecx的值赋值到this指针中,这句是不是很懵逼,就是m_i在this指针的偏移就是0,赋值给this,其实就是赋值给m_i
00007FF6E8D41DB0 89 08 mov dword ptr [rax],ecx
}
00007FF6E8D41DB2 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
// 这个我们不用寄存器表示了,就是构造函数就是需要返回this
00007FF6E8D41DB9 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
00007FF6E8D41DC0 5F pop rdi
00007FF6E8D41DC1 5D pop rbp
00007FF6E8D41DC2 C3 ret
这个就是我们的构造函数了。
在栈上的对象构造就是这么简单。
关于对象是怎么new出来的,我们要看下下篇。后面还有有继承的时候对象是怎么构造的。
所以没有继承的对象,构造就是调用了构造函数,重点就是编译器会把a的地址传入,并且就是我们传说中的this指针。
构造函数其实也简单,就是使用这个this指针,进行各个变量的偏移赋值。
6.2 构造函数详解
上面介绍了对象是怎么构造的,其实就是编译器自动调用构造函数,这节我们就来分析构造函数。
通过前面学习,我们知道类有三种构造函数,默认构造函数,拷贝构造函数,移动构造函数,我们就分别来分析。
6.2.1 默认构造函数
6.2.1.1 不写构造函数
我们上面写的例子是,我们主动写了构造函数,那我们这里就猜想一下,如果我们不写构造函数,编译器会不会给我们产生默认构造函数,我们来看看:
class C
{
public:
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
};
int main()
{
std::cout << "Hello World!\n";
// 我们创建对象
//A a(1); // 这样子就构造起来了
// 那我们的
C c; // 直接调用变量是不行的
c.show();
C c1 = C(); // C c1(); 这样语法不行
c1.show();
}
我们执行看看:
Hello World!
m_a: -858993460 m_b:-858993460
m_a: 0 m_b:0
没加括号的,应该是没有初始化,那到底有没有调用构造,我们反汇编查看。
C c; // 直接调用变量是不行的
c.show();
00007FF69CC12000 48 8D 4D 08 lea rcx,[c] // 这个就是啥也没干,构造函数也没有
00007FF69CC12004 E8 5C F4 FF FF call C::show (07FF69CC11465h)
C c1 = C(); // C c1(); 这样语法不行
00007FF69CC12009 48 8D 45 28 lea rax,[c1] // 把c1的地址 放入rax
00007FF69CC1200D 48 8B F8 mov rdi,rax // 把rax的值存入到rdi
00007FF69CC12010 33 C0 xor eax,eax // 这是eax清0 比mov eax,0这个效率高
00007FF69CC12012 B9 08 00 00 00 mov ecx,8 // ecx=8
00007FF69CC12017 F3 AA rep stos byte ptr [rdi]
c1.show();
00007FF69CC12019 48 8D 4D 28 lea rcx,[c1]
00007FF69CC1201D E8 43 F4 FF FF call C::show (07FF69CC11465h)
rep 和 stos是两条指令,下面有指令的详解。
这句汇编的意思就是,把this指针指向的ecx(8字节)空间清0,这也是我们刚刚看到的效果,默认初始化为0了。
通过上面的分析我们就感受到,编译器在这种情况下,是不会自动生成一个默认构造函数的,只是把类变量的值清0了。
6.2.1.2 啥时候编译器会生成构造函数
通过上面的分析,我们发现,编译器并不会给我们生成构造函数,这就跟我们老师讲的不一样了,老师说编译器会自动生成构造函数的,那啥情况下会生成默认构造函数呢?
其实这里可以去看《深入分析c++对象模型》,有下面四种情况:
- 包含类成员B,且B有默认构造函数
- 继承类B,且B有默认构造函数
- 类中包含虚函数
- 类中带有虚基类
我们今天就发现第一种,后面几种可能会在后面分析吧。
class C
{
public:
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
B b; // 有一个类B的成员,这样就符合合成默认构造函数的条件了
};
刚好利用上了之前的b,好了我们直接反汇编查看:
C c;
00007FF691222200 48 8D 4D 08 lea rcx,[c]
00007FF691222204 E8 61 F2 FF FF call C::C (07FF69122146Ah)
一下子就看到了,我们明显没有写C的构造函数,这里就调用了,说明编译器给我们自动生成了一个默认构造函数,那我们继续看看这个生成的默认构造函数长啥样。
00007FF691222170 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
00007FF691222175 55 push rbp
00007FF691222176 57 push rdi
00007FF691222177 48 81 EC E8 00 00 00 sub rsp,0E8h // windows汇编,我一直对这个大小都感觉奇怪,linux汇编看着就很顺眼
00007FF69122217E 48 8D 6C 24 20 lea rbp,[rsp+20h]
// 这个this就不用再分析了吧,其实就是传进来的rcx
00007FF691222183 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
// 直接偏移8,就是到了类b的位置
00007FF69122218A 48 83 C0 08 add rax,8
// 然后把类B直接赋值给rcx,这就是this指针,哈哈哈
00007FF69122218E 48 8B C8 mov rcx,rax // 把b的地址赋值给rcx,作为类B构造函数的的this指针
00007FF691222191 E8 D9 F2 FF FF call B::B (07FF69122146Fh)
00007FF691222196 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF69122219D 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
00007FF6912221A4 5F pop rdi
00007FF6912221A5 5D pop rbp
这个看着并没有做初始化动作,其实是我们没有分析带()的版本,不带(),编译器是不会给类的变量初始化的。
其他的就不介绍了,可以自行分析。
6.2.1.3 如果自己有构造函数
如果类C我自己有自己的构造函数呢?这样编译器会怎么处理。
class C
{
public:
C()
{
m_a = 0;
m_b = 0;
}
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
B b; // 有一个类B的成员,这样就符合合成默认构造函数的条件了
};
我们反汇编看看:
C()
00007FF631A52370 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
00007FF631A52375 55 push rbp
00007FF631A52376 57 push rdi
00007FF631A52377 48 81 EC E8 00 00 00 sub rsp,0E8h
00007FF631A5237E 48 8D 6C 24 20 lea rbp,[rsp+20h]
00007FF631A52383 48 8D 0D DD 1C 01 00 lea rcx,[__339112C5_07@1 深入分析类和对象@cpp (07FF631A64067h)]
00007FF631A5238A E8 63 F0 FF FF call __CheckForDebuggerJustMyCode (07FF631A513F2h)
// 编译器会在构造函数调用之前,插入代码,就是插入了调用B构造函数的代码
00007FF631A5238F 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
// 还是继续偏移8字节,指向b
00007FF631A52396 48 83 C0 08 add rax,8
00007FF631A5239A 48 8B C8 mov rcx,rax
// 然后开始构造,跟编译器生成的一模一样
00007FF631A5239D E8 CD F0 FF FF call B::B (07FF631A5146Fh)
{
m_a = 0;
// 后面就是编译器取到this指针,开始初始化,this指针其实就是传进来的对象
00007FF631A523A2 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF631A523A9 C7 00 00 00 00 00 mov dword ptr [rax],0
m_b = 0;
00007FF631A523AF 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF631A523B6 C7 40 04 00 00 00 00 mov dword ptr [rax+4],0
}
00007FF631A523BD 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF631A523C4 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
00007FF631A523CB 5F pop rdi
00007FF631A523CC 5D pop rbp
00007FF631A523CD C3 ret
6.2.2 拷贝构造函数
分析过了默认构造函数,现在我们继续来看看拷贝构造函数,盲猜一下,这个跟默认构造函数差不多,哈哈哈。(我猜肯定不是盲猜,我是分析过了的,哈哈哈)
6.2.2.1 不写拷贝构造函数
我们如果不定义自己的拷贝构造函数,编译器会帮我们生成么?
C c2(1, 2);
C c3(c2); // 这种就是调用拷贝构造函数
c3.show();
没写拷贝构造函数,我们打印了c3的m_a和m_b,发现是有值的,说明编译器帮我们拷贝。
我们就来分析一下编译器是怎么拷贝的?
C c3(c2); // 这种就是调用拷贝构造函数
00007FF6E4732164 48 8B 45 08 mov rax,qword ptr [c2]
00007FF6E4732168 48 89 45 28 mov qword ptr [c3],rax
// 不看不知道,一看吓一跳,编译器竟然就用了mov qword来拷贝构造
qword就是操作八个字节的移动,所以我把类C改大,我们在来反汇编看看:
class C
{
public:
C()
{
m_a = 0;
m_b = 0;
}
C(int a, int b) : m_a(a), m_b(b)
{}
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
int m_c;
int m_d;
//B b; // 有一个类B的成员,这样就符合合成默认构造函数的条件了
};
反汇编:
C c3(c2);
00007FF626382165 48 8D 45 38 lea rax,[c3]
00007FF626382169 48 8D 4D 08 lea rcx,[c2]
00007FF62638216D 48 8B F8 mov rdi,rax
00007FF626382170 48 8B F1 mov rsi,rcx
00007FF626382173 B9 10 00 00 00 mov ecx,10h // 16字节
00007FF626382178 F3 A4 rep movs byte ptr [rdi],byte ptr [rsi]
又看到了我们熟悉的汇编了,rep重复操作,movs,这个好像是新的,我们来百度看看:
movs是汇编语言中的一种指令,它用于将数据从一个存储器位置移动到另一个存储器位置。具体来说,movs指令可以实现从源地址复制指定数量的字节(或者单词)到目标地址。
就是循环把rsi(c2)寄存器指向的值,赋值到rdi(c3)中。
6.2.2.2 啥时候编译器会生成拷贝构造函数
这个也是老生常谈的问题了(其实就是第二次,后面好像还有几次,尴尬)。
这个其实跟默认构造函数的条件是一样的。
- 包含类成员B,且B有默认构造函数
- 继承类B,且B有默认构造函数
- 类中包含虚函数
- 类中带有虚基类
我们这个也是只分析第一种:
00007FF6BF276780 48 89 54 24 10 mov qword ptr [rsp+10h],rdx
00007FF6BF276785 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
00007FF6BF27678A 55 push rbp
00007FF6BF27678B 57 push rdi
00007FF6BF27678C 48 81 EC E8 00 00 00 sub rsp,0E8h
00007FF6BF276793 48 8D 6C 24 20 lea rbp,[rsp+20h]
00007FF6BF276798 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6BF27679F 48 8B 8D E8 00 00 00 mov rcx,qword ptr [__that]
00007FF6BF2767A6 8B 09 mov ecx,dword ptr [rcx]
// 先拷贝偏移为0的4字节
00007FF6BF2767A8 89 08 mov dword ptr [rax],ecx
00007FF6BF2767AA 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6BF2767B1 48 8B 8D E8 00 00 00 mov rcx,qword ptr [__that]
00007FF6BF2767B8 8B 49 04 mov ecx,dword ptr [rcx+4]
// 再拷贝偏移为4的4字节
00007FF6BF2767BB 89 48 04 mov dword ptr [rax+4],ecx
00007FF6BF2767BE 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6BF2767C5 48 8B 8D E8 00 00 00 mov rcx,qword ptr [__that]
// 然后在拷贝偏移为8的4字节
00007FF6BF2767CC 8B 49 08 mov ecx,dword ptr [rcx+8]
00007FF6BF2767CF 89 48 08 mov dword ptr [rax+8],ecx
00007FF6BF2767D2 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6BF2767D9 48 8B 8D E8 00 00 00 mov rcx,qword ptr [__that]
00007FF6BF2767E0 8B 49 0C mov ecx,dword ptr [rcx+0Ch]
// 最后拷贝偏移为12的4字节
00007FF6BF2767E3 89 48 0C mov dword ptr [rax+0Ch],ecx
00007FF6BF2767E6 48 8B 85 E8 00 00 00 mov rax,qword ptr [__that]
00007FF6BF2767ED 48 83 C0 10 add rax,10h
00007FF6BF2767F1 48 8B 8D E0 00 00 00 mov rcx,qword ptr [this]
00007FF6BF2767F8 48 83 C1 10 add rcx,10h
00007FF6BF2767FC 48 8B D0 mov rdx,rax
00007FF6BF2767FF E8 84 AC FF FF call B::B (07FF6BF271488h) // 这样再调用B
00007FF6BF276804 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6BF27680B 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
00007FF6BF276812 5F pop rdi
00007FF6BF276813 5D pop rbp
不知道为啥编译器自己生成的拷贝构造函数,需要单个变量进行赋值,为啥不用rep movs这样赋值,是不是需要调用B的构造函数?
6.2.2.3 如果有自己的拷贝构造函数
如果我们自己定义了拷贝构造函数,按照猜测,编译器也会在我们写拷贝构造函数中插入代码。
C(const C& c1)
00007FF6C88B21F0 48 89 54 24 10 mov qword ptr [rsp+10h],rdx
00007FF6C88B21F5 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
00007FF6C88B21FA 55 push rbp
00007FF6C88B21FB 57 push rdi
00007FF6C88B21FC 48 81 EC E8 00 00 00 sub rsp,0E8h
00007FF6C88B2203 48 8D 6C 24 20 lea rbp,[rsp+20h]
00007FF6C88B2208 48 8D 0D 58 1E 01 00 lea rcx,[__339112C5_07@1 深入分析类和对象@cpp (07FF6C88C4067h)]
00007FF6C88B220F E8 DE F1 FF FF call __CheckForDebuggerJustMyCode (07FF6C88B13F2h)
// 每次都是从这里开始
00007FF6C88B2214 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6C88B221B 48 83 C0 10 add rax,10h // 偏移16字节 找到b
00007FF6C88B221F 48 8B C8 mov rcx,rax
00007FF6C88B2222 E8 66 F2 FF FF call B::B (07FF6C88B148Dh) // 调用B构造函数
{
m_a = c1.m_a;
00007FF6C88B2227 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6C88B222E 48 8B 8D E8 00 00 00 mov rcx,qword ptr [c1]
00007FF6C88B2235 8B 09 mov ecx,dword ptr [rcx]
00007FF6C88B2237 89 08 mov dword ptr [rax],ecx
// 这里我偷懒只写了一个变量的赋值,如果都写的话,这里的代码也是一大推
}
00007FF6C88B2239 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6C88B2240 48 8D A5 C8 00 00 00 lea rsp,[rbp+0C8h]
00007FF6C88B2247 5F pop rdi
00007FF6C88B2248 5D pop rbp
6.2.3 移动构造函数
《深入探索c++对象模式》这本书出的比较早,没有c++11的语法,我们这个移动构造函数是c++11提出的,这个需要我们自己探索一波,如果有问题,可以在评论区指出。
6.2.3.1 不写移动构造函数
如果不写移动构造函数,然后使用了std::move程序会不会有bug?
C c2(1, 2);
C c3(c2); // 这种就是调用拷贝构造函数
c3.show();
// 移动构造函数
C c4(std::move(c2));
c4.show();
printf("c2 = %p %p\n", &c2, &c4);
c2.show(); // 看着没有被移除
这个看着c2是没有被移除的,所以我们看汇编才能看到效果了。
// 移动构造函数
C c4(std::move(c2));
00007FF7B49425FA 48 8D 4D 08 lea rcx,[c2]
00007FF7B49425FE E8 8F EE FF FF call std::move<C & __ptr64> (07FF7B4941492h)
00007FF7B4942603 48 8B D0 mov rdx,rax
00007FF7B4942606 48 8D 4D 68 lea rcx,[c4]
00007FF7B494260A E8 74 EE FF FF call C::C (07FF7B4941483h)
这个看着就是调用拷贝构造函数,看来c++11为了兼容以前的,如果没写移动构造就会调用拷贝构造。
那如果我没拷贝构造?会不会也直接用指令来拷贝?
// 移动构造函数
C c4(std::move(c2));
00007FF735112073 48 8D 4D 08 lea rcx,[c2]
00007FF735112077 E8 16 F4 FF FF call std::move<C & __ptr64> (07FF735111492h)
00007FF73511207C 48 8D 4D 68 lea rcx,[c4]
00007FF735112080 48 8B F9 mov rdi,rcx
00007FF735112083 48 8B F0 mov rsi,rax
00007FF735112086 B9 10 00 00 00 mov ecx,10h
00007FF73511208B F3 A4 rep movs byte ptr [rdi],byte ptr [rsi]
果然,还是熟悉的味道,就是使用这个指令来赋值。
6.2.3.2 啥时候编译器会生成移动拷贝构造函数
这个 c++对象模型中并没有讲,只能自己摸索,也只是试一种,其他的可能后面会介绍。
class B
{
public:
B()
{
cout << "B 构造函数" << endl;
}
B(int i) : m_a(i), m_b(m_a) // 这样子写代码有没有问题?
{
cout << "B i 构造函数" << endl;
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
B(B& b)
{
cout << "B 移动拷贝构造函数" << endl;
}
B(B&& b) // B写了移动构造也没调用
{
m_a = b.m_a;
m_b = b.m_b;
}
int m_b;
int m_a; // 这两个怎么初始化
};
class C
{
public:
C()
{
m_a = 0;
m_b = 0;
}
C(int a, int b) : m_a(a), m_b(b)
{}
C(const C& c1)
{
cout << "c 拷贝构造" << endl;
m_a = c1.m_a;
m_b = c1.m_b;
}
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
int m_c;
int m_d;
B b; // 有一个类B的成员,这样就符合合成默认构造函数的条件了
};
int main()
{
std::cout << "Hello World!\n";
C c2(1, 2);
C c3(c2);
c3.show();
// 移动构造函数
C c4(std::move(c2)); // c还是调用拷贝构造函数,b调用的是构造函数,离谱哈哈哈哈
c4.show();
printf("c2 = %p %p\n", &c2, &c4);
c2.show(); // 看着没有被移除
}
看来c++11为了兼容,是不会自己合成移动构造函数了,如果以后发现问题,再回来修改。
C(C&& c) : b(std::move(c.b)) // 只能手动调用
{
cout << "c 移动拷贝构造" << endl;
m_a = c.m_a;
m_b = c.m_b;
}
只有主动调用移动构造函数,才会调用。
6.2.4 深浅拷贝
深浅拷贝的概念,一般出现在类成员存在指针的时候。
我们在看拷贝构造函数的时候,编译器其实就在做内存复制,所以如果类成员有指针的话,如果还是使用这种内存复制(也就是浅拷贝)的时候,指针的地址也会一样,这样如果要释放指针,就是导致两次释放,所以需要深拷贝。
class C
{
public:
C()
{
m_a = 0;
m_b = 0;
m_p = new int(1);
}
C(int a, int b) : m_a(a), m_b(b)
{}
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << " m_p:" << m_p << endl;
}
private:
int m_a;
int m_b;
int m_c;
int m_d;
//B b; // 有一个类B的成员,这样就符合合成默认构造函数的条件了
int* m_p;
};
int main()
{
std::cout << "Hello World!\n";
C c5;
c5.show();
C c6(c5);
c6.show();
}
打印输出:
Hello World!
m_a: 0 m_b:0 m_p:00000237947B1300
m_a: 0 m_b:0 m_p:00000237947B1300
两个一模一样,这样两个指针都是指向同一个内存,这种明显是不行的。
C(const C& c1)
{
cout << "c 拷贝构造" << endl;
m_a = c1.m_a;
m_b = c1.m_b;
m_p = new int(*c1.m_p);
}
是需要这样写,我这个例子只是一个int型,就可以这么操作,如果是一个数组的话,就需要申请内存后,然后再进行拷贝,这就是深拷贝。
6.3 初始化列表执行的时机
我们在写构造函数的时候,是不是都会把初始化类变量写成构造函数():的后面,这个位置是初始化列表。
有没有听老师讲过,为了装逼,所有的类成员变量都在初始化列表中初始化,然后老师又说在初始化列表中初始化,会提升性能。
虽然我们不能怀疑老师,(毕竟高校老师很牛逼),但是为了研究的目的,我们就来一探初始化列表的真面目。
6.3.1 普通成员变量
我们先来看看普通变量。
class A
{
public:
A(int i) : m_i(i) // 把这个打开,然后我们来反汇编查看
{
// m_i = i; // 我们来把这个屏蔽掉
}
int m_i;
};
要想分析背后的秘密,还是看汇编。
class A
{
public:
A(int i) : m_i(i) // 把这个打开,然后我们来反汇编查看
00007FF6EDC31DA3 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF6EDC31DAA 8B 8D E8 00 00 00 mov ecx,dword ptr [i]
00007FF6EDC31DB0 89 08 mov dword ptr [rax],ecx
// m_i = i; // 我们来把这个屏蔽掉
}
这是不是很熟悉,这不就是上面刚在构造函数中初始化是一样的道理么,事实上确实是这么一回事,如果普通变量,确实性能差不多,不过写在初始化列表中,纯粹是为了装逼。
6.3.2 类变量
那初始化列表,有没有真的提升性能呢?还真有,如果是类变量的话,就有。
// 我们再写一个类
class B
{
public:
B()
{
cout << "B 构造函数" << endl;
}
B(int i)
{
cout << "B i 构造函数" << endl;
}
B(const B& b)
{
cout << "B 拷贝构造函数" << endl;
}
};
class A
{
public:
A(int i) : m_i(i)//, b(10) // 把这个打开,然后我们来反汇编查看
{
// m_i = i; // 我们来把这个屏蔽掉
b = 10;
}
int m_i;
B b;
};
这样子我们加入一个类B,然后在构造函数中给b赋值,我们直接执行看看:
Hello World!
B 构造函数
B i 构造函数
搞了两次,一个就是编译器默认加的构造代码,一次就是我们自己写的赋值。
class A
{
public:
A(int i) : m_i(i)//, b(10) // 把这个打开,然后我们来反汇编查看
00007FF6297A5C93 48 8B 85 00 01 00 00 mov rax,qword ptr [this]
00007FF6297A5C9A 8B 8D 08 01 00 00 mov ecx,dword ptr [i]
00007FF6297A5CA0 89 08 mov dword ptr [rax],ecx
{
00007FF6297A5CA2 48 8B 85 00 01 00 00 mov rax,qword ptr [this]
00007FF6297A5CA9 48 83 C0 04 add rax,4
00007FF6297A5CAD 48 8B C8 mov rcx,rax
00007FF6297A5CB0 E8 9C B7 FF FF call B::B (07FF6297A1451h)
// m_i = i; // 我们来把这个屏蔽掉
b = 10;
00007FF6297A5CB5 BA 0A 00 00 00 mov edx,0Ah
00007FF6297A5CBA 48 8D 8D C4 00 00 00 lea rcx,[rbp+0C4h]
00007FF6297A5CC1 E8 95 B7 FF FF call B::B (07FF6297A145Bh)
00007FF6297A5CC6 48 8B 85 00 01 00 00 mov rax,qword ptr [this]
00007FF6297A5CCD 8B 8D C4 00 00 00 mov ecx,dword ptr [rbp+0C4h]
00007FF6297A5CD3 89 48 04 mov dword ptr [rax+4],ecx
//b.m_b = 10;
}
我们反汇编查看,是不是B的构造函数被调用了两次。这就是性能的问题了,如果这时候写在初始化列表中,就会少了一次调用。
6.3.3 必须在初始化列表中初始化
但是有一些变量就必须在初始化列表中初始化。
- 成员是引用
- 成员是const常量
- 父类中只有有参构造函数(如果我们没写构造函数,编译器会自动插入一个无参构造)
- 成员是类对象且构造函数有参
这个就不展开讲了,就是这几种情况,一定要在初始化列表中初始化。
6.3.4 初始化顺序问题
我们先来看看代码:
class B
{
public:
B()
{
cout << "B 构造函数" << endl;
}
B(int i) : m_a(i), m_b(m_a) // 这样子写代码有没有问题?
{
cout << "B i 构造函数" << endl;
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
B(const B& b)
{
cout << "B 拷贝构造函数" << endl;
}
int m_b;
int m_a; // 这两个怎么初始化
};
初始化成员列表中,先初始化m_a,然后再用m_a初始化m_b。
我们直接打印两个值:
B 构造函数
B i 构造函数
m_a: 10 m_b:-764006336
是不是有问题,我们来反汇编查看:
B(int i) : m_a(i), m_b(m_a) // 这样子写代码有没有问题?
// 把this指针存入到rax中
00007FF615271F23 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
// 把this指针存入到rcx中
00007FF615271F2A 48 8B 8D E0 00 00 00 mov rcx,qword ptr [this]
// 再把this指针偏移4字节后的地址,给ecx,就是m_a
00007FF615271F31 8B 49 04 mov ecx,dword ptr [rcx+4]
// 这里就是 使用ecx(m_a) 直接赋值给rax(m_b) 所以就导致m_b的值不可预料
00007FF615271F34 89 08 mov dword ptr [rax],ecx
00007FF615271F36 48 8B 85 E0 00 00 00 mov rax,qword ptr [this]
00007FF615271F3D 8B 8D E8 00 00 00 mov ecx,dword ptr [i]
// 这样再把i的值赋值给m_a
00007FF615271F43 89 48 04 mov dword ptr [rax+4],ecx
所以成员列表中初始化是看在类中定义的顺序,不能随便用成员变量赋值给另一个成员变量。
6.4 类成员布局
上面介绍了类构造函数,接着我们再来看看类成员变量是怎么存的,类成员函数又是怎么调用的。
6.4.1 没有成员的类大小
如果我们定义一个空的类,它的大小是多少?这个一般实际上是没有这个问题,都是面试问的。
class D
{
};
int main()
{
std::cout << "Hello World!\n";
D d;
D d1;
cout << " sizeof(D):" << sizeof(D) << " sizeof(d):" << sizeof(d) << endl;
}
我们来输出打印:
Hello World!
sizeof(D):1 sizeof(d):1
大小是1,原因呢?其实是为了占空间,所以空类的大小是1字节,就是占位。
6.4.2 类成员布局
接下来我们来看看类成员布局,类有两种成员,一种是普通成员变量,一种是静态成员变量。
6.4.2.1 普通成员变量
我们先来看看普通成员变量。
class D
{
public:
int m_a;
int m_b;
};
int main()
{
std::cout << "Hello World!\n";
D d;
cout << " sizeof(D):" << sizeof(D) << " sizeof(d):" << sizeof(d) << endl;
cout << " &d:" << &d << " &d.m_a:" << &d.m_a << " &d.m_b:" << &d.m_b << endl;
}
打印输出:
Hello World!
sizeof(D):8 sizeof(d):8
&d:000000B20132F748 &d.m_a:000000B20132F748 &d.m_b:000000B20132F74C
看到打印结果,我们也明白了,类的大小就是成员变量的大小,并且布局,成员变量是在this指针开头的。(至少目前是这样的,哈哈哈)。
6.4.2.2 静态成员变量
静态成员变量是类公有,那应该不存在类对象中了吧?我们来看看。
class D
{
public:
int m_a;
int m_b;
static int m_c;
};
// 静态变量需要初始化
int D::m_c = 0;
int main()
{
std::cout << "Hello World!\n";
D d;
cout << " sizeof(D):" << sizeof(D) << " sizeof(d):" << sizeof(d) << endl;
cout << " &d:" << &d << " &d.m_a:" << &d.m_a << " &d.m_b:" << &d.m_b << endl;
cout << " &m_c:" << &d.m_c << endl;
}
打印结果:
Hello World!
sizeof(D):8 sizeof(d):8
&d:0000004FA0F1F798 &d.m_a:0000004FA0F1F798 &d.m_b:0000004FA0F1F79C
&m_c:00007FF7C86DD170
静态变量没有存在类对象中,类对象大小还是8,并且静态成员每次的值都是一样的。(不是很了解windows内存分区,如果是linux 这个m_c是存在数据段的,也就是全局数据区)
6.4.3 类函数布局
6.4.3.1 普通成员函数布局
在我没学对象模型之前,一直以为类函数是跟对象绑定的,可能是受c写面向对象的影响,但是学了之后,才发现自己错的很离谱。
class D
{
public:
int m_a;
int m_b;
static int m_c;
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
void show2()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
};
// 静态变量需要初始化
int D::m_c = 0;
int main()
{
std::cout << "Hello World!\n";
D d;
cout << " sizeof(D):" << sizeof(D) << " sizeof(d):" << sizeof(d) << endl;
cout << " &d:" << &d << " &d.m_a:" << &d.m_a << " &d.m_b:" << &d.m_b << endl;
cout << " &m_c:" << &d.m_c << endl;
cout << " &D::show:" << &(D::show) << " &D::show2:" << &(D::show2) << endl; // 这样打印不对
printf("show %p show2 %p\n", &(D::show), &(D::show2));
}
我们看看打印结果:
show 00007FF6579D1456 show2 00007FF6579D1460
多次执行这个数据都不会改变,是因为类成员函数也是存储在代码段中,如果使用类对象调用,编译器会找到这个函数的,其实跟普通函数的性能是一样的。
d.show();
00007FF7B6A076E0 48 8D 4D 08 lea rcx,[d]
00007FF7B6A076E4 E8 76 99 FF FF call D::show (07FF7B6A0105Fh)
d.show2();
00007FF7B6A076E9 48 8D 4D 08 lea rcx,[d]
00007FF7B6A076ED E8 8C 9D FF FF call D::show2 (07FF7B6A0147Eh)
其实我们通过反汇编也能看出,调用类成员函数跟直接调用函数性能是一样的。
6.4.3.2 静态成员函数布局
静态函数,是类共有的,那肯定是只有一份的。
class D
{
public:
int m_a;
int m_b;
static int m_c;
void show()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
void show2()
{
cout << "m_a: " << m_a << " m_b:" << m_b << endl;
}
static void show3()
{
cout << "hha" << endl;
}
};
// 静态变量需要初始化
int D::m_c = 0;
int main()
{
std::cout << "Hello World!\n";
D d;
cout << " sizeof(D):" << sizeof(D) << " sizeof(d):" << sizeof(d) << endl;
cout << " &d:" << &d << " &d.m_a:" << &d.m_a << " &d.m_b:" << &d.m_b << endl;
cout << " &m_c:" << &d.m_c << endl;
cout << " &D::show:" << &(D::show) << " &D::show2:" << &(D::show2) << endl; // 这样打印不对
printf("show %p show2 %p\n", &(D::show), &(D::show2));
printf("show3 %p\n", &(D::show3));
}
打印:
show3 07FF7C1DF110E
// 在看看汇编
d.show3();
00007FF7C1DF76F2 E8 17 9A FF FF call D::show3 (07FF7C1DF110Eh)
看看汇编也是这样。
6.5 附加:this指针是啥
6.5.1 this指针是啥
其实this指针就是指向当前对象的地址。
算了好像就只有这句话,憋不出来,后面如果有新的,在加。
6.5.2 汇编中的this指针
通过我们上面分析,this指针其实都是编译器默认生成的函数的第一个参数,我们在代码中不需要写,都是编译器做了,所以this指针的位置,都是在调用构造函数的函数里生成的,比如我们今天介绍的都在栈里,那如果new出来的对象,this指针存在哪?大家可以自行分析。
6.5.3 跟c的面向对象区别
因为c语言并没有编译器帮我们把this指针变成第一个参数传入,所以需要我们自己写的时候,明确把this指针作为第一个参数传入,从汇编的角度也看,两者其实是一样的。