C++对象模型的那些事儿之五:NRV优化和初始化列表
前言
在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的优化操作:NRV,以及关于初始化列表的一些容易踩的“坑”!
NRV
NRV是named Return Value的缩写,翻译过来就是具名返回值优化,这个优化到底在编译层干了些什么事,我们先来看个例子:
class Animal{
public:
Animal(){
cout<<"Animal construct!"<<endl;
age = 0;
}
~Animal(){
cout<<"Animal destruct!"<<endl;
}
Animal(const Animal& _animal){
cout<<"Copy Construct!"<<endl;
age = _animal.age;
}
private:
int age;
};
Animal get(){
Animal animal;
return animal;
}
int main(){
Animal animal = get();
}
这里先不说运行上述代码会输出什么,读者可以思考思考,亦可带着疑惑继续往下看。
不用NRV优化
在上述例子中,get函数里面定义了一个animal对象,然后将其作为返回值返回。如果按照调用拷贝构造函数的方法,编译器会对代码进行如下扩张行为:
- 首先加上一个额外参数,类型是类对象的一个引用,这个参数用来放置被“拷贝构建”而得到的返回值
- 在return命令之前安插一个拷贝构造函数调用操作,以便将欲传回的对象的内容当做上述新增参数的初值。
上述两个步骤的扩张行为听起来还是比较绕口,我们以下面的伪码来替代以下:
Animal get(Animal &_result)//增加一个额外参数作为引用传进函数
{
Animal animal;
animal.Animal::Animal();//这里会调用Anmial的空构造函数
_result.Animal(animal);//调用拷贝构造函数将animal的值赋给返回值
}
按照上述伪码的梳理之后,应该比较理解编译器的那两步扩张行为。看起来就比较麻烦,临时变量animal根本就没有起到什么作用。编译器当然不允许这种浪费资源的对象存在,所以,今天的主角NRV优化要入场了。
NRV优化
观察事物都是从表象入手,然后再去探究底层。我们先来看看最开始的例子的输出结果:
Animal construct!
Animal destruct!
想必大家也看到,输出表明调用get函数后,压根就没有拷贝构造函数什么事,从头到尾都只有构造函数和析构函数在工作。这是为什么?按照拷贝构造函数的理解,这里应该会输出Copy Construct!。很显然,编译器在里面动了手脚。
既然animal完全属于浪费资源,拖慢编译的存在,何不直接以_result替代之?NRV就做了这样的优化,以下时优化的伪码:
Animal get(Animal &_result)//额外参数作为引用参数传进函数
{
//此处不会生成animal这个临时对象,而是直接对_result进行构造
_result.Animal::Animal();
//...将处理animal对象的操作全部移交到_result上
return;//直接返回,_result里面已存有返回值
}
我们可以看到,这里正好可以跟输出对应,仅在构造_result的时候调用了空构造函数,在函数结束后调用了析构函数。从而避免了临时对象以及拷贝构造函数带来的消耗。
额外的测试
经过上述对NRV的讲解之后,笔者对其非常感兴趣,想看看编译器在哪些地方偷偷的帮我优化了代码,于是写下如下两行:
Animal a = Animal(1024);
Animal b = (Animal)1024;
不做NRV的话,自然是先创建一个临时对象,再将临时对象通过拷贝构造函数赋给指定对象。很显然,编译器不会这么干,输出也验证了这一点:
Animal construct!
Animal construct!
Animal destruct!
Animal destruct!
在《深度探索C++对象模型》一书中提到,如果没有显示定义拷贝构造函数的时候,编译器时不会激活NRV优化的,想来大数据量的测试才能看出NRV优化的效果,这里我就偷个懒,仅仅在此说明一下。
初始化列表
显示定义构造函数的时候,成员变量要么在初始化列表初始化,要么就在函数体内初始化。函数体内初始化就不再次讨论,本小节来讨论初始化列表干了那些事。
一下四种情况,必须在初始化列表中初始化:
- 当初始化一个引用成员变量时
- 当初始化一个const成员变量时
- 当调用一个基类的构造函数,其拥有一组参数时
- 当调用一个成员遍历的构造函数,其拥有一组参数时
为什么必须呢?就是要让大家养成一个习惯,从而来提高效率,在这四种情况下,如果不在初始化列表中初始化,程序能够正确编译,但效率不高,如下:
class Animal{
public:
string name;
int age;
Animal(){
name = 0;
age =0;
}
};
上述测试代码中,针对string变量name的初始化会先产生一个临时string对象,然后通过拷贝构造函数来初始化name,最后析构掉临时对象。其伪码如下:
Animal::Animal(){
name.string::string();
//产生临时对象
string temp = string(0);
//调用赋值运算符函数
name.string::operator=(temp);
//析构临时对象
temp.string::~string();
}
可见这样效率真的非常低,如果放在初始化列表中来初始化的话,就如下代码:
Animal::Animal():name(0){
age = 0;
}
编译器对初始化列表的初始化操作会默认下面的扩张行为:
Animal::Animal(){
name.string::string(0);//直接调用构造函数来初始化name
age = 0;
}
小心!陷阱!
初始化列表的陷阱经常作为面试的考题,观察下列代码:
class A{
public:
int i;
int j;
A(int val):j(val),i(j){}
};
int main(){
A a(1);
cout<<"a.i="<<a.i<<endl;
cout<<"a.j="<<a.j<<endl;
}
如果对初始化列表理解不足的话,可以会觉得输出1,1,可是,测试输出如下:
a.i = 32765
a.j = 1
i被初始化为随机值,j被初始化为1,这里就涉及到编译器在安插构造函数的顺序的问题了。
在初始化列表中初始化的变量,都会按照声明顺序,在构造函数内安插其初始化代码。
总结
本篇博客首先讲了NRV优化的概念和实现方式,并对其做了测试,需要注意的是:
- 编译器只有在显示定义了拷贝构造函数的情况下才会激活NRV优化
- 一旦函数变得复杂,优化也变得比较难以实施,所以NRV褒贬不一
- NRV是针对效率来考虑了,不用NRV并不会引起编译错误,效率大打折扣
另外,对于初始化列表,需要注意:
- 变量的初始化顺序不是由初始化列表中的顺序决定,而是由变量的定义来决定
本篇博客就讲到这里。
About Me
由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。
最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me。
另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode
欢迎持续关注!Thx!
C++对象模型的那些事儿之五:NRV优化和初始化列表的更多相关文章
- C++对象模型的那些事儿之四:拷贝构造函数
前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...
- C++对象模型的那些事儿之三:默认构造函数
前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...
- C++对象模型的那些事儿之二:对象模型(下)
前言 上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承.多重继承和虚继承后内存布局上又有哪些变化呢?多态真正 ...
- C++对象模型的那些事儿之一:对象模型(上)
前言 很早以前就听人推荐了<深入理解C++对象模型>这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久.近期由于找工作对C++的知识做了一个全面系统的学习, ...
- GC那些事儿--Android内存优化第一弹
引言 接App优化之内存优化(序), 作为App优化系列中内存优化的一个小部分. 由于内存相关知识比较生涩, 内存优化中使用到的相关工具, 也有很多专有名词. 对Java内存管理, GC, Andro ...
- C++对象模型那点事儿(布局篇)
1 前言 在C++中类的数据成员有两种:static和nonstatic.类的函数成员由三种:static,nonstatic和virtual. 上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类 ...
- C++对象模型的那些事儿之六:成员函数调用方式
前言 C++的成员函数分为静态函数.非静态函数和虚函数三种,在本系列文章中,多处提到static和non-static不影响对象占用的内存,而虚函数需要引入虚指针,所以需要调整对象的内存布局.既然已经 ...
- 程序分析与优化 - 4 工作列表(worklist)算法
本章是系列文章的第四章,介绍了worklist算法.Worklist算法是图分析的核心算法,可以说学会了worklist算法,编译器的优化方法才算入门.这章学习起来比较吃力,想要融汇贯通的同学,建议多 ...
- 全网扫描扫描10000端口后的优化脚本&域名列表指定端口的批量测试
方法一: #coding=utf-8 import urllib2 import threading from time import ctime,sleep print "Start-Ti ...
随机推荐
- ●BZOJ 4289 PA2012 Tax
●赘述题目 算了,题目没有重复的必要. 注意理解:对答案造成贡献的是每个点,就是了. 举个栗子: 对于如下数据: 2 1 1 2 1 答案是 2: ●题解 方法:建图(难点)+最短路. 先来几个链接: ...
- ●BZOJ 2119 股市的预测
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2119 题解: 这个题很好的. 首先把序列转化为差分序列,问题转化为找到合法的子序列,使得去除 ...
- bzoj 1085: [SCOI2005]骑士精神
Description 在一个5×5的棋盘上有12个白色的骑士和12个黑色的骑士,且有一个空位.在任何时候一个骑士都能按照骑士的走法(它可以走到和它横坐标相差为1,纵坐标相差为2或者横坐标相差为2,纵 ...
- ●BZOJ 4822 [Cqoi2017]老C的任务
题链: https://www.luogu.org/problemnew/show/P3755 (洛谷上数据范围给全了的) 题解: 树状数组,离线询问 (本来想弄一个二维树状数组/二维RMQ,然后直接 ...
- [BZOJ]1079 着色方案(SCOI2008)
相邻色块不同的着色方案,似乎这道题已经见过3个版本了. Description 有n个木块排成一行,从左到右依次编号为1~n.你有k种颜色的油漆,其中第i种颜色的油漆足够涂ci个木块.所有油漆刚好足够 ...
- bzoj 2734: [HNOI2012]集合选数
题目描述 <集合论与图论>这门课程有一道作业题,要求同学们求出{1, 2, 3, 4, 5}的所有满足以 下条件的子集:若 x 在该子集中,则 2x 和 3x 不能在该子集中. 同学们不喜 ...
- bzoj2243[SDOI2011]染色 树链剖分+线段树
2243: [SDOI2011]染色 Time Limit: 20 Sec Memory Limit: 512 MBSubmit: 9012 Solved: 3375[Submit][Status ...
- linux办公软件的使用和病毒防范
今天看了linux办公软件的使用和病毒防范,特做此记录,将不熟悉的内容总结一下: openoffice 和liberoffice是可以跨平台的两款办公软件.odt是openoffice的扩展名.lib ...
- React .js框架的环境搭建
React学习笔记(一)- 环境搭建 最近在学习react相关的知识,刚刚起步,一路遇坑不断.自己做个笔记,方便日后总结,也供相同趣味的小伙伴一起交流探讨. 学习时主要参考官网的教程:https: ...
- Goland 提示 :configuration is still incorrect 的解决
安装好 Goland 后,调试编译的时候提示 goland configuration is still incorrect,百度 和 Google 都没有明确答案 Google 上有一些提示,但是也 ...