拷贝控制
本章笔记将记录,类在控制拷贝,赋值、移动和销毁的时候都干些什么。学习的重点主要关注以下五个与拷贝控制有关的特殊的成员函数:
-
拷贝构造函数
-
拷贝赋值运算符
-
移动构造函数
-
移动赋值运算符
-
析构函数
拷贝控制操作的定义:当类需要进行构造,赋值,析构的时候需要进行的操作叫做拷贝控制操作。
一、 拷贝、赋值和销毁
本小节主要关注拷贝构造函数,拷贝赋值运算符和析构函数的相关特性,以及使用这些拷贝控
制操作的时机。当我们没有显式的定义这5个特殊的拷贝控制函数时,编译器时常会隐式的帮我
们定义好这些拷贝控制函数,但是有时候这些编译器自定义的拷贝控制函数并不是我们所需要
的,使用它们将会导致严重的后果。以下将分别学习如何定义类的拷贝,赋值和销毁以及什么
时候去定义这些函数。
(一)拷贝构造函数
- 拷贝构造函数的形式:
class Foo
{
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
};
-
形参和返回值必须时引用类型:
①、传值调用时:
调用 Foo(const Foo )–>发生拷贝初始化–>再次调用Foo(const Foo)–>
再次发生拷贝初始化…无限循环。②、非引用类型返回值时:
返回时发生拷贝初始化–>调用拷贝控制函数–>返回值时再次发生拷贝初始化…
开始无限循环小结:当非引用传参和非引用返回值时将无限次的递归调用拷贝构造函数本身,这是
不合法的,所以拷贝构造函数必须得是引用类型不能是值的原因。 -
合成拷贝构造函数
- 构造方式:成员类型决定拷贝的方式,编译器从给定的对象中依次的将每个非static成员拷贝到正在创建的对象中。
< 类类型>:使用类的拷贝构造函数拷贝
< 数组>:我们不能直接拷贝整个数组,但是会逐个元素的拷贝一个数组类型的成员
< 内置类型>:直接拷贝
- 拷贝初始化及其限制
-
拷贝初始化和直接初始化的区别
直接初始化:要求编译器使用普通的函数匹配,选择最匹配的构造函数。
拷贝初始化:将右侧运算符的对象拷贝到正在创建的对象时,发生拷贝初始化。通过拷贝构造函数或者移动构造函数完成。 -
拷贝初始化发生的时机
①、使用=定义变量时
②、将一个对象作为实参传递给一个非引用类型的形参
④、从一个返回类型为非引用类型的函数返回一个对象时
⑤、用花括号列表初始化一个数组中的元素或者一个[[聚合类]]中的成员
⑥、某些类类型对它们所分配的对象使用拷贝初始化时 -
拷贝初始化的限制
当一个拷贝构造函数被explicit修饰为显式时,只能使用直接初始化。
-
编译器如何绕过拷贝构造函数
待确认:当能够直接初始化时,编译器可以绕过拷贝/移动构造函数。当拷贝/移动构造函数必须存在且可以访问。
(二)拷贝赋值运算符
-
拷贝赋值运算符的形式
Foo& operator=(const Foo&);
- 赋值运算符本质: 是一个Foo& operator=(const Foo&)函数,包含一个参数列表和一个返回类型。
- 要求:
- 对于形参:接受与目标同类型的引用;
- 返回值:返回指向其左侧运算对象的引用;
- 标准库:要求保存在容器中的类型具有赋值运算符,且返回值为引用类型。
-
合成拷贝赋值运算符
-
产生条件:用户未定义自己的拷贝赋值运算符
-
工作机理:将右侧对象的每个非static成员依次赋予左侧运算对象的对应成员。
类类型:调用相应对象自个的拷贝赋值运算符
内置类型:直接赋值
数组:逐个赋值数组成员
-
(三)析构函数
析构函数与构造函数执行相反的操作。构造函数:初始化对象的非static成员;析构函数:释放对象所使用的资源,并销毁非static数据成员。析构函数是唯一的,不接受参数,不能被重载。
-
析构函数形式
~Foo(){};
-
析构函数作用:
- 释放对象资源,销毁所有的非static数据成员
- 析构函数首先执行函数体,然后销毁数据成员。成员按照初始化顺序逆序销毁。
- 隐式销毁一个指针时
-
析构函数使用条件
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论时标准容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,delete该对象时,会被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
-
析构函数的执行
- 先执行函数体,再销毁数据成员。ps: 函数体自身并不能直接销毁成员。
- 析构函数体时作为成员销毁步骤之外的另一部分进行的
-
合成析构函数
当一个类的析构函数没有被显式的定义时,编译器回自动合成一个析构函数。
(四)三/五法则
- 需要析构函数的类也需要拷贝和赋值操作。
如果需要自己定义析构函数,那么涉及动态内存分配。对于拷贝操作就需要定义自己特定行
为的拷贝构造函数和拷贝赋值运算符。
- 需要拷贝操作的类也需要赋值操作,反之亦然。
无论需要拷贝构造函数还是拷贝赋值运算符,如果类的成员可以安全的销毁,我们就不一定
需要定义自己的析构函数了。
(五) =default关键字
- 在拷贝控制成员后边加=default,显示的要求编译器生成合成版本
- 合成的函数隐式的声明为内联函数,如果不希望生成内联函数,则可以在类外定义使用=default。
- 只能对具有合成版本的成员函数使用=default(默认构造函数和拷贝控制成员)
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const Sales_data&) = default; // 默认为内联函
~Sales_data()=default;
Sales_data& operator=(const Sales_data&); // 希望不被声明为内联
private:
...
}
//类外定义=default,拷贝运算符不会是内联函数了
Sales_data& Sales_data::operator=(const Sales_data&) = default;
(六) 阻止拷贝(=delete)
某些类不需要拷贝构造函数和拷贝赋值运算符,此时需要某些机制阻止编译器自动合成拷
贝控制操作。
如iostream: 阻止拷贝,避免多个对象写入或读取相同的IO缓冲。
-
定义删除函数(= delete):
- 关键字=delete: 告诉编译器,不需要合成相应的操作。
- 必须使用在被删除函数,第一次声明时
-
=delete和=default的区别
- =delete必须出现在函数第一次声明的时候,而=default不需要
=default: 一个默认成员只影响为这个成员而生成的代码,因此=default直到编译器生 成代码的时候才需要 =delete: 编译器需要实现直到一个函数时删除的,以便禁止试图使用它的操作。
- =delete可以对所有的成员函数使用,而=default之能应用于拷贝控制函数
=delete:对所有成员函数使用,其意义- 可以引导函数匹配的过程
-
析构函数不能是删除的成员
- 原因:如果析构函数被删除了,就无法销毁此类型的对象了。
- 编译器: 不允许定义该类型的变量或 创建该类的临时对象,因为无法被释放
- 动态分配:虽然定义没有析构函数的变量,但是可以动态分配此类型,就是无法释放该对象。
-
合成拷贝控制成员被删除的四个情景
如果一个类有数据成员不能默认构造,拷贝,赋值或销毁,则对应的成员函数被定义成删除的。- 类中某成员析构函数是删除或不可访问 --> 该类的合成析构函数是删除的, 合成拷贝构造函数也是删除的
- 类中某成员拷贝构造函数是删除或不可访问–> 该类的合成拷贝构造函数也是删除的
- 类中某成员的拷贝赋值运算符是删除或不可访问 / 有成员是const或引用& -->类的合成拷贝运算符是被定义为删除的
- 类内某个成员的析构函数是删除或不可访问, 或有一个引用成员或const成员,这两个成员都没有类内初始化器且其类型为显示的定义默认构造函数–>那么默认构造函数被定义为删除的。
-
private拷贝控制
在新标准前,为了阻止发生拷贝,一般将拷贝构造函数和拷贝赋值函数声明为private,这样两者就不可以被用户访问了。
- 阻止拷贝:声明为private,以及不定义
虽然拷贝构造函数和拷贝赋值运算符被声明为private的,但是还是可以被友元和其他成员函数访问,而产生拷贝行为,因此我们不去定义拷贝构造函数和拷贝赋值函数
- 编译器:
1、试图拷贝对象的用户代码在编译阶段被标记为错误 2、成员函数或友元函数中的拷贝操作回导致链接时错误
- 在新标准下,阻止拷贝的类应该使用=delete声明