拷贝控制
-
拷贝控制操作定义:
- 拷贝控制操作包括 拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符,析构函数。1 2决定了使用同一类型的对象来构造这个对象的时候会发生什么。3 4 决定了使用同一类型的对象来赋值给次对象时候会发生什么。5 决定了次对象销毁时候发生什么。
- 拷贝控制操作的重要之处在于,即使我们不定义这几种操作,编译器也会替我们定义这些操作。但是这有时会导致灾难,因此我们一定要在定义一个类的时候自己定义这些操作。
-
拷贝构造函数
- 如果一个构造函数的第一个参数是自身类型的引用, 并且没有其他参数,或者其他参数有默认值,那么就是拷贝构造函数。
拷贝构造函数的参数必须是一个引用。如果不是引用,在调用拷贝构造函数时,首先需要将实参通过拷贝构造函数赋值给形参,那么就会一直循环。 - 如果没有显式定义拷贝构造函数,那么编译器会为我们定义一个 合成拷贝构造函数 。
- 初始化方法分为 直接初始化 和 拷贝初始化。
- 直接初始化就是 直接要求 编译器为其寻找合适的构造函数的初始化。
- 拷贝初始化就是最终需要用到拷贝构造函数的初始化。是先建立一个对象(这个对象可以由构造函数构建而来,也可以是由类类型转换而来的),然后使用这个对象来初始化另外一个对象。典型的有使用 = 运算符进行的初始化,比如使用初始化器的。编译器调用拷贝构造函数或者移动构造函数,将右侧对象拷贝到构造对象中去。
拷贝初始化是 不仅仅用于 = 的初始化,有时会隐式地调用:在将实参的赋值给形参(非引用)的时候,也会发生拷贝初始化。当返回值为一个非引用类型的时候,也会发生拷贝初始化。
- 显式构造函数:前面使用explicit修饰的构造函数。有了这个修饰,这个构造函数只可以用于显式构造的方式,不可以用于隐式构造 的方式。所谓的隐式构造其实就是 在不使用这个构造函数形式的情况下,调用这个构造函数,将参数类型转换为这个对象类型,也叫做 类类型转换。隐式构造很普遍,比如使用 = 的拷贝初始化
string s = "abc"
其实就是将"abc" 通过 隐式转换 为一个string类型临时对象,然后调用拷贝构造函数来初始化对象。
- 如果一个构造函数的第一个参数是自身类型的引用, 并且没有其他参数,或者其他参数有默认值,那么就是拷贝构造函数。
-
拷贝赋值运算符
- 运算符的重载:某些运算符的重载必须定义为成员函数,也就是一个的操作数已经确定了。比如 = (赋值运算符)为二元运算符,应该定义在左侧类当中。参数就只有一个右侧操作数即可,因为左侧操作数就是this指针所指向的。
- 如果不定义 拷贝赋值运算符的话,系统自动合成一个拷贝赋值运算符。
- 在 = 赋值初始化的时候,使用的确实是 拷贝构造函数 或者 移动构造函数(当右侧为右值的时候)。
在 = 进行赋值(不是初始化)的时候,使用拷贝赋值运算符
-
析构函数
- 析构函数没有参数,不可以被重载,不过也可以进行重写,然后定义自己的退出策略。重写只会改变函数体,不会改变析构部分。
- 析构函数分两部分,一部分是函数体,一部分是析构部分。析构部分是隐式的,析构部分按照类中成员出现的顺序的***逆序*** 进行析构。对于类成员,调用其析构函数,对于内置成员,直接释放空间。但是注意:
对于内置指针,不会释放其所指向的空间,因此建议使用智能指针来替代内置指针
- 调用析构函数的时机
- 对于普通的类对象来说,当离开作用域的时候,会调用析构函数
- 对于动态分配的对象,当使用delete运算符的时候,就会调用析构函数
- 对于临时对象,常见他的完整表达式结束,就被销毁
- 什么时候需要自定义析构函数?
- 当数据成员中有指针指向的 动态内存 成员的时候,需要有自定义的析构函数。因为默认的析构函数不会执行delete操作,会造成内存泄漏。
- 此时,不可以使用合成的拷贝构造函数和拷贝赋值运算符,因为会导致浅拷贝, 多个对象指向同一块内存区域,当销毁的时候,会delete多次,造成错误。
-
使用default 和 delete
- 对于编译器可以自动生成的函数,我们在函数声明之后,
;
之前,使用 = default 来要求编译器生成合成版本。 - 对于有些函数,我们不定义的话,编译器自动生成。但是这样的函数是没有意义的,使用的话,是错误的。我们使用=delete 来进行删除。表示不希望使用这个函数。
- delete有时也会由编译器来标注,因为某一个函数不符合运行条件。比如一个类的成员的析构函数为删除的或者不可访问的,那么这个类的合成析构函数也是删除的。
- 对于编译器可以自动生成的函数,我们在函数声明之后,
-
拷贝控制和资源管理
- 类可以分为 行为像值 和 行为像指针。像值的可以在拷贝的时候,重新分配一个值,拷贝的两者之间(拷贝和被拷贝)是互不影响的。像指针的则是共享数据的。
- 通常 资源管理 类更需要拷贝控制,
- 在第13.2章中有一些例子讲解,讲类的设计。有需要的时候可以看看。
-
对象移动
-
对象拷贝是很普遍的操作,但是在某些场景下,拷贝完之后就立即删除了,那就很不划算。使用移动就好很多。而有些类不共享,不可以拷贝,只可以移动。比如unique_ptr这种。因此,移动也是需要的。
因此,很多类是不仅支持拷贝,而且支持移动。 -
右值引用:
- 表达式就是由 运算符和运算对象组成,可以进行计算,最后得到结果的一个式子。常量和变量就是最简单的表达式。
- 左值和右值是表达式的属性。左值可以位于赋值语句的左侧,右值不可以。左值使用的是值的“身份”,也就是值在内存中的位置,在汇编中会将其替换为地址;右值是值的本身。左值一般是变量,而且是非const变量。其余的可以认为是右值。右值往往是用完就不再可以找得到的,因为其大多数时候不是变量(除了const),是短暂的。左值是长久的。
- 表达式会要求左值或者右值,返回一个左值或者右值。
比如: = 运算符要求左侧是一个左值,得到的结果也是一个左值。 - 引用分为左值引用和右值引用。左值引用使用type &来标志,右值引用使用 type &&来标志。非const的左值引用只能引用一个左值。对于右值(常量等)可以使用const左值引用。
而右值引用只可以引用右值,不可以直接引用左值。
右值引用本身是一个左值。 - move库函数可以将一个左值变成右值,从而完成右值引用的绑定。但是这个左值从此不可以再使用了。
-
移动构造函数 和 移动赋值运算符
- 移动构造函数的形参是一个 右值引用,因此要求实参是一个右值。参数和拷贝构造函数类似,只可以有一个右值引用形参,如果有其他的参数的话,需要有默认值。
移动构造函数其实就是将资源进行一个 “抢夺”。右值原来的资源需要进行一些设置(比如将指针设置为nullptr),防止在调用析构函数的时候造成错误(对于临时对象来说,其语句结局就执行析构函数)。 - 移动构造函数相比于 拷贝构造的函数的区别在于
- 移动构造函数的调用者会接管实参的资源,不需要重新申请资源。在拷贝构造函数中,调用者可能还会申请资源
- 实参的资源被完全掠夺,从此不可再用。而拷贝构造函数的实参没有变化
- 移动构造函数和 移动赋值运算符 只是在没有 自定义的拷贝构造函数和拷贝赋值运算符,析构函数。并且所有的非static成员是可以移动的。(可以移动的元素包括内置类型,如果类类型可以移动,那么是可以移动的)
- 移动构造函数的形参是一个 右值引用,因此要求实参是一个右值。参数和拷贝构造函数类似,只可以有一个右值引用形参,如果有其他的参数的话,需要有默认值。
-
如果没有 移动构造函数和移动赋值运算符,那么即使在参数为右值的情况下,也会调用拷贝构造函数。拷贝构造函数可以应对参数为右值的情况。但是如果有合成的或者定义的移动构造函数或者移动赋值运算符,那么当参数为右值的时候,会优先使用移动构造函数或者移动赋值运算符。
-
右值引用和成员函数
-