六、c++学习(加餐2:深入分析类和对象(上))

上上篇我们介绍了类,上一篇介绍了汇编,有了汇编的知识,我们这次就来深入分析类和对象。看看类是怎么构造的,对象又是什么?

C++学习,b站直播视频

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是两条指令,下面有指令的详解。

汇编中 rep指令 和 stos指令ollydbg图解

这句汇编的意思就是,把this指针指向的ecx(8字节)空间清0,这也是我们刚刚看到的效果,默认初始化为0了。

通过上面的分析我们就感受到,编译器在这种情况下,是不会自动生成一个默认构造函数的,只是把类变量的值清0了。

6.2.1.2 啥时候编译器会生成构造函数

通过上面的分析,我们发现,编译器并不会给我们生成构造函数,这就跟我们老师讲的不一样了,老师说编译器会自动生成构造函数的,那啥情况下会生成默认构造函数呢?

其实这里可以去看《深入分析c++对象模型》,有下面四种情况:

  1. 包含类成员B,且B有默认构造函数
  2. 继承类B,且B有默认构造函数
  3. 类中包含虚函数
  4. 类中带有虚基类

我们今天就发现第一种,后面几种可能会在后面分析吧。

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 啥时候编译器会生成拷贝构造函数

这个也是老生常谈的问题了(其实就是第二次,后面好像还有几次,尴尬)。

这个其实跟默认构造函数的条件是一样的。

  1. 包含类成员B,且B有默认构造函数
  2. 继承类B,且B有默认构造函数
  3. 类中包含虚函数
  4. 类中带有虚基类

我们这个也是只分析第一种:

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 必须在初始化列表中初始化

但是有一些变量就必须在初始化列表中初始化。

  1. 成员是引用
  2. 成员是const常量
  3. 父类中只有有参构造函数(如果我们没写构造函数,编译器会自动插入一个无参构造)
  4. 成员是类对象且构造函数有参

这个就不展开讲了,就是这几种情况,一定要在初始化列表中初始化。

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指针作为第一个参数传入,从汇编的角度也看,两者其实是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值