C++幕后故事(十一)--隐藏的对象

C++幕后故事(十一)–隐藏的对象

这一节的主要知识点:

1.什么隐藏的对象
2.隐藏的对象在哪里?
3.隐藏的对象的副作用以及如何避免。
4.为什么需要隐藏的对象。

1. 临时对象含义

这里所说的隐藏的对象,其实是临时对象。它没有名字,生命周期可能转瞬间消失,要是没有练就一双火眼金睛,很难发现其所在之处(我觉得叫匿名对象可能更合适)。

但是在实际中大部分人常常把局部对象临时对象搞混了。局部对象是有名字,这个就是和临时对象最大的区别。局部对象在代码中清晰可见,而临时对象往往深而不露。

我举个例子,你立马就能区别出两者:

class Obj
{
public:
    Obj() { cout << "Obj ctor" << endl; }
    ~Obj() { cout << "~Obj dtor" << endl; }

};

Obj test_local_temporary_object()
{
// 局部对象
    Obj local_obj;  
    return local_obj;
}

看到临时对象在哪里了?

其实它隐藏起来了,仔细看这个函数的返回值,它是将局部对象local_obj返回出去。这里其实就藏了一个临时对象,local_obj就会被转为临时对象作为返回值传递出去。

2. 临时对象的藏身之处

2.1 隐式类型转换

class ConvertObj
{
public:
    ConvertObj() { cout << "ConvertObj ctor" << endl; }
    ~ConvertObj() { cout << "mNum:"<< mNum << " ~ConvertObj dtor" << endl; }

    ConvertObj(int num) 
    {  
        mNum = num;
        cout << "mNum:" << mNum << "ConvertObj ctor num" << endl; 
    }

    ConvertObj(const ConvertObj &rhs) 
    {   
        mNum = rhs.mNum;
        cout << "ConvertObj copy ctor" << endl; 
    }

    int mNum = 0;

};

void ConvertUp(ConvertObj obj)
{
    //TODO something
}

void test_implicit_convert()
{
    // 这里就进行了隐式类型转换
ConvertUp(1);
// mNum:1 ConvertObj ctor num
// mNum:1 ~ConvertObj dtor
}

2.2 函数返回值

这段代码我是在ubuntu 14.04 GCC 4.8.4,要开启选项 -fno-elide-constructors

ConvertObj test_ret_obj()
{
    ConvertObj obj;
    return obj;

    // ConvertObj ctor
    // ConvertObj copy ctor
    // mNum:0 ~ConvertObj dtor
    // mNum:0 ~ConvertObj dtor
}

在函数返回时会将局部对象进行copy构造一份,然后局部对象再析构,最后这个函数执行完临时对象再析构。

3. 临时对象的销毁时机

在代码中临时对象并不是显而易见的,所以我们对一个临时对象的生命周期,往往不是很敏感。

在C++标准中,对临时对象的生命周期有明确的规定。

1.如果在一个完整的表达式产生了临时对象,那么只有在整个完整的表达式最后一个步骤完成了,此临时对象才能消失。

((ObjA > ObjC) && (ObjB > ObjD)) ? ObjA+ObjD : Convert(ObjB+ObjD);

大家看这个表达式,这里面的每个子表达式都有可能会产生临时对象。而这个临时对象,只有在整个表达式都计算完毕之后才能销毁。

2.凡是有表达式执行产生的临时对象,只有在对象的初始化操作完成之后才能消失。

string verbose_msg = verbose ? log_time + log_file_name : log_file_name;

这里的表达式所产生的临时对象,之后verbose_msg这个变量被初始化完毕之后才能销毁。

3.如果一个临时对象被绑定到reference上,只有在reference对象生命结束或者是出了临时对象的作用域,这时临时对象就会被销毁。

const string &name = “linux”;

这里就是将临时对象绑定了reference对象上,可以说延长了临时对象的生命周期。当name结束时,临时对象也就被销毁了。

4. 为什么需要临时对象

其实最大的作用就是两个字:方便。

4.1 编译器的方便

因为编译器在返回一个局部对象的给外面的调用者,这时就会产生一个临时对象。当然,编译器开启优化选项的时候会把这一步优化掉的。

4.2 方便调用者

在某些情况下,当参数可以发生隐式类型转换的时候。这时编译器就会很乐意帮我们做了转换,使得调用可以正常进行,为我们程序员提供了方便。

5. 临时对象的副作用

最大的副作用就是:带来了额外的开销。
因为需要构造临时对象,所以肯定避免不了对象的构造、析构函数的调用。当一个对象的构造和析构开销很大的话,临时对象就是一个优化点。

6. 避免不必要的临时对象

6.1 NRV优化

NRV全称是name return value,就是返回值优化。

编译器在函数返回一个局部对象时,会直接将临时对象保存到函数外,这样就节省了对象的copy构造和析构的成本,来提交效率。

这段代码再vs2013 使用release选项

ConvertObj test_ret_obj()
{
    ConvertObj obj;
    return obj;

// 开启优化选项
// ConvertObj ctor
// mNum:0 ~ConvertObj dtor
}

6.2 函数重载

针对隐式类型参数转换,这个其实很好解决的。就是我们重载这个函数就可以了。

void test(string str)
{
    //TODO something
}

// 如果没有下面重载test函数,你在调用test函数传递的参数是char *参数
// 这时就会构造一个临时对象,先是通过string的构造函数将char *参数
// 转换为string类型,再传递进去。重载test函数之后,就会省去一个临时对象的构造
void test(char *str)
{
    //TODO something
}

注意:

如果当参数是个类类型的时候,我们可以使用explicit关键字,明确的拒绝隐式转换。
比如上面的类类型:

class ConvertObj
{
public:
    explicit ConvertObj() { cout << "ConvertObj ctor" << endl; }
    ~ConvertObj() { cout << "mNum:"<< mNum << " ~ConvertObj dtor" << endl; }
    // 拒绝隐式类型转换
    explicit ConvertObj(int num) 
    {  
        mNum = num;
        cout << "mNum:" << mNum << "ConvertObj ctor num" << endl; 
    }

    ConvertObj(const ConvertObj &rhs) 
    {   
        mNum = rhs.mNum;
        cout << "ConvertObj copy ctor" << endl; 
    }

    int mNum = 0;
};


void test_implicit_convert()
{
    // 这是再编译的时候,就会报错
    ConvertUp(1);
}

7. 总结

临时对象是个很容易忽视知识点,而且这个知识点是面试官很喜欢问的地方。虽然大部分情况下编译器会帮我们做优化,但是我们自己也要有意识的去避免这个问题,不能依赖编译器的优化。

这篇文章仅仅谈到了临时对象的一些东西,并没有扩展到其他的方面。但是我猜想这里和C++11中的forward和move有着千丝万缕的关系,没有做实验验证,当然我的猜想也有可能是错误的。

image


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 635672377@qq.com

文章标题:C++幕后故事(十一)--隐藏的对象

文章字数:1.7k

本文作者:刘世雄

发布时间:2020-01-10, 14:12:18

最后更新:2020-01-10, 06:13:31

原始链接:http://lsxcpp.com/2020/01/10/cpp-11-hide-object/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录