文章目录

  • 7.1 对象操作
  • 7.1.1 对象拷贝
  • 7.1.1.1 无拷贝赋值运算符
  • 7.1.1.2 测试编译器会不会自动生成
  • 7.1.1.3 自己写拷贝赋值运算符
  • 7.1.2 对象移动
  • 7.1.2.1 无移动赋值运算符
  • 7.1.2.2 测试会不会自动生成
  • 7.1.2.3 写了移动赋值运算符
  • 7.2 析构函数
  • 7.2.1 主动写析构函数
  • 7.2.2 不写析构函数
  • 7.2.3 析构函数被合成
  • 7.2.4 析构函数被扩展
  • 7.3 运行时期的对象
  • 7.3.1 局部对象
  • 7.3.1.1 析构插入的位置
  • 7.3.1.2 注意对象定义位置
  • 7.3.1.3 对象成员的初值
  • 7.3.2 全局对象
  • 7.3.2.1 构造函数调用的位置
  • 7.3.2.2 生成的构造函数是被谁调用的
  • 7.3.2.3 析构函数调用位置
  • 7.2.3.4 对象成员的初值
  • 7.3.3 静态局部对象
  • 7.3.3.1 静态局部变量生成的时机
  • 7.3.3.2 构造函数调用
  • 7.3.3.4 析构函数调用
  • 7.3.3.5 对象成员的初值
  • 7.3.4 对象数组
  • 7.4 临时对象
  • 7.4.1 拷贝构造相关临时性对象
  • 7.4.2 等号赋值相关临时性对象
  • 7.4.3 直接运算产生的对象
  • 7.5 总结


7.1 对象操作

我们在学类的时候就发现对象拷贝和拷贝构造函数很像,对象移动和移动构造函数很像,要说差别的话,就是对象拷贝和对象移动要先处理原来的对象内容,因为是先构造了对象,然后才进行对象的操作,所以需要先处理原来对象的内容。

7.1.1 对象拷贝

7.1.1.1 无拷贝赋值运算符

我们先来看看对象拷贝,当我们不重载拷贝赋值运算符的时候:

class A
{
public:
	A(int i) : m_i(i) {}

	int m_i;

};	

    A a2(1);		// 要先构造,然后拷贝
00007FF6296E238E BA 01 00 00 00       mov         edx,1  
00007FF6296E2393 48 8D 4D 24          lea         rcx,[a2]  
00007FF6296E2397 E8 2F EF FF FF       call        A::A (07FF6296E12CBh)  
	a2 = a;			// 这个才是对象拷贝
00007FF6296E239C 8B 45 04             mov         eax,dword ptr [a]  
    // 刚好我的A类大小只有4字节,直接就dword拷贝了,这样跟拷贝构造一样

我们把类A改大一点,来看看情况,我的猜测是跟拷贝构造函数是一样的。

class A
{
public:
	A(int i) : m_i(i) {}

	int m_i;
	int m_j;
	int m_a;
	int m_b;
	int m_c;

};

   A a2(1);		// 要先构造,然后拷贝
00007FF74149238F BA 01 00 00 00       mov         edx,1  
00007FF741492394 48 8D 4D 38          lea         rcx,[a2]  
00007FF741492398 E8 2E EF FF FF       call        A::A (07FF7414912CBh)  
   a2 = a;			// 这个才是对象拷贝
00007FF74149239D 48 8D 45 38          lea         rax,[a2]  
00007FF7414923A1 48 8D 4D 08          lea         rcx,[a]  
00007FF7414923A5 48 8B F8             mov         rdi,rax  
00007FF7414923A8 48 8B F1             mov         rsi,rcx  
00007FF7414923AB B9 14 00 00 00       mov         ecx,14h  
00007FF7414923B0 F3 A4                rep movs    byte ptr [rdi],byte ptr [rsi]

看到这个,是不是像看到老朋友一样了,是的,其实这个拷贝赋值运算符其实跟拷贝构造有点像,应该说功能是一样的,只不过调用的地方不一样。

7.1.1.2 测试编译器会不会自动生成

默认情况下都是使用bitwise copy,下面有几种情况,会生成拷贝赋值运算符。

  1. 包含类成员B,且B有拷贝赋值运算符
  2. 继承类B,且B有拷贝赋值运算符
  3. 类中包含虚函数(如果有虚函数,一定不能拷贝右端vptr地址)[这个后面要详细讲]
  4. 类中带有虚基类

这几个操作跟构造函数也是差不多的,我们这里先用第1种情况分析,其他的看看后面的情况。

class B
{
public:
	int m_bi;
	int m_bj;

	B& operator=(const B& b)
	{
		m_bi = b.m_bi;
		m_bj = b.m_bj;
		return *this;
	}

};

class A
{
public:
	A(int i) : m_i(i) {}

	int m_i;
	int m_j;
	int m_a;
	int m_b;
	int m_c;

	B m_bb;
};

加了一个类B,我们再来看看赋值拷贝运算符。

a2 = a;
00007FF6B32D20DC 48 8D 55 08          lea         rdx,[a]  
00007FF6B32D20E0 48 8D 4D 48          lea         rcx,[a2]  
    // 两个参数 第一个是a,第二个是a2,这取的是地址
00007FF6B32D20E4 E8 77 F3 FF FF       call        A::operator= (07FF6B32D1460h)

看到这个call就开心了。没错,call调用的就是编译器给我们生产的一个拷贝赋值运算符。

06.2  深入分析类和对象(下).exe!A::operator=(const A &):
00007FF6B32D1F30 48 89 54 24 10       mov         qword ptr [rsp+10h],rdx  
00007FF6B32D1F35 48 89 4C 24 08       mov         qword ptr [rsp+8],rcx  
00007FF6B32D1F3A 55                   push        rbp  
00007FF6B32D1F3B 57                   push        rdi  
00007FF6B32D1F3C 48 81 EC E8 00 00 00 sub         rsp,0E8h  
00007FF6B32D1F43 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF6B32D1F48 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1F4F 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [__that]  
00007FF6B32D1F56 8B 09                mov         ecx,dword ptr [rcx]  
00007FF6B32D1F58 89 08                mov         dword ptr [rax],ecx  
00007FF6B32D1F5A 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1F61 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [__that]  
00007FF6B32D1F68 8B 49 04             mov         ecx,dword ptr [rcx+4]  
00007FF6B32D1F6B 89 48 04             mov         dword ptr [rax+4],ecx  
00007FF6B32D1F6E 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1F75 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [__that]  
00007FF6B32D1F7C 8B 49 08             mov         ecx,dword ptr [rcx+8]  
00007FF6B32D1F7F 89 48 08             mov         dword ptr [rax+8],ecx  
00007FF6B32D1F82 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1F89 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [__that]  
00007FF6B32D1F90 8B 49 0C             mov         ecx,dword ptr [rcx+0Ch]  
00007FF6B32D1F93 89 48 0C             mov         dword ptr [rax+0Ch],ecx  
00007FF6B32D1F96 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1F9D 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [__that]  
00007FF6B32D1FA4 8B 49 10             mov         ecx,dword ptr [rcx+10h]  
00007FF6B32D1FA7 89 48 10             mov         dword ptr [rax+10h],ecx  
00007FF6B32D1FAA 48 8B 85 E8 00 00 00 mov         rax,qword ptr [__that]  
00007FF6B32D1FB1 48 83 C0 14          add         rax,14h  
00007FF6B32D1FB5 48 8B 8D E0 00 00 00 mov         rcx,qword ptr [this]  
00007FF6B32D1FBC 48 83 C1 14          add         rcx,14h  
00007FF6B32D1FC0 48 8B D0             mov         rdx,rax  
00007FF6B32D1FC3 E8 8E F4 FF FF       call        B::operator= (07FF6B32D1456h)  
00007FF6B32D1FC8 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF6B32D1FCF 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
00007FF6B32D1FD6 5F                   pop         rdi  
00007FF6B32D1FD7 5D                   pop         rbp  
00007FF6B32D1FD8 C3                   ret

这个其实跟上一节的拷贝构造很像,一个变量一个变量赋值。

7.1.1.3 自己写拷贝赋值运算符

如果自己重载了拷贝赋值运算符,我猜测现象应该跟拷贝构造一样。

class A
{
public:
	A(int i) : m_i(i) {}

	int m_i;
	int m_j;
	int m_a;
	int m_b;
	int m_c;

	A& operator=(const A& a)
	{

	}

	B m_bb;
};

// 我们反汇编看一下
A& operator=(const A& a)
	{
00007FF7CAFA2200 48 89 54 24 10       mov         qword ptr [rsp+10h],rdx  
00007FF7CAFA2205 48 89 4C 24 08       mov         qword ptr [rsp+8],rcx  
00007FF7CAFA220A 55                   push        rbp  
00007FF7CAFA220B 57                   push        rdi  
00007FF7CAFA220C 48 81 EC E8 00 00 00 sub         rsp,0E8h  
00007FF7CAFA2213 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF7CAFA2218 48 8D 0D 48 1E 01 00 lea         rcx,[__9652333E_07@1  深入分析类和对象(下)@cpp (07FF7CAFB4067h)]  
00007FF7CAFA221F E8 C9 F1 FF FF       call        __CheckForDebuggerJustMyCode (07FF7CAFA13EDh)  

		return *this;
00007FF7CAFA2224 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
	}

分析了汇编之后,再加了打印,我发现类A中的类B成员的值,是不会被拷贝的,还是保留原来的值?

这说明如果是重载了拷贝运算符,啥事情就需要自己动手了,自己类中的类成员也是需要自己去手动赋值。

// 所以正常写法应该是这样子的
	A& operator=(const A& a)
	{
		m_i = a.m_i;
		m_bb = a.m_bb;			// 需要自己手动赋值
		return *this;
	}

7.1.2 对象移动

7.1.2.1 无移动赋值运算符

我们先来看看不写移动赋值运算符时,编译器是怎么做的:

a2 = std::move(a);			// 这个才是对象拷贝
00007FF644E022EC 48 8D 4D 04          lea         rcx,[a]  
00007FF644E022F0 E8 48 F1 FF FF       call        std::move<A & __ptr64> (07FF644E0143Dh)  
00007FF644E022F5 8B 00                mov         eax,dword ptr [rax]  
00007FF644E022F7 89 45 24             mov         dword ptr [a2],eax 
    
// 可以把类A大小调大一点
    	a2 = std::move(a);			// 这个才是对象拷贝
00007FF6841823AD 48 8D 4D 08          lea         rcx,[a]  
00007FF6841823B1 E8 87 F0 FF FF       call        std::move<A & __ptr64> (07FF68418143Dh) 
00007FF6841823B6 48 8D 4D 38          lea         rcx,[a2]  
00007FF6841823BA 48 8B F9             mov         rdi,rcx  
00007FF6841823BD 48 8B F0             mov         rsi,rax  
00007FF6841823C0 B9 14 00 00 00       mov         ecx,14h  
00007FF6841823C5 F3 A4                rep movs    byte ptr [rdi],byte ptr [rsi]

这个汇编,其实跟上面的一模一样,如果我们这时候操作对象a,其实还是可以操作的。

7.1.2.2 测试会不会自动生成

class B
{
public:
	int m_bi;
	int m_bj;

	B(int bi) : m_bi(bi)
	{
		std::cout << "B构造函数" << std::endl;
	}

	B(B& b)
	{
		std::cout << "B拷贝构造" << std::endl;
	}

	/*B& operator=(const B& b)
	{
		m_bi = b.m_bi;
		m_bj = b.m_bj;
		std::cout << "B等号运算符" << std::endl;
		return *this;
	}*/

	B& operator=(const B&& b)
	{
		m_bi = b.m_bi;
		m_bj = b.m_bj;
		std::cout << "B移动运算符" << std::endl;
		return *this;
	}

};

class A
{
public:
	A(int i) : m_i(i), m_bb(i) {}

	int m_i;
	int m_j;
	int m_a;
	int m_b;
	int m_c;

	A& operator=(const A& a)
	{
		std::cout << "A的拷贝运算符" << std::endl;
		m_i = a.m_i;
		// m_bb = a.m_bb;
		return *this;
	}

	B m_bb;
};

int main()
{
	std::cout << "Hello World!\n";

	A a(1);

	A a2(2);		// 要先构造,然后拷贝
	a2 = std::move(a);			// 这个才是对象拷贝
	// a2 = a;

	std::cout << "断点 " << std::endl;
}

我们继续来分析,如果类A不重载移动赋值运算符,类B重载了移动赋值运算符,我们来看看有没有生成。

Hello World!
B构造函数
B构造函数
A的拷贝运算符
断点

结论是编译器不会生成,但是如果存在拷贝赋值运算符,类A会调用拷贝赋值运算符,如果类A不存在拷贝赋值运算符,那类A就生成一个重载拷贝赋值运算符。

7.1.2.3 写了移动赋值运算符

不能默认生成,那就只能自己手动写了,手动写的时候,也需要显示调用类B的移动赋值运算符。

A& operator=(const A&& a)
{
    std::cout << "A的移动运算符" << std::endl;
    m_i = a.m_i;
    m_bb = std::move(a.m_bb);
    return *this;
}

7.2 析构函数

我们介绍了那么多的构造函数,现在我们来介绍一下析构函数。

7.2.1 主动写析构函数

我们先来看看写了析构函数之后,编译器是怎么调用析构函数的:

// 析构函数
class C
{
public:

	C()
	{
		m_pi = new int(1);
	}

	~C()
	{
		delete m_pi;
	}

	int* m_pi;
};

int main()
{
	std::cout << "Hello World!\n";

	C c;
	std::cout << "断点 " << std::endl;
}

我们反汇编看看:

int main()
{
00007FF66CC025C0 40 55                push        rbp  
00007FF66CC025C2 57                   push        rdi  
00007FF66CC025C3 48 81 EC 08 01 00 00 sub         rsp,108h  
00007FF66CC025CA 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF66CC025CF 48 8D 7C 24 20       lea         rdi,[rsp+20h]  
00007FF66CC025D4 B9 0A 00 00 00       mov         ecx,0Ah  
00007FF66CC025D9 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00007FF66CC025DE F3 AB                rep stos    dword ptr [rdi]  
00007FF66CC025E0 48 8B 05 21 CA 00 00 mov         rax,qword ptr [__security_cookie (07FF66CC0F008h)]  
00007FF66CC025E7 48 33 C5             xor         rax,rbp  
00007FF66CC025EA 48 89 85 D8 00 00 00 mov         qword ptr [rbp+0D8h],rax  
00007FF66CC025F1 48 8D 0D 6F 2A 01 00 lea         rcx,[__9652333E_07@1  深入分析类和对象(下)@cpp (07FF66CC15067h)]  
00007FF66CC025F8 E8 77 EE FF FF       call        __CheckForDebuggerJustMyCode (07FF66CC01474h)  
// 上面就是入栈操作了,不用解释了吧。

	C c;
00007FF66CC02610 BA 08 00 00 00       mov         edx,8  
00007FF66CC02615 48 8D 4D 08          lea         rcx,[c]  
00007FF66CC02619 E8 1F EE FF FF       call        C::__autoclassinit2 (07FF66CC0143Dh)  
    // 本来想AI一下,觉得发现chatgpt查不出来,百度看了一下,两个结论不一样,干脆自己看汇编把。
00007FF66CC0261E 48 8D 4D 08          lea         rcx,[c]  
00007FF66CC02622 E8 4A EC FF FF       call        C::C (07FF66CC01271h)  
    // 调用构造函数
00007FF66CC02627 90                   nop  

....  // 省略


}
// 这后面是main函数执行完了,执行一些回收工作
00007FF66CC0264C 48 8D 4D 08          lea         rcx,[c]  
00007FF66CC02650 E8 9C EE FF FF       call        C::~C (07FF66CC014F1h)  
    // 这个就是调用了析构函数,因为是栈上的对象,所以需要在函数调用结束后才会调用析构,这个我们在下节分析
00007FF66CC02655 33 C0                xor         eax,eax  
00007FF66CC02657 8B F8                mov         edi,eax  
00007FF66CC02659 48 8D 4D E0          lea         rcx,[rbp-20h]  
00007FF66CC0265D 48 8D 15 1C 96 00 00 lea         rdx,[__xt_z+1E0h (07FF66CC0BC80h)]  
00007FF66CC02664 E8 75 ED FF FF       call        _RTC_CheckStackVars (07FF66CC013DEh)  
00007FF66CC02669 8B C7                mov         eax,edi  
00007FF66CC0266B 48 8B 8D D8 00 00 00 mov         rcx,qword ptr [rbp+0D8h]  
00007FF66CC02672 48 33 CD             xor         rcx,rbp  
00007FF66CC02675 E8 BB EB FF FF       call        __security_check_cookie (07FF66CC01235h)  
00007FF66CC0267A 48 8D A5 E8 00 00 00 lea         rsp,[rbp+0E8h]  
00007FF66CC02681 5F                   pop         rdi  
00007FF66CC02682 5D                   pop         rbp  
00007FF66CC02683 C3                   ret  
    // 后面这一堆就不分析了,看不懂的函数了,也没必要看了。

__autoclassinit2函数汇编分析:

00007FF66CC02410 48 89 54 24 10       mov         qword ptr [rsp+10h],rdx  
00007FF66CC02415 48 89 4C 24 08       mov         qword ptr [rsp+8],rcx  
00007FF66CC0241A 55                   push        rbp  
00007FF66CC0241B 57                   push        rdi  
00007FF66CC0241C 48 81 EC C8 00 00 00 sub         rsp,0C8h  
00007FF66CC02423 48 8B EC             mov         rbp,rsp  
00007FF66CC02426 48 8B BD E0 00 00 00 mov         rdi,qword ptr [this]  
00007FF66CC0242D 33 C0                xor         eax,eax  
00007FF66CC0242F 48 8B 8D E8 00 00 00 mov         rcx,qword ptr [rbp+0E8h]  
00007FF66CC02436 F3 AA                rep stos    byte ptr [rdi]  
   // 这个代码看到这里就不用说了吧,就是做了一个初始化清0操作
00007FF66CC02438 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
00007FF66CC0243F 5F                   pop         rdi  
00007FF66CC02440 5D                   pop         rbp  
00007FF66CC02441 C3                   ret

7.2.2 不写析构函数

上面我们认识了析构函数是咋样的了,那现在我把析构函数屏蔽掉, 然后再反汇编看看效果:

这次就不拷贝代码了,也只拷贝汇编代码退出部分了:

} // 上面代码是main函数的代码,我们这里不需要,就不拷贝出来,我们看main函数结束后的代码
// 是不是发现并没有析构函数
00007FF61E9C65EA 33 C0                xor         eax,eax  
00007FF61E9C65EC 8B F8                mov         edi,eax  
00007FF61E9C65EE 48 8D 4D E0          lea         rcx,[rbp-20h]  
00007FF61E9C65F2 48 8D 15 87 56 00 00 lea         rdx,[__xt_z+1E0h (07FF61E9CBC80h)]  
00007FF61E9C65F9 E8 E0 AD FF FF       call        _RTC_CheckStackVars (07FF61E9C13DEh)  
00007FF61E9C65FE 8B C7                mov         eax,edi  
00007FF61E9C6600 48 8B 8D D8 00 00 00 mov         rcx,qword ptr [rbp+0D8h]  
00007FF61E9C6607 48 33 CD             xor         rcx,rbp  
00007FF61E9C660A E8 26 AC FF FF       call        __security_check_cookie (07FF61E9C1235h)  
00007FF61E9C660F 48 8D A5 E8 00 00 00 lea         rsp,[rbp+0E8h]  
00007FF61E9C6616 5F                   pop         rdi  
00007FF61E9C6617 5D                   pop         rbp  
00007FF61E9C6618 C3                   ret

得出结论,如果我们不写析构函数,编译器也不会给我们生成,如果像我写这个类C一样使用了指针,那就需要自己写一个析构函数,这样才能不内存泄露。

7.2.3 析构函数被合成

既然上面得到的结论是我们自己不写析构函数的时候,析构函数不会被合成。那析构函数是一直不会被合成?还是在特殊情况会合成?

其实析构函数会在下面两种情况中被合成:

  1. 包含类成员B,且B有析构函数
  2. 继承类B,且B有析构函数

是不是很熟悉的两个条件,是的,这种编译器会合成的条件基本都是一样的。

我们现在也是看第一种,涉及到继承的,我们到继承再说。

class B
{
public:
	int m_bi;
	int m_bj;

	B(int bi) : m_bi(bi)
	{
		std::cout << "B构造函数" << std::endl;
	}

	// 析构函数
	~B()
	{

	}

};

// 析构函数
class C
{
public:

	C() : m_bb(1)
	{
		m_pi = new int(1);
	}

	/*~C()
	{
		delete m_pi;
	}*/

	int* m_pi;

	B m_bb;
};

我们反汇编来看看:

}  // 还是拷贝main函数后面的
00007FF7B8086A8C 48 8D 4D 08          lea         rcx,[c]  
00007FF7B8086A90 E8 61 AA FF FF       call        C::~C (07FF7B80814F6h)  
00007FF7B8086A95 33 C0                xor         eax,eax  
00007FF7B8086A97 8B F8                mov         edi,eax  
00007FF7B8086A99 48 8D 4D E0          lea         rcx,[rbp-20h]  
00007FF7B8086A9D 48 8D 15 DC 51 00 00 lea         rdx,[string "Hello World!\n"+70h (07FF7B808BC80h)]  
00007FF7B8086AA4 E8 35 A9 FF FF       call        _RTC_CheckStackVars (07FF7B80813DEh)

这样我们一样就看到了,编译器给我们生成了析构函数。我们看看析构函数做了啥

00007FF7B8081DF0 48 89 4C 24 08       mov         qword ptr [rsp+8],rcx  
00007FF7B8081DF5 55                   push        rbp  
00007FF7B8081DF6 57                   push        rdi  
00007FF7B8081DF7 48 81 EC E8 00 00 00 sub         rsp,0E8h  
00007FF7B8081DFE 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF7B8081E03 48 8B 85 E0 00 00 00 mov         rax,qword ptr [this]  
00007FF7B8081E0A 48 83 C0 08          add         rax,8  
00007FF7B8081E0E 48 8B C8             mov         rcx,rax  
00007FF7B8081E11 E8 E5 F6 FF FF       call        B::~B (07FF7B80814FBh)  
    // 这个看着只是调用了B的析构函数,然后其他的事情,编译器并没有帮我们处理,果然是要靠自己
00007FF7B8081E16 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
00007FF7B8081E1D 5F                   pop         rdi  
00007FF7B8081E1E 5D                   pop         rbp  
00007FF7B8081E1F C3                   ret

7.2.4 析构函数被扩展

上面介绍了,析构函数被合成的情况,那如果我自己实现了析构函数,下面两种情况也会对析构函数进行扩展。

  1. 包含类成员B,且B有析构函数
  2. 继承类B,且B有析构函数

代码就不粘贴了,反正是把类C的析构函数给解开了。

~C()
	{
00007FF732CB2190 48 89 4C 24 08       mov         qword ptr [rsp+8],rcx  
00007FF732CB2195 55                   push        rbp  
00007FF732CB2196 57                   push        rdi  
00007FF732CB2197 48 81 EC 08 01 00 00 sub         rsp,108h  
00007FF732CB219E 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF732CB21A3 48 8D 0D BD 2E 01 00 lea         rcx,[__9652333E_07@1  深入分析类和对象(下)@cpp (07FF732CC5067h)]  
00007FF732CB21AA E8 C5 F2 FF FF       call        __CheckForDebuggerJustMyCode (07FF732CB1474h)  
		delete m_pi;
00007FF732CB21AF 48 8B 85 00 01 00 00 mov         rax,qword ptr [this]  
00007FF732CB21B6 48 8B 00             mov         rax,qword ptr [rax]  
00007FF732CB21B9 48 89 85 C8 00 00 00 mov         qword ptr [rbp+0C8h],rax  
00007FF732CB21C0 BA 04 00 00 00       mov         edx,4  
00007FF732CB21C5 48 8B 8D C8 00 00 00 mov         rcx,qword ptr [rbp+0C8h]  
00007FF732CB21CC E8 FE F1 FF FF       call        operator delete (07FF732CB13CFh)  
    // 这个调用delete函数,这个函数下节课分析
	}
00007FF732CB21D1 48 8B 85 00 01 00 00 mov         rax,qword ptr [this]  
00007FF732CB21D8 48 83 C0 08          add         rax,8  
00007FF732CB21DC 48 8B C8             mov         rcx,rax  
00007FF732CB21DF E8 17 F3 FF FF       call        B::~B (07FF732CB14FBh)  
    // 调用了B的析构函数,看来析构B在析构A的后面
00007FF732CB21E4 48 8D A5 E8 00 00 00 lea         rsp,[rbp+0E8h]  
00007FF732CB21EB 5F                   pop         rdi  
00007FF732CB21EC 5D                   pop         rbp  
00007FF732CB21ED C3                   ret

从这里也看出来了,编译器会给析构函数中,插入析构B的代码,这样就是先析构A,后面在析构B,刚好跟构造函数是相反的,构造函数是先构造B,再构造A。

后面的析构函数遇到了继承的时候,会再详细讲。

7.3 运行时期的对象

介绍了类的构造和析构函数了之后,接下来就重点分析对象了,分析对象在不同阶段,编译器是怎么帮我们调用构造和析构函数的。

7.3.1 局部对象

就按照上面的,我们先分析局部对象

7.3.1.1 析构插入的位置

我们先来一个简单的例子:

// 我们就在这个代码中研究对象
class A
{
public:
	A()				// 之前学过了,不是必要的时候编译器不会帮我们生成构造函数,所以我们需要自己定义
	{

	}

	~A()
	{

	}

	int m_i;
};


int main()
{
	std::cout << "Hello World!\n";

	// 局部对象,我们先来看个简单的
	{
		A a;		// 我们在这个括号的作用域内定义一个对象a,那构造和析构是在哪里被调用的
	}

	std::cout << "断点" << std::endl;
}

我们先猜测,按照作用域,对象a是离开了右括号就会调用析构函数了。

我们反汇编证明一下:

// 局部对象,我们先来看个简单的
	{
		A a;		// 我们在这个括号的作用域内定义一个对象a,那构造和析构是在哪里被调用的
00007FF714F22460 48 8D 4D 04          lea         rcx,[rbp+4]  
00007FF714F22464 E8 16 EF FF FF       call        A::A (07FF714F2137Fh)  
	}
00007FF714F22469 48 8D 4D 04          lea         rcx,[rbp+4]  
00007FF714F2246D E8 5E EE FF FF       call        A::~A (07FF714F212D0h)

很好,我们猜测的很对。

接下来我们再看看使用Switch的:

{
		A a1;
		switch (a1.m_i)
		{
		case -1:
			return -1;

		case 0:
			return 0;

		case 1:
			return 1;

		case 2:
			return 2;
		}

	}

这种写法,那我们猜测,这种写法,肯定每个return都会去析构对象,我们反汇编查看:

{
		A a1;
00007FF79CB06662 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB06666 E8 14 AD FF FF       call        A::A (07FF79CB0137Fh)  
    // 构造函数
		switch (a1.m_i)
00007FF79CB0666B 8B 45 24             mov         eax,dword ptr [rbp+24h]  
00007FF79CB0666E 89 85 74 01 00 00    mov         dword ptr [rbp+174h],eax  
00007FF79CB06674 83 BD 74 01 00 00 FF cmp         dword ptr [rbp+174h],0FFFFFFFFh  
00007FF79CB0667B 74 1D                je          __$EncStackInitStart+8Bh (07FF79CB0669Ah)  
00007FF79CB0667D 83 BD 74 01 00 00 00 cmp         dword ptr [rbp+174h],0  
00007FF79CB06684 74 32                je          __$EncStackInitStart+0A9h (07FF79CB066B8h)  
00007FF79CB06686 83 BD 74 01 00 00 01 cmp         dword ptr [rbp+174h],1  
00007FF79CB0668D 74 44                je          __$EncStackInitStart+0C4h (07FF79CB066D3h)  
00007FF79CB0668F 83 BD 74 01 00 00 02 cmp         dword ptr [rbp+174h],2  
00007FF79CB06696 74 56                je          __$EncStackInitStart+0DFh (07FF79CB066EEh)  
00007FF79CB06698 EB 6F                jmp         __$EncStackInitStart+0FAh (07FF79CB06709h)  
            // 这个一堆不知道在比较啥
		{
		case -1:
			return -1;
00007FF79CB0669A C7 85 04 01 00 00 FF FF FF FF mov         dword ptr [rbp+104h],0FFFFFFFFh  
00007FF79CB066A4 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB066A8 E8 23 AC FF FF       call        A::~A (07FF79CB012D0h)  
    // 第一个return 带了析构
00007FF79CB066AD 8B 85 04 01 00 00    mov         eax,dword ptr [rbp+104h]  
00007FF79CB066B3 E9 83 00 00 00       jmp         __$EncStackInitStart+12Ch (07FF79CB0673Bh)  

		case 0:
			return 0;
00007FF79CB066B8 C7 85 24 01 00 00 00 00 00 00 mov         dword ptr [rbp+124h],0  
00007FF79CB066C2 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB066C6 E8 05 AC FF FF       call        A::~A (07FF79CB012D0h)  
    // 第二个return 带了析构
00007FF79CB066CB 8B 85 24 01 00 00    mov         eax,dword ptr [rbp+124h]  
00007FF79CB066D1 EB 68                jmp         __$EncStackInitStart+12Ch (07FF79CB0673Bh)  

		case 1:
			return 1;
00007FF79CB066D3 C7 85 44 01 00 00 01 00 00 00 mov         dword ptr [rbp+144h],1  
00007FF79CB066DD 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB066E1 E8 EA AB FF FF       call        A::~A (07FF79CB012D0h)  
    // 第三个return 带了析构
00007FF79CB066E6 8B 85 44 01 00 00    mov         eax,dword ptr [rbp+144h]  
00007FF79CB066EC EB 4D                jmp         __$EncStackInitStart+12Ch (07FF79CB0673Bh)  

		case 2:
			return 2;
00007FF79CB066EE C7 85 64 01 00 00 02 00 00 00 mov         dword ptr [rbp+164h],2  
00007FF79CB066F8 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB066FC E8 CF AB FF FF       call        A::~A (07FF79CB012D0h)  
    // 第四个return 带了析构
00007FF79CB06701 8B 85 64 01 00 00    mov         eax,dword ptr [rbp+164h]  
00007FF79CB06707 EB 32                jmp         __$EncStackInitStart+12Ch (07FF79CB0673Bh)  
		}

	}
00007FF79CB06709 48 8D 4D 24          lea         rcx,[rbp+24h]  
00007FF79CB0670D E8 BE AB FF FF       call        A::~A (07FF79CB012D0h)  
    // 最后右括号处理完,也带了析构

所以编译器在我们看不见的地方,偷偷做了很多处理工作,默默的付出。

7.3.1.2 注意对象定义位置

介绍了这么多,是不是感觉没啥用?当然前面都是做铺垫,现在铺垫的差不多了,我们就来正戏了。

// 注意对象定义的位置
// 我们来看这句话的意思
{
    A a3;

    int cache = 0;
    if (cache)
    {
        return 1;		// 这样就直接返回了,虽然我们这个代码比较简单,cache不可能为1的
    }

    a3.m_i = 11;	// 这里才使用a3
}

虽然这个代码比较简单,但我感觉如果是从c++学起的,应该不会这么写代码,如果是写c多年,可能一时改变不过了,因为c语言,需要把变量定义在最前面,就像现在这样。

其实这个代码问题也不大,就是有点消耗性能,我们现在cache不可能等1,但是实际代码中是有可能等1,从这个判断退出,结果对象a3没有使用,就要构造和析构了,白白搞了一圈,我们来看看汇编:

A a3;
00007FF627A66718 48 8D 4D 44          lea         rcx,[rbp+44h]  
00007FF627A6671C E8 5E AC FF FF       call        A::A (07FF627A6137Fh)  
    // 定义了a3的时候,就构造了一次
    int cache = 0;
00007FF627A66721 C7 45 64 00 00 00 00 mov         dword ptr [rbp+64h],0  
		if (cache)
00007FF627A66728 83 7D 64 00          cmp         dword ptr [rbp+64h],0  
00007FF627A6672C 74 1B                je          __$EncStackInitStart+13Ah (07FF627A66749h)  
		{
			return 1;		// 这样就直接返回了,虽然我们这个代码比较简单,cache不可能为1的
00007FF627A6672E C7 85 C4 01 00 00 01 00 00 00 mov         dword ptr [rbp+1C4h],1  
00007FF627A66738 48 8D 4D 44          lea         rcx,[rbp+44h]  
00007FF627A6673C E8 8F AB FF FF       call        A::~A (07FF627A612D0h)  
    // 这里跟我们猜测的一样,需要析构
00007FF627A66741 8B 85 C4 01 00 00    mov         eax,dword ptr [rbp+1C4h]  
00007FF627A66747 EB 39                jmp         __$EncStackInitStart+173h (07FF627A66782h)  
		}

		a3.m_i = 11;	// 这里才使用a3
00007FF627A66749 C7 45 44 0B 00 00 00 mov         dword ptr [rbp+44h],0Bh  
	}
00007FF627A66750 48 8D 4D 44          lea         rcx,[rbp+44h]  
00007FF627A66754 E8 77 AB FF FF       call        A::~A (07FF627A612D0h) 
    // 实际上我们需要析构的只是这里的析构函数

《深入探索c++对象模型》给我们的结论是:一般而言我们会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和摧毁操作。

实际上代码需要这样写:

{
		// A a3;

		int cache = 0;
		if (cache)
		{
			return 1;		// 这样就直接返回了,虽然我们这个代码比较简单,cache不可能为1的
		}

		A a3;			// 需要定义在这里,就不会造成性能浪费
		a3.m_i = 11;	// 这里才使用a3
	}

7.3.1.3 对象成员的初值

这个点是我附近的值,因为后面的全局对象的值,可能会不一样。

我们先看局部对象的值,我们先来猜测:局部对象是在栈上的,栈上的内存值是不知道的,所以值是不会进行初始化的。

A a3;
//a3.m_i = 11;	// 这里才使用a3
std::cout << a3.m_i << std::endl;

// 输出
Hello World!
-858993460		// 没有进行初始化
断点

从这里就看出,并没有初始化了。

7.3.2 全局对象

上面我们介绍了局部对象,接下来我们看看全局对象。

7.3.2.1 构造函数调用的位置

我们先看看下面的代码

A g_a;			// 这里定义一个全局对象

int main()
{
    std::cout << "Hello World!\n";

	// 全局对象
	g_a.m_i = 22;
}

全局对象是定义在函数外的,我们知道普通的全局变量,生命周期是随着程序的结束才结束的。

全局的类对象的生命周期也是一样的,只不过类有点特殊,需要调用构造函数和析构函数。

我们看上面代码,就是如果定义了全局类对象,我们是可以在main函数开始的时候就可以调用了g_a对象,

那这个构造函数是在哪里被调用的呢?还是用反汇编来告诉大家答案

A g_a;			// 这里定义一个全局对象
00007FF6E90618C0 40 55                push        rbp  
00007FF6E90618C2 57                   push        rdi  
00007FF6E90618C3 48 81 EC E8 00 00 00 sub         rsp,0E8h  
00007FF6E90618CA 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF6E90618CF 48 8D 0D 91 47 01 00 lea         rcx,[__DBDEBA3D_07@2  深入分析类和对象(下)@cpp (07FF6E9076067h)]  
00007FF6E90618D6 E8 17 FB FF FF       call        __CheckForDebuggerJustMyCode (07FF6E90613F2h)  
00007FF6E90618DB 48 8D 0D 9E E8 00 00 lea         rcx,[g_a (07FF6E9070180h)]  
00007FF6E90618E2 E8 9D FA FF FF       call        A::A (07FF6E9061384h)  
    // 明显看到了调用了构造函数
00007FF6E90618E7 48 8D 0D D2 94 00 00 lea         rcx,[`dynamic atexit destructor for 'g_a'' (07FF6E906ADC0h)]  
00007FF6E90618EE E8 B1 F8 FF FF       call        atexit (07FF6E90611A4h)  
00007FF6E90618F3 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
00007FF6E90618FA 5F                   pop         rdi  
00007FF6E90618FB 5D                   pop         rbp  
00007FF6E90618FC C3                   ret

我同时在main函数和A g_a;加上断点,结果是先执行g_a代码,然后反汇编看到下面的代码。

这个看到是一个函数,前面有push,后面有ret,很明显是编译器生成了一个函数。

我们先看函数内容,明显就看到这个函数内存调用了构造函数。

最后一个atexit()这个函数熟悉不?学过那本《unix高级编程》的应该熟悉把,这个我们析构函数再介绍。

7.3.2.2 生成的构造函数是被谁调用的

本来是想用dumpbin.exe反汇编分析的,结果看了一下,比较懵逼,所以只能用g++来编译并反汇编了。

00000000004016d3 <_Z41__static_initialization_and_destruction_0ii>:
  4016d3:	55                   	push   %rbp
  4016d4:	48 89 e5             	mov    %rsp,%rbp
  4016d7:	48 83 ec 20          	sub    $0x20,%rsp
  4016db:	89 4d 10             	mov    %ecx,0x10(%rbp)
  4016de:	89 55 18             	mov    %edx,0x18(%rbp)
  4016e1:	83 7d 10 01          	cmpl   $0x1,0x10(%rbp)
  4016e5:	75 39                	jne    401720 <_Z41__static_initialization_and_destruction_0ii+0x4d>
  4016e7:	81 7d 18 ff ff 00 00 	cmpl   $0xffff,0x18(%rbp)
  4016ee:	75 30                	jne    401720 <_Z41__static_initialization_and_destruction_0ii+0x4d>
  4016f0:	48 8d 0d 3d 59 00 00 	lea    0x593d(%rip),%rcx        # 407034 <_ZStL8__ioinit>
  4016f7:	e8 74 00 00 00       	callq  401770 <_ZNSt8ios_base4InitC1Ev>
  4016fc:	48 8d 0d 9a ff ff ff 	lea    -0x66(%rip),%rcx        # 40169d <__tcf_0>
  401703:	e8 08 fe ff ff       	callq  401510 <atexit>
  401708:	48 8d 0d 21 59 00 00 	lea    0x5921(%rip),%rcx        # 407030 <g_a>
  40170f:	e8 2c 17 00 00       	callq  402e40 <_ZN1AC1Ev>
      // 这一句就是调用类A的构造函数
  401714:	48 8d 0d 9d ff ff ff 	lea    -0x63(%rip),%rcx        # 4016b8 <__tcf_1>
  40171b:	e8 f0 fd ff ff       	callq  401510 <atexit>
  401720:	90                   	nop
  401721:	48 83 c4 20          	add    $0x20,%rsp
  401725:	5d                   	pop    %rbp
  401726:	c3                   	retq

这个是g++生成的g_a调用构造函数的代码,明显看到我们这个函数是有函数名:_Z41__static_initialization_and_destruction_0ii。

看着函数名应该是做一些静态初始化工作。

这个函数是被谁调用的?其实是在这里调用的,

0000000000401727 <_GLOBAL__sub_I_g_a>:
  401727:	55                   	push   %rbp
  401728:	48 89 e5             	mov    %rsp,%rbp
  40172b:	48 83 ec 20          	sub    $0x20,%rsp
  40172f:	ba ff ff 00 00       	mov    $0xffff,%edx
  401734:	b9 01 00 00 00       	mov    $0x1,%ecx
  401739:	e8 95 ff ff ff       	callq  4016d3 <_Z41__static_initialization_and_destruction_0ii>
  40173e:	90                   	nop
  40173f:	48 83 c4 20          	add    $0x20,%rsp
  401743:	5d                   	pop    %rbp
  401744:	c3                   	retq   
  401745:	90                   	nop
  401746:	90                   	nop
  401747:	90                   	nop
  401748:	90                   	nop
  401749:	90                   	nop
  40174a:	90                   	nop
  40174b:	90                   	nop
  40174c:	90                   	nop
  40174d:	90                   	nop
  40174e:	90                   	nop
  40174f:	90                   	nop

但是也只能追到这里了,后面讲linux的时候,我们再尝试追一下main函数是怎么调用的。

7.3.2.3 析构函数调用位置

我们在初始化的时候,就看到了atexit()函数调用,这个函数其实就是注册一个函数,在main函数退出之后才调用的。我记得地址是:07FF6E90611A4h。

我们就带着这个疑问来看看:

算了,直接看vs的反汇编是有点难度,我们还是来看g++。

401714:	48 8d 0d 9d ff ff ff 	lea    -0x63(%rip),%rcx        # 4016b8 <__tcf_1>
  40171b:	e8 f0 fd ff ff       	callq  401510 <atexit>

从这个注册的函数,我们就发现是把__tcf_1这个函数注册到退出执行函数了。

我们来看看:

00000000004016b8 <__tcf_1>:
  4016b8:	55                   	push   %rbp
  4016b9:	48 89 e5             	mov    %rsp,%rbp
  4016bc:	48 83 ec 20          	sub    $0x20,%rsp
  4016c0:	48 8d 0d 69 59 00 00 	lea    0x5969(%rip),%rcx        # 407030 <g_a>
  4016c7:	e8 84 17 00 00       	callq  402e50 <_ZN1AD1Ev>
  4016cc:	90                   	nop
  4016cd:	48 83 c4 20          	add    $0x20,%rsp
  4016d1:	5d                   	pop    %rbp
  4016d2:	c3                   	retq

是不是一样就看到了g_a调用析构函数。

那到底是怎么调用__tcf_1,我们后面看看有没有介绍一下。

7.2.3.4 对象成员的初值

全局对象我们知道是放在数据区的,因为我们不给初值,所以编译器默认是给我们初始化为0。

这个就不证明了,有需要的自己证实。

7.3.3 静态局部对象

差点忘记了,c++还有一个很特殊的变量,静态局部变量,这种变量声明周期跟全局变量一样,但作用域只能是一个函数内。

所以我们要研究一波。

7.3.3.1 静态局部变量生成的时机

我们都知道,静态局部变量是在函数内部定义的,如果我们不调用这个函数?会发生什么?

// 我们就在这个代码中研究对象
class A
{
public:
	A()				// 之前学过了,不是必要的时候编译器不会帮我们生成构造函数,所以我们需要自己定义
	{
		std::cout << "构造 this:" << this << std::endl;
	}

	~A()
	{
		std::cout << "析构" << std::endl;
	}

	int m_i;
};

// 静态局部对象
void func()
{
	static A s_a;

}

int main()
{
	std::cout << "Hello World!\n";
}

这个例子是我们不调用这个函数,我们看打印结果:

Hello World!
断点

看到这个打印,就明白了,如果没有调用这个函数,是不会调用类A的构造函数。

int main()
{
	func();    // 反之,这样调用就会调用类A的构造函数
	std::cout << "Hello World!\n";
}

7.3.3.2 构造函数调用

我们都知道静态局部变量,只会调用一次,那编译器是怎么控制只调用一次呢?

包括类A的构造函数,也是只调用一次。

// 静态局部对象
void func()
{
00007FF6AED22490 40 55                push        rbp  
00007FF6AED22492 57                   push        rdi  
00007FF6AED22493 48 81 EC E8 00 00 00 sub         rsp,0E8h  
00007FF6AED2249A 48 8D 6C 24 20       lea         rbp,[rsp+20h]  
00007FF6AED2249F 48 8D 0D C1 2B 01 00 lea         rcx,[__DBDEBA3D_07@2  深入分析类和对象(下)@cpp (07FF6AED35067h)]  
00007FF6AED224A6 E8 83 EF FF FF       call        __CheckForDebuggerJustMyCode (07FF6AED2142Eh)  
	static A s_a;
00007FF6AED224AB B8 04 01 00 00       mov         eax,104h  
00007FF6AED224B0 8B C0                mov         eax,eax  
00007FF6AED224B2 8B 0D 88 CD 00 00    mov         ecx,dword ptr [_tls_index (07FF6AED2F240h)]  
00007FF6AED224B8 65 48 8B 14 25 58 00 00 00 mov         rdx,qword ptr gs:[58h]  
00007FF6AED224C1 48 8B 0C CA          mov         rcx,qword ptr [rdx+rcx*8]  
00007FF6AED224C5 8B 04 08             mov         eax,dword ptr [rax+rcx]  
00007FF6AED224C8 39 05 B6 CC 00 00    cmp         dword ptr [$TSS0 (07FF6AED2F184h)],eax  
00007FF6AED224CE 7E 3A                jle         func+7Ah (07FF6AED2250Ah)  
00007FF6AED224D0 48 8D 0D AD CC 00 00 lea         rcx,[$TSS0 (07FF6AED2F184h)]  
00007FF6AED224D7 E8 02 EF FF FF       call        _Init_thread_header (07FF6AED213DEh)  
00007FF6AED224DC 83 3D A1 CC 00 00 FF cmp         dword ptr [$TSS0 (07FF6AED2F184h)],0FFFFFFFFh  
00007FF6AED224E3 75 25                jne         func+7Ah (07FF6AED2250Ah)  
00007FF6AED224E5 48 8D 0D 94 CC 00 00 lea         rcx,[s_a (07FF6AED2F180h)]  
00007FF6AED224EC E8 B1 EE FF FF       call        A::A (07FF6AED213A2h)  
00007FF6AED224F1 48 8D 0D 08 6B 00 00 lea         rcx,[`func'::`2'::`dynamic atexit destructor for 's_a'' (07FF6AED29000h)]  
00007FF6AED224F8 E8 AC EC FF FF       call        atexit (07FF6AED211A9h)  
00007FF6AED224FD 90                   nop  
00007FF6AED224FE 48 8D 0D 7F CC 00 00 lea         rcx,[$TSS0 (07FF6AED2F184h)]  
00007FF6AED22505 E8 E3 EE FF FF       call        _Init_thread_footer (07FF6AED213EDh)  
	//A s_a2;
}
00007FF6AED2250A 48 8D A5 C8 00 00 00 lea         rsp,[rbp+0C8h]  
00007FF6AED22511 5F                   pop         rdi  
00007FF6AED22512 5D                   pop         rbp  
00007FF6AED22513 C3                   ret

这个代码是有点小复杂,我们先执行完这个函数,然后看看对应的内存在分析:

七、c++学习(加餐3:深入分析类和对象(下))_学习

其实这段汇编代码可以分成三部分:

// 第一部分:
00007FF6AED224AB B8 04 01 00 00       mov         eax,104h  
00007FF6AED224B0 8B C0                mov         eax,eax  
00007FF6AED224B2 8B 0D 88 CD 00 00    mov         ecx,dword ptr [_tls_index (07FF6AED2F240h)]  
00007FF6AED224B8 65 48 8B 14 25 58 00 00 00 mov         rdx,qword ptr gs:[58h]  
00007FF6AED224C1 48 8B 0C CA          mov         rcx,qword ptr [rdx+rcx*8]  
00007FF6AED224C5 8B 04 08             mov         eax,dword ptr [rax+rcx]  
00007FF6AED224C8 39 05 B6 CC 00 00    cmp         dword ptr [$TSS0 (07FF6AED2F184h)],eax
00007FF6AED224CE 7E 3A                jle         func+7Ah (07FF6AED2250Ah)  
// 大家发现没有$TSS0的内存就是g_a内存的后面,其实windos编译器就是使用这个内存来标记这个静态变量有没有被初始化,第一步明显就是判断这个标记,eax的值其实就是等于这个标记的值,但是为啥算这么复杂就不做评论了。
    
    
// 第二步
00007FF6AED224D0 48 8D 0D AD CC 00 00 lea         rcx,[$TSS0 (07FF6AED2F184h)]  
00007FF6AED224D7 E8 02 EF FF FF       call        _Init_thread_header (07FF6AED213DEh)  
00007FF6AED224DC 83 3D A1 CC 00 00 FF cmp         dword ptr [$TSS0 (07FF6AED2F184h)],0FFFFFFFFh  
00007FF6AED224E3 75 25                jne         func+7Ah (07FF6AED2250Ah)  
// 这一步感觉是在防止多线程操作这块内存,上锁的感觉,就是把$TSS0置为0FFFFFFFF,_Init_thread_header这个函数我就不分析了
    
// 第三步
00007FF6AED224E5 48 8D 0D 94 CC 00 00 lea         rcx,[s_a (07FF6AED2F180h)]  
00007FF6AED224EC E8 B1 EE FF FF       call        A::A (07FF6AED213A2h)  
00007FF6AED224F1 48 8D 0D 08 6B 00 00 lea         rcx,[`func'::`2'::`dynamic atexit destructor for 's_a'' (07FF6AED29000h)]  
00007FF6AED224F8 E8 AC EC FF FF       call        atexit (07FF6AED211A9h)  
00007FF6AED224FD 90                   nop  
00007FF6AED224FE 48 8D 0D 7F CC 00 00 lea         rcx,[$TSS0 (07FF6AED2F184h)]  
00007FF6AED22505 E8 E3 EE FF FF       call        _Init_thread_footer (07FF6AED213EDh)  
// 第三步就比较简单了,调用类A的构造函数,并注册析构函数注册到atexit里,最后调用_Init_thread_footer解锁,并把$TSS0这个寄存器的值改变。

如果继续调用这个函数,就会先判断第一步,第一步就直接退出了。

静态局部变量,只会初始化一次的秘密就是这样。

7.3.3.4 析构函数调用

这个就不介绍了,因为都是利用atexit注册的回调,跟上面全局变量是一样的。

7.3.3.5 对象成员的初值

要想知道初值,就需要知道这个静态局部变量编译器放在哪个位置?

没讲内存四区就是麻烦,其实这个静态局部变量也是放在数据区,跟全局变量一样,因为没有显示给值,所以就会放在bss段,然后值全部为0。

7.3.4 对象数组

对象数组就不介绍了,有多少个对象,就调用构造函数几次。

int main()
{
	std::cout << "Hello World!\n";

	A a[10];
}

// 后面是打印
Hello World!
构造 this:00000010B98FF6C8
构造 this:00000010B98FF6CC
构造 this:00000010B98FF6D0
构造 this:00000010B98FF6D4
构造 this:00000010B98FF6D8
构造 this:00000010B98FF6DC
构造 this:00000010B98FF6E0
构造 this:00000010B98FF6E4
构造 this:00000010B98FF6E8
构造 this:00000010B98FF6EC
断点
析构
析构
析构
析构
析构
析构
析构
析构
析构
析构

7.4 临时对象

我们接下来看的是一些我们不知道的时候,会产生一些临时对象。

7.4.1 拷贝构造相关临时性对象

这个好像现在编译器默认就优化了,所以只贴代码了,大家各自理解了。

// 我们就在这个代码中研究对象
class A
{
public:
	A()				// 之前学过了,不是必要的时候编译器不会帮我们生成构造函数,所以我们需要自己定义
	{
		std::cout << "构造 this:" << this << std::endl;
	}

	A(const A& a)
	{
		std::cout << "拷贝构造 this:" << this << std::endl;
	}

	~A()
	{
		std::cout << "析构" << std::endl;
	}

	A operator+(A& a2)
	{
		A temp;
		printf("----------\n");
		return temp;
	}

	int m_i;
};

int main()
{
	std::cout << "Hello World!\n";

	A a1;
	A a2;

	A a3 = a1 + a2;
}

再看看打印:

Hello World!
构造 this:00000081BBF6F654
构造 this:00000081BBF6F674
构造 this:00000081BBF6F694
----------
断点
析构
析构
析构

并没有调用拷贝构造,虽然网上有介绍加上不优化的命令就可以,不过就不用这么麻烦了,反正编译器都处理好了,编译器不可能会退化。

7.4.2 等号赋值相关临时性对象

这个其实跟上面是一样的,只不过这次是赋值。

// 类A中加了这个
	A operator=(const A& a)
	{
		std::cout << "拷贝赋值 this:" << this << std::endl;
		return *this;
	}

// main函数改变:
A a1;
A a2;
A a3;
a3 = a1 + a2;

看看打印结果:

Hello World!
构造 this:0000009F3D54FBC4
构造 this:0000009F3D54FBE4
构造 this:0000009F3D54FC04
构造 this:0000009F3D54FCE4
----------
拷贝赋值 this:0000009F3D54FC04
拷贝构造 this:0000009F3D54FD04
析构
析构
断点
析构
析构
析构

看来编译器对这种情况不优化了,多了一个拷贝构造。

所以从这样看来调用拷贝构造函数比等号赋值会更好。

7.4.3 直接运算产生的对象

除了上面两种外,我们还有可能在运算对象的时候,会产生一个临时对象。

int main()
{
	std::cout << "Hello World!\n";

	A a1;
	A a2;
	/*A a3;
	a3 = a1 + a2;*/

	if ((a1 + a2).m_i > 3 || (a1 + a2).m_i > 5)
	{

	}
}

就是在这种情况下,我们使用了a1+a2,这时候编译器为了算出这个值,是会产生一个临时变量的,可以看看打印:

Hello World!
构造 this:00000098DD0FF7E4
构造 this:00000098DD0FF804
构造 this:00000098DD0FF904
----------
构造 this:00000098DD0FF924
----------
析构
析构
断点
析构
析构

有四个构造函数,并且有两个析构函数在断点之前就释放了,说明又两个生成的临时对象。

我们刚刚也看到了临时对象,如果不使用的话,就会立刻释放掉,所以需要去接受这个临时对象,我们才能使用。

7.5 总结

终于在两节中,讲完了一些简单的对象模型的知识,当然在没有继承的时候,这些知识还是比较简单,我们后面会介绍继承,所以在介绍了继承之后,我们会继续学习对象模式的继承部分。