本文大部分基于我个人的理解。主要是因为网上关于匿名对象的讨论有点乱,我想找点权威的解释,在 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

Last modification:April 11th, 2020 at 03:59 pm