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有着千丝万缕的关系,没有做实验验证,当然我的猜想也有可能是错误的。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 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" 转载请保留原文链接及作者。