c++中的构造(包括移动),赋值(包括移动),析构详解
这五种操作:构造(包括移动),赋值(包括移动),析构其实就是定义了对一个对象进行构造,赋值,析构时的行为。理解这些行为并不复杂,复杂的是理解在继承下这些行为的表现。需要注意的是他们并不会被继承(传统意义上的继承)。
拷贝构造函数
形式:
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
};
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。引用是必须的(否则会出现无限循环),const是通常的。不应是explicit(传参,返回都是隐式)。
如果我们没有为一个类定义一个拷贝构造函数,编译器会为我们定义一个。合成版本会逐个按位拷贝非static成员,如果是类类型,机会调用类的拷贝构造函数。
拷贝赋值运算符
形式:
class Foo(){
public:
Foo& operator=(const Foo& ); //赋值云运算符
};
这本质上是运算符重载,重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号。因此,赋值运算符就是一个名为operator=的函数。类似于其他任何函数,运算符函数也有一个返回类型和一个参数列表。
重载运算符的参数表示运算符的运算对象。赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。右侧参数作为参数传递。为了与内置类型赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝运算符。就是将右侧运算对象的每个非static成员赋值于左侧运算对象的对应成员,这一功作主要是通过成员类型的拷贝赋值运算符来完成的。
析构函数
形式:
class Foo{
public:
~Foo(); //析构函数
};
由于析构函数不接受参数,因此它不能被重载。对于一个给定的类,只会有唯一一个析构函数。
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按照初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
与普通指针不同,智能指针是类类型,所以具有析构函数。因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。
何时会调用析构函数?
无论何时一个对象被销毁,就会自动调用其析构函数。
变量在离开作用域时被销毁。
当一个对象被销毁时,其成员被销毁。
容器被销毁时,其元素被销毁
对于动态分配的对象,当其指向它的指针应用delete运算符时被销毁。
对于临时对象,当创建它的完整表达式结束时被销毁。
析构函数自动运行,我们的程序可以按需求分配资源,无需担心何时释放这些资源。
当一个类未定义自己的析构函数时,编译器会为它定义一个合成的析构函数。合成的析构函数体为空。如下:
class Sales_data{
public:
~Sales_data(){}
};
在析构函数执行完毕后,成员会被自动销毁。特别的,string的析构函数会被调用,它将释放bookNo成员所用的内存。
认识到析构函数体本身并不直接销毁成员是非常重要的。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
移动构造函数
形式:
class StrVec{
public:
StrVec(StrVec&& s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;
}
};
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用,不同于构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
除了完成资源移动,移动构造函数还必须确保移动后源对象处于这样一种状态,销毁它是无害的.特别是,一旦资源完成移动,源对象必须不在指向被移动的资源,这些资源的所有权已经归属新创建的对象。
由于移动操作“窃取”资源,它并不分配资源。因此,移动操作不会抛出任何异常。用noexcept告知标准库我们的移动构造函数不会抛出异常,因此标准库减少了未处理抛出异常这种可能性而做的额外的工作。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。声明与定义都必须指定noexcept。不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
移动赋值运算符
形式:
class StrVec{
public:
StrVec& operator=(StrVec&& rhs)noexcept{
if(this!=(&rhs)){
free() //释放已有元素
elements=rhs.elements; //从rhs接管资源
first_free=rhs.first_free;
cap=rhs.cap;
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
};
我们检查this指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们不需要做任何事情。否则,我们释放左侧运算对象所使用的内存,并接管给定对象的内存。与移动构造函数一样,我们将rhs中的指针职位nullptr。
我们费尽心机的检查自赋值情况可能有些奇怪。毕竟,移动赋值运算符需要右侧运算对象的一右值。我们进行检查的原因是次此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同资源)。
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此 ,当我们编写一个移动操作时,必须确保移动后源对象进入一个可析构状态。我们的StrVec的移动操作满足这一要求,就是通过将以后源对象的指针成员设为nullptr实现的。
除了将移动后源对象置为析构安全的状态后,移动操作还必须保证对象仍然是有效的。也就是说可以安全的为其赋予新值,或可以安全的使用而不依赖于当前的值。另一方面,移动操作后对源对象留下的值没有任何要求。因此我们的程序不应依赖于移后源对象中的值。
如果一个类定义了自己的拷贝构造函数,拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。只有当一个类没有定义任何版本的拷贝控制成员,且类的每个非static成员都可移动时,编译器才会为它合成移动构造函数和移动赋值运算符。编译器可以移动内置类型的成员,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。
需要析构函数的类也需要拷贝和赋值操作
需要拷贝操作的类也需要赋值操作,反之亦然
使用=default
将拷贝控制成员定义为=default,来现实的要求编译器生成合成的版本。
class Foo{
public:
Foo()=default;
Foo(const Foo& )=default;
Foo& operator=(const Foo& );
~Foo()=default;
};
Foo& Foo::operator=(const Foo& )=default;
当我们在类内用=default修饰成员的声明时,合成的函数隐式的声明为内联的。如果我们不希望合成的构造函数是内联的,应该只对成员的类外定义使用=default.就像对拷贝赋值运算符所做的那样。
我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。
阻止拷贝
为了阻止拷贝,看起来可能应该不定义拷贝控制成员,但是,这种策略是无效的,如果我们的类为定义这些操作,编译器会为他们合成新的版本。
我们可以将拷贝构造函数和赋值运算符定义为删除的函数来阻止拷贝,删除的函数看起来是这样一种函数:我们虽然声明了他们,但我们不能使用他们。在函数的参数列表后面加上=delete来指出我们希望它定义为删除的。
class NoCopy{
public:
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy& )=delete; //阻止拷贝
NoCopy& operator=(const NoCopy& )=delete; //阻止拷贝
~NoCopy()=delete; //使用合成的默认构造函数
};
与=default不同。=delete必须出现在函数第一次声明的时候。我们可以对任何函数指定=delete(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default).析构函数不能是删除的成员。如果析构函数被删除,就无法销毁此类型的对象了,对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时对象,没有定义创建就可以。对于析构函数已经删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
在继承中基类的析构函数是虚函数,派生类继承的是虚属性,而不是析构函数。
c++中的构造(包括移动),赋值(包括移动),析构详解的更多相关文章
- Mybatis中接口和对应的mapper文件位置配置详解
Mybatis中接口和对应的mapper文件位置配置详解 原链接为:https://blog.csdn.net/fanfanzk1314/article/details/71480954 今天遇到一个 ...
- 配置tomcat服务器内存大小中的Xms、Xmx、PermSize、MaxPermSize 详解
1.参数的含义 -vmargs -Xms256m -Xmx512m -XX:PermSize=256M -XX:MaxPermSize=512M -vmargs 说明后面是VM的参数,所以后面的其实都 ...
- PHP中IP地址与整型数字互相转换详解
这篇文章主要介绍了PHP中IP地址与整型数字互相转换详解,本文介绍了使用PHP函数ip2long与long2ip的使用,以及它们的BUG介绍,最后给出自己写的两个算法,需要的朋友可以参考下 IP转换成 ...
- ArcGIS中的北京54和西安80投影坐标系详解
ArcGIS中的北京54和西安80投影坐标系详解 1.首先理解地理坐标系(Geographic coordinate system),Geographic coordinate system直译为地理 ...
- [转]js中几种实用的跨域方法原理详解
转自:js中几种实用的跨域方法原理详解 - 无双 - 博客园 // // 这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同 ...
- Nginx服务器中配置非80端口的端口转发方法详解
这篇文章主要介绍了Nginx服务器中配置非80端口的端口转发方法详解,文中使用到了Nginx中的proxy_pass配置项,需要的朋友可以参考下 nginx可以很方便的配置成反向代理服务器: 1 2 ...
- java使用POI操作XWPFDocument中的XWPFRun(文本)对象的属性详解
java使用POI操作XWPFDocument中的XWPFRun(文本)对象的属性详解 我用的是office word 2016版 XWPFRun是XWPFDocument中的一段文本对象(就是一段文 ...
- Java中JSON字符串与java对象的互换实例详解
这篇文章主要介绍了在java中,JSON字符串与java对象的相互转换实例详解,非常不错,具有参考借鉴价值,需要的朋友可以参考下 在开发过程中,经常需要和别的系统交换数据,数据交换的格式有XML.JS ...
- Android java程序员必备技能,集合与数组中遍历元素,增强for循环的使用详解及代码
Android java程序员必备技能,集合与数组中遍历元素, 增强for循环的使用详解及代码 作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 For ...
- storm源码之理解Storm中Worker、Executor、Task关系 + 并发度详解
本文导读: 1 Worker.Executor.task详解 2 配置拓扑的并发度 3 拓扑示例 4 动态配置拓扑并发度 Worker.Executor.Task详解: Storm在集群上运行一个To ...
随机推荐
- 原生js--HTTP进度事件
1.HTTP进度事件属于XHR2规范定义的系列事件 2.事件模型中会触发不同的事件,所以不再需要检查readyState事件 3.当调用send()时,触发loadstart事件 4.当正在加载服务器 ...
- OGG 3节点级联时 关键参数
目标架构为: node1-> node2->node3 node1-> node2 已经同步中,只是需要在此基础上做个node2 ->node3 的同步. 部署后发现 node ...
- sencha touch 常见问题解答(26-50)
26.sencha touch在华为.红米等部分手机下hide事件失效,msgbox无法关闭怎么办 答:请看http://www.cnblogs.com/cjpx00008/p/3535557.htm ...
- sencha touch 压缩js,css遇到的问题
在使用工具压缩css和jss时,我遇到了以下问题 1. showBtn: { tap: function (t, value) { this.redirectTo(t.config.goto); } ...
- hihoCoder挑战赛28 题目2 : 二进制翻转
题目2 : 二进制翻转 时间限制:20000ms 单点时限:1000ms 内存限制:256MB 描述 定义函数 Rev(x) 表示把 x 在二进制表示下翻转后的值 例如: Rev(4)=1,因为 4 ...
- 【咸鱼教程】一个简单的弹出二级菜单UIPopupMenu
一. 实际效果 演示地址 二.实现原理主要用Button+List组件,和遮罩实现. 1. 点击Button时,将List下移展开.2. 再次点击Button,或者选中List中的某一项时,将List ...
- Egret中的三种单例写法
1 普通的单例写法 as3中也是这么个写法. 缺点:每个单例类里都要写instance和getInstance. class Single{ private static instance:Singl ...
- 【CF887E】Little Brother 二分+几何
[CF887E]Little Brother 题意:给你n个圆和一条线段,保证圆和圆.圆和线段所在直线不相交,不相切,不包含.求一个过线段两端点的圆,满足不和任何圆相交(可以相切.包含).问圆的最小半 ...
- Gradle 教程
extends:http://www.zhihu.com/question/27866554/answer/38427122 stormzhang博客精华 有一个Gradle 的教程,足够你入门啦. ...
- Unity3D笔记 愤怒的小鸟<三> 实现Play界面2
前言:在Play页面中给Play页面添加一个“开始游戏”和“退出游戏”按钮顺便再来一个背景音乐 添加按钮可以是GUI.Button(),也可以是GUILayout.Button():给图片添加按钮可以 ...