匿名对象?临时对象?
关于匿名对象与临时对象,这个概念不是绝对的,概念的区分往往十分拗口难记。要根据作用域,生存时间和用法来来决定;工作多年这些拗口的概念我从来没有真的记住过,也没有一个博客讲清楚他们的区别。这里我们参考《Effective C++》中得称谓,称之为局部对象。但是局部也是有范围得。下面论证。
1 | 无名则无份-临时对象临时生存 |
2 | 名正则言顺-起个名字活得久 |
3 | 非分之想-不要越界。 |
4 | 编译器的能力 |
5 | 原则 |
假设我们有这样一个类:
class Test
{
public:
Test(int i = 0):m_test(i)
{
cout<<this<<"--construct in 'Test(int i = 0)'"<<endl;
}
~Test()
{
cout<<this<<"--deconstruct in '~Test()'"<<endl;
}
Test(const Test &one)
{
m_test = one.m_test;
cout<<this<<"--copy 'Test(const Test &one)'"<<endl;
}
Test &operator =(const Test &one)
{
if(this == &one)
return *this;
m_test = one.m_test;
cout<<this<<"assign in 'operator =(const Test &one)'"<<endl;
}
void DisTestVal() const
{
cout<<"show value 'in DisTestVal():'"<<m_test<<"--at--"<<this<<endl;
}
const void * GetThis() const
{
return this;
}
private:
int m_test;
};
下面我们将使用不同的方式来辩证的讨论上面类在使用中构建对象的类型:
临时对象/匿名对象:
1)无名则无份:
int main()
{
Test(2);
getchar();
cout<<"main func finished"<<endl;
}
运行结果:
执行getchar()
匿名对象 的生命周期转瞬即逝,其生存空间就是代码所在的行,本质上匿名对象就是构造函数得调用,函数调用完成,其生命周期也就结束了。
看到了吗,在这里我们使用getchar来阻塞main函数的结束,Test(2)这个对象在main函数还没有结束时就析构了,其生命周期昙花一现,其生存空间就时代码所在的行。如果我们构建一个正常的对象于main函数中,其生命周期同main函数。但是匿名对象就不行。没有名字便没有生存的资格。当然我们很少使用这种匿名对象,但是,其可以发起函数调用。如果这个对象对外提供指针,由于其短暂的生命周期与生存空间,等到我们能用指针的时候,它却已经失效了。
2)名正则言顺:
如何延长一个匿名对象的生命周期呢?他与常规对象的本质区别就是没有一个名字。所以给它取个名字吧。
int main()
{
const Test &rt = Test(2); //有了名字,才能活下来
getchar();
cout<<"main func finished"<<endl;
}
运行结果:
这里我们给Test(2)起了别名,当然它也没有其他的名字,其声明周期便同main了。其效果与Test tobj(2)是一样的,只不过其类型是cosnt Test&的。
3)非分之想
按照上面的规则,在一个作用域中,一个匿名对象只要有名字,便可以活下来,那如果跨作用域呢?
const Test ReturnTemp()
{
const Test & temp = Test(3);
return temp;
// return Test(4);
}
int main()
{
const Test &rtt = ReturnTemp();
rtt.DisTestVal();
cout<<"----------------------------"<<endl;
const Test tt = ReturnTemp(); //这里产生拷贝,但是拷贝之前,临时对象已经被析构了
tt.DisTestVal();
getchar();
cout<<"main func finished"<<endl;
}
运行结果:
结果分析:
以上代码1处已经析构了,我们拿到了一个已经析构对象的引用。2处用这个引用调用函数依然获取到了“正确”值。
使用3处析构对象在4处拷贝构造了另一个对象,函数调用结果也没报错。
匿名对象与临时对象都是相对的概念,当这个无名对象产生于自己的作用域空间时,我们叫它匿名对象,一来没有名字,而来确实构造了一个对象。
临时对象定义可以认为是从时间角度出发对一个匿名对象的另一种称谓。在一个作用域内,匿名对象有名字了(const T &类型),不出作用域,其生命周期同作用域。
4)编译器的能力问题:
以上用析构的对编译器没有报错,运行结果也无异常,这真是令人恐怖的地方。试想你编写过的程序,是不是运行的一段时间突然闪退,或者突然崩溃呢?
很多这种可怕的错误,编译器是检查不出来的,我在windows和linux使用Qt-ide和 gcc,两者编译都没有报错。所以这种定时炸弹说不定什么时候爆炸了。
编译器能力有限,不要指望编译器能帮你把所有错误检查出来,有时连警告也没有!
5)原则:
就此不管临时对象也罢,匿名对象也罢,你只要记住以下原则:
const T &能够接受一个临时对象的引用而不是临时对象!
函数栈上的对象一定不要返回指针或者引用。
很多时候,返回对象并没有太多问题,至于性能问题,Linux和Windows平台都有RVO和NRVO机制。比你想象的性能好很多。关于返值优化这里不展开讨论。
参考资料:
《Effective C++ 》第二版中文版 条款31
援引:
局部对象 ----- 顾名思义 ---- 仅仅是局部的。也就是说,局部对象是在被定义时创建,在离开生命空间时被销毁的。所谓生命空间,是指它们所在的函数体。当函数返回时,程序的控制离开了这个空间,所以函数内部所有的局部对象被自动销毁。因此,如果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前被销毁了。