关于对象的生命周期是怎样的,先来看下面一段代码:
class Test
{
public:
Test(int a = 5, int b = 5) :ma(a), mb(b)
{
cout << "Test(int, int)" << endl;
}
~Test()
{
cout << "~Test()" << endl;
}
Test(const Test &src) :ma(src.ma), mb(src.mb)
{
cout << "Test(const Test&)" << endl;
}
void operator=(const Test &src)
{
ma = src.ma; mb = src.mb; cout << "operator=" << endl;
}
private:
int ma;
int mb;
};
Test t1(10, 10);
int main()
{
Test t2(20, 20);
Test t3 = t2;
static Test t4 = Test(30, 30);
t2 = Test(40, 40);
t2 = (Test)(50, 50);
t2 = 60;
Test *p1 = new Test(70, 70);
Test *p2 = new Test[2];
Test *p3 = &Test(80, 80);
Test &p4 = Test(90, 90);
delete p1;
delete[]p2;
}
Test t5(100, 100);
打印结果如下:
它的打印分析结果如下:
Test(int, int) //构造t1(10,10)
Test(int, int) //构造t5(100,100)
Test(int, int) //构造t2(20,20)
Test(const Test&) //拷贝构造t3
Test(int, int) //构造t4(30,30)(由于是初始化,临时对象被编译器优化)
Test(int, int) //构造临时对象temp1(40,40)
operator= //给t2赋值
~Test() //语句结束,析构临时对象temp1
Test(int, int) //构造临时对象temp2(50,5)(由于是逗号表达式,表达式结果为50,经Test显式强转)
operator= //给t2赋值
~Test() //语句结束,析构临时对象temp2
Test(int, int) //构造临时对象temp3(60,5)(Test隐式强转)
operator= //给t2赋值
~Test() //语句结束,析构临时对象temp3
Test(int, int) //构造p1指向的对象(70,70)
Test(int, int) //构造p2指向的对象0(5,5)
Test(int, int) //构造p2指向的对象1(5,5)
Test(int, int) //构造p3指向的临时对象temp4(80,80)
~Test() //语句结束,析构临时对象temp4
Test(int, int) //构造对象p4(90,90)(由于是初始化,临时对象被编译器优化)
~Test() //析构p1指向的对象
~Test() //析构p2指向的对象1
~Test() //析构p2指向的对象0
~Test() //析构对象p4
~Test() //析构对象t3
~Test() //析构对象t2
~Test() //析构对象t4
~Test() //析构对象t5
~Test() //析构对象t1
通过以上分析可以得出以下结论:
对于全局(或者静态全局)对象,程序一开始(还未进入main函数之前)就对其进行构造,程序快要结束时等其他局部变量析构完成之后才进行析构(先构造的后析构);
对于局部对象,当程序运行至其构造代码行时才进行构造,程序结束前进行析构(在全局对象之前,局部对象同样满足先构造的对象后析构的原则);
对于被static修饰的局部变量,其构造和普通对象的构造区别在于其在数据段进行构造,因此其析构时在所有的普通局部对象之后,全局对象之前;
对于以new方式构造的对象,在指向该对象的指针变量被delete时才进行析构;
对于已经存在的对象以赋值方式赋值时会通过构造产生一个临时对象,通过临时对象赋值该对象,当赋值语句结束时,临时对象被析构;
对于形似
Test t = Test(10,10)
的对象构造时,不是这样的:先构造一个临时对象,再通过拷贝构造函数构造对象t,等该语句结束时再析构掉临时对象。而是这样的:编译器直接进行对象t的构造,不会产生临时对象,对于这种情况编译器会优化构造的过程,不去产生临时对象;对于像指针p3指向的对象,它其实是一个临时对象,该语句执行完之后就会被析构。
还有一种情况,就是在函数调用时对象的生命周期如何呢?
请看以下代码:
class Test
{
public:
Test(int a = 5) :ma(a){ cout << "Test(int)" << endl; }
~Test(){ cout << "~Test()" << endl; }
Test(const Test &src) :ma(src.ma)
{
cout << "Test(const Test&)" << endl;
}
void operator=(const Test &src)
{
ma = src.ma;
cout << "operator=" << endl;
}
int GetValue(){ return ma; }
private:
int ma;
};
Test GetTestObject(Test t)
{
int value = t.GetValue();
Test tmp(value);
return tmp;
}
int main()
{
Test t1(20);
Test t2;
t2 = GetTestObject(t1);
cout << t2.GetValue() << endl;
return 0;
}
它的打印结果如下:
它的打印结果分析如下:
Test(int) //构造对象t1(20)
Test(int) //构造对象t2(5)
Test(const Test&) //拷贝构造对象t
Test(int) //构造对象tmp(20)
Test(const Test&) //拷贝构造临时对象(main上的)
~Test() //析构对象tmp
~Test() //析构对象t
operator= //给对象t2赋值
~Test() //析构临时对象
20
~Test() //析构对象t2
~Test() //析构对象t1
可以看到调用函数传参是,会通过t1拷贝构造形参对象t,返回时会通过对象tmp拷贝构造一个临时量(由于对象是一个整体不仅仅只有成员变量所占的空间那么大,还会有编译器自动为其产生的默认构造函数,析构函数等等,对象的字节数定会超过4个字节,所以采用临时量返回),然后tmp析构,形参对象t析构,函数调用结束后才赋值给t2,赋值语句结束,临时对象析构。
如果将Test GetTestObject(Test t)中的形参改为用引用接收会有什么不一样的吗?请看以下代码:
Test GetTestObject(Test &t)
{
int value = t.GetValue();
Test tmp(value);
return tmp;
}
int main()
{
Test t1(20);
Test t2;
t2 = GetTestObject(t1);
cout << t2.GetValue() << endl;
return 0;
}
其打印结果如下:
明显可以看出相比较上次少了一次拷贝构造和析构,引用不就是别名嘛,形参采用引用的话实际上是直接操作对象t1,不用再去产生别的对象,会提高代码的效率。
如果不采用tmp返回而是采用临时量返回呢?请看下面代码:
Test GetTestObject(Test &t)
{
int value = t.GetValue();
return Test(value);
}
int main()
{
Test t1(20);
Test t2;
t2 = GetTestObject(t1);
cout << t2.GetValue() << endl;
return 0;
}
打印结果如下:
可以看出同过临时量返回时,会比上一次的更为高效,返回时构造的临时量其实是在main()的栈上,编译器并没有先在调用函数的栈上构造临时对象而是直接构main栈上构造临时对象,这也是编译器的一种优化。
有没有可能再高效一点呢?请看下面代码:
Test GetTestObject(Test &t)
{
int value = t.GetValue();
return Test(value);
}
int main()
{
Test t1(20);
Test t2 = GetTestObject(t1);
cout << t2.GetValue() << endl;
return 0;
}
打印结果如下:
比较上次可以发现,这次链临时对象都没有产生,应该说是最高效的了吧,好像就跟没有调用GetTestObject(Test &t)似的,那么问题来了,对象t2是在函数GetTestObject(Test &t)结束之前完成构造还是结束之后才构造的?
通过调试会发现,对象t2的构造是在调用函数没结束之前就完成构造的。
大家一定会有这种疑问:调用函数的时候有没有传对象t2的地址进去,那么在被调用函数中是怎么找到对象t2并对它进行构造的?
我找到了汇编代码,如下:
可以清楚的看到,在调用函数之前,对象t2的地址也被压了进去,表面上看只传了一个参数,实际却传了两个参数,t2是隐式的。
总结一下:对于函数调用时,编译器会做出三种优化:
函数形参在接收参数时采用引用接收是更为高效,编译器会阻止新对象的产生;
函数若要返回类类型时采用临时量回比返回局部对象更为高效,编译器会直接经对象构建在main函数栈上的临时空间里;
对象在接收返回值为类类型的函数时采用初始化比采用赋值更为高效,编译器会阻止临时对象的产生,并且会将此对象的地址隐式传递给该函数,该函数会在调用结束之前完成该对象的构造。