本文大部分基于我个人的理解。主要是因为网上关于匿名对象的讨论有点乱,我想找点权威的解释,在 C++ Primer 中也没找到相关的内容(也可能是我漏过去了)。总之这篇文章如果有理解上的偏差十分正常,欢迎指正。
匿名对象
class cls {
public:
cls(int i = 0)
: _i(i) {}
private:
int _i;
}
正常创建对象是这样 cls obj(1)
,而匿名对象是类似这样 cls(1)
。
cls(1);
sleep(3);
匿名对象的生命周期仅为当前 C++ 语句。如果你给析构函数加上输出的话,你就会看到上面的程序先打印析构函数的输出,然后才睡眠三秒。
而匿名对象的生命周期也有办法延长,即用一个引用指向一个匿名对象:
const cls& re = cls(1);
sleep(3);
如上面的代码所示,我用一个常引用指向匿名对象,这样该匿名对象就无法在当前 C++ 语句执行完毕就销毁,即生命周期扩展到当前代码块。所以上面的输出应该是先睡眠三秒,然后再打印析构函数的输出。但是注意这里的引用必须要加 const
,我的理解是因为匿名对象的特殊性,其成员变量不允许被修改,所以必须用常引用。这和后面要说的匿名对象做实参类似。
当然这样单独把匿名对象写出来没啥用,看下面这个例子:
cls obj1(10);
cls obj2 = obj1;
cls obj3 = cls(9); // 匿名对象
其中第二句执行的时候,看似会先调用默认构造函数来构造 obj2
,然后再调用赋值运算符重载为 obj2
赋值。但编译器在这里其实会作以优化,即直接用 obj1
构造 obj2
,类似于 cls obj2(obj1)
,即调用拷贝构造函数 。而再看第三句,这样写看似会先构建一个匿名对象,然后根据上面说的编译器优化规则,用这个匿名对象拷贝构造 obj3
。但编译器又作了进一步的优化,这样写会直接用匿名对象的构造参数 9
构造 obj3
,类似于 cls obj3(9)
。
匿名对象做函数参数或者返回值
当然像上面那样写纯属脱裤子放屁,更常用的是将匿名对象用在函数实参或者返回值上。
void func(cls obj) {
cout << obj._i << endl;
}
int main() {
cls obj(10);
func(obj);
func(cls(11));
}
在 C++ 中,若函数形参是类对象且实参也是类对象的时候,例如上面的第二句 func(obj)
,编译器会用实参拷贝构造形参。但如果实参是一个匿名对象,例如上面的第三句 func(cls(11))
,编译器并不会先构造这个匿名对象,然后再用这个匿名对象拷贝构造形参。而是会做些优化,即直接用匿名对象的参数列表构造形参,从而省去构建这个匿名对象的麻烦。在上面的例子中,根本就没有匿名对象被创建,编译器直接用 11
构造形参 obj
。
void func(const cls& obj) {
cout << obj._i << endl;
}
另一个例子是形参为引用,根据之前的讨论我们知道,这里的引用必须加 const
。而此时在主函数中调用这个函数,就确实会创建一个匿名对象了。
cls func(int i) {
return cls(i);
}
int main() {
cls obj = func(11);
}
然后是匿名对象用在函数返回值的情况,这里编译器又做了优化。编译器首先将 cls obj = func(11)
优化成 cls obj = cls(i)
,其中的 i
即函数 func()
返回值中的 i
,然后根据之前说的匿名对象赋值原则,这又相当于 cls obj(i)
。所以从头到尾就只有 obj
这个对象被创建。关于这一点,csdn 中有一个博客也介绍到了:https://blog.csdn.net/china_jeffery/article/details/78893758
总结
写的比较乱,主要是网上对这部分讲解的也比较乱,我想找个权威的解释,C++ primer 里面也没找到匿名对象相关的内容。
但总的来看,编译器对于匿名对象的态度是,尽可能的不构建匿名对象,越能省去匿名对象的存在感就越好。例如上面三个用到匿名对象的例子,其实编译器压根就没创建任何一个匿名对象,都是直接使用匿名对象的参数去直接构造要赋值的有名对象。
除非真的不能省去构建匿名对象的时候,编译器才会真的去生成一个匿名对象供我们使用,例如这个例子:
int main() {
cls obj(1);
obj = cls(10);
}
这个例子中,并没有在定义对象 obj
的同时直接将匿名对象赋值给他,而是先正常构造 obj
,然后再把匿名对象赋值给 obj
。这种情况下,很明显优化不了。于是真正的执行步骤是,先构造 obj
,然后构造匿名对象,最后调用赋值运算符重载将匿名对象赋值给 obj
。
类似的例子还有:
cls func(int i) {
return cls(i);
}
int main() {
cls obj(2);
obj = func(3);
}
由于函数的返回值赋值给了一个已经创建的对象,所以编译器无法优化。所以真实的执行步骤是,用 i
构造一个匿名对象,然后使用赋值运算符重载将其赋值给 obj
。