拷贝控制

文章目录
  1. 1. 拷贝、赋值、销毁
    1. 1.1. 拷贝构造函数
      1. 1.1.1. 合成拷贝构造函数
      2. 1.1.2. 拷贝初始化
      3. 1.1.3. 参数和返回值
      4. 1.1.4. 拷贝初始化的限制
    2. 1.2. 拷贝赋值运算符
    3. 1.3. 析构函数
    4. 1.4. 三五法则
    5. 1.5. 使用=default
    6. 1.6. 阻止拷贝
      1. 1.6.1. 定义删除的函数
      2. 1.6.2. 析构函数不能是删除的
      3. 1.6.3. 合成的拷贝成员可能是删除的:
      4. 1.6.4. private控制
  2. 2. 拷贝控制和资源管理
    1. 2.1. 行为像值的类
    2. 2.2. 定义行为像指针的类
  3. 3. 交换操作
    1. 3.1. swap函数应该调用swap,而不是std::swap
    2. 3.2. 在赋值运算符中使用swap
  4. 4. 拷贝控制示例
  5. 5. 动态内存管理类
  6. 6. 对象移动
    1. 6.1. 右值引用
    2. 6.2. 标准库move函数
    3. 6.3. 移动构造函数和移动赋值运算符
      1. 6.3.1. 移动操作、标准容器和异常
      2. 6.3.2. 移动赋值运算符
      3. 6.3.3. 移动对象必须可析构
      4. 6.3.4. 合成的移动操作
      5. 6.3.5. 移动右值,拷贝左值
      6. 6.3.6. 如果没有移动构造函数,右值也被拷贝
      7. 6.3.7. 移动迭代器
    4. 6.4. 右值引用和成员函数
      1. 6.4.1. 重载和引用函数
  1. 拷贝、赋值、销毁
  2. 拷贝控制和资源管理
  3. 交换操作
  4. 拷贝控制示例
  5. 动态内存管理
  6. 对象移动

我们将学到:拷贝、赋值、移动、销毁做什么?下面是拷贝操作的几个函数:

  1. 拷贝构造函数
  2. 拷贝赋值运算符
  3. 移动构造函数
  4. 移动赋值运算符
  5. 析构函数

函数类–初始化:拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么
运算符类—赋值:拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象是做什么。

如果一个类没有定义所有的拷贝控制成员,编译器会自动定义缺失的操作。

拷贝、赋值、销毁

拷贝构造函数

如果一个函数的第一个参数是自身类型的引用,且任何额为的参数都有默认值,则,此构造函数是拷贝构造函数。

1
2
3
4
5
class Foo
{
plublic:
Foo(canst Foo&); // 拷贝构造函数
};

合成拷贝构造函数

如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。这个拷贝构造函数称为合成拷贝构造函数。合成的拷贝构造函数会将其参数逐个拷贝到正在创建的对象中。除了静态成员。

合成拷贝构造函数如何拷贝:

  1. 类类型:使用其拷贝构造函数来拷贝
  2. 内置类型:直接拷贝
  3. 数组:逐个拷贝数组成员

例子:

1
2
3
4
5
Sales_data::Sales_data(const Sales_data& orig):
bookNO(orig.bookNO),
units_sold(orig.units_sold),
revenue(orig.revenue)
{}

拷贝初始化

示例:

1
2
3
4
5
string dots(10,'.');  ///直接初始化
string s(dots); ///直接初始化
string s2 = dots; ///拷贝初始化
string null_book = "999999-999" ///拷贝初始化
string nines = string(100,'9'); ///拷贝初始化
  1. 直接初始化:我们实际上要求编译器用普通的函数匹配,来选择与我们提供的参数最匹配的构造函数。
  2. 拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话,还进行类型转换。

,拷贝初始化是通过拷贝构造函数或者移动构造函数完成的。拷贝初始化发生的场景:

  1. 将一个对象作为实参传递给一个非引用类型的形参
  2. 从一个返回类型为非引用类型的函数返回一个对象
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  4. insert 、push(emplace 直接初始化)
  5. 使用=定义变量

参数和返回值

当一个函数具有非引用的返回值类型时,返回值会被用来初始化调用方的结果。为什么拷贝构造函数的参数必须是引用: 如果参数不是引用类型,则调用永远不能成功—为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数。如此无限循环。

拷贝初始化的限制

如果我们的初始值要求通过一个explicit的构造函数来进行类型转换,那么使用拷贝初始化和拷贝初始化就有区别了

1
2
vector<int> v1(10);  ///正确,直接初始化
vector<int> v2 = 10; 错误,接受大小数的构造函数是explicit的。

拷贝赋值运算符

与类控制其对象如何初始化一样,类也可以控制其对象如何赋值。

赋值运算符就是一个名为opertor =的函数,类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表。
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式this参数。

1
Foo& operator=(const Foo &);

赋值运算符通常应该返回一个指向其左侧运算对象的引用。

1
2
3
4
5
6
7
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
booNO = rhs.bookNO;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}

析构函数

  1. 构造函数初始化对象的非static数据成员。
  2. 析构函数释放对象使用的资源,并销毁对象的非静态成员。

析构函数是类的一个成员函数,名字由波浪号接类名构成,没有返回值,不接受参数。,所以不能重载,对于给定的类,只会有唯一一个析构函数。

1
~Foo();
  1. 在构造函数中,成员的初始化是在函数体执行前完成的,且按照他们在类中出现的顺序进行初始化。
  2. 在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。

隐式销毁一个内置指针类型的成员不会delete它所指向的对象。智能指针是类类型,所以具有析构函数,所以销毁的时候会销毁其指向的对象。

什么时候会调用析构函数:

  1. 变量在离开其作用域时
  2. 对象被销毁,其成员被销毁
  3. 容器被销毁,其成员被销毁
  4. 动态分配的对象,delete 运算符时会被销毁
  5. 对于临时对象,当创建他的完整表达式结束时被销毁
析构函数自身并不直接销毁成员,成员实在析构函数体之后隐含的析构阶段中被销毁的

三五法则

  1. 需要析构函数的类也需要拷贝和赋值。
  2. 需要拷贝操作的类也需要赋值操作。

使用=default

可以通过将拷贝控制成员定义为=default来显示的要求编译器生产合成的版本。合成函数将隐式的声明为内联的,如果不希望合成的成员函数是内联函数,应该只对成员的类外定义使用=default。

我们只能对具有合成版本的成员函数函数使用=default。

阻止拷贝

应用场景:iostream类阻止了拷贝,以避免多个对象写入或读取相同的io缓存。

定义删除的函数

我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数,来阻止拷贝。删除函数是这样一种函数:我们虽然声明了他们,单不能以任何方式使用他们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的函数。

=default 、=delete区别:

  1. =delete必须在函数第一次声明的时候。=default直到编译器生产代码的时候才需要。
  2. 可以对任何函数指定=delete,只能对编译器可以合成的默认构造函数或者拷贝控制成员使用=default。

析构函数不能是删除的

析构函数不能是删除的,如果析构函数被删除了,就无法消化此类型的对象了。对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或者创建该类的临时变量。如果一个类的某个成员的类型删除了析构函数,我们不能定义该类的变量或者临时对象。因为如果一个成员的析构函数是删除的,则改成员无法被销毁,而如果一个成员无法被销毁,则对象整体也就无法被销毁了。

对于析构函数已删除的类型,不能定义改类型的变量或释放指向改类型动态分配对象的指针。

对于删除了析构函数的类型,虽然不能定义这种类型的变量或者成员,但可以动态分配这种类型的对象。但是不能释放这些对象。

合成的拷贝成员可能是删除的:

  1. 析构函数:如果类的某个成员的析构函数是删除的或不可访问的,则类的合成析构函数被定义为删除的
  2. 拷贝构造函数:如果类的某个成员的拷贝构造函数是删除的或者不可访问的,则类的合成拷贝构造函数被定义为删除的,如果类的某个成员的析构函数是删除的或者不可访问的,则类合成的拷贝构造函数也被定义为删除的。
  3. 拷贝赋值运算符:如果类的某个成员的拷贝赋值运算符是删除的,或者不可访问的,或类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。
  4. 构造函数:如果类的某个成员的析构函数是删除的或者不可访问的,或类有一个引用成员,他没有类内初始化器,或是类有一个const成员,它没有类内初始化器且类未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。

如果一个类有数据成员不能默认构造、拷贝、赋值、销毁。则对应的成员函数将被定义为删除的。

一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义为删除的。原因是:如果没有这条规则,我们可能创建出无法销毁的对象。

对于据用引用成员或者无法默认构造的const成员的类。编译器不会为其合成默认构造函数。如果一个类有const成员,则他不能使用合成的拷贝赋值运算符,毕竟,此运算符试图赋值所有成员,而将一个新值赋予一个const对象是不可能的。虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,而不是引用本身。这种行为看起来不是我们期望的,因此对于有引用成员的类,合成拷贝赋值运算符被定义为删除的。

private控制

就版本使用private来阻止拷贝。

拷贝控制和资源管理

管理类外资源的类必须定义拷贝控制函数。

  1. 类值拷贝:意味着类有自己的状态。当我们拷贝一个像值的对象时,副本和原对象完全独立的,改变副本不会对原对象有任何印象。
  2. 类指针拷贝:副本和原对象使用相同的底层数据,改变副本也会改变原对象。

行为像值的类

赋值运算符通常组合了析构函数和构造函数的操作:

  1. 类型析构函数,赋值操作会销毁左侧运算对象的资源。
  2. 类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
1
2
3
4
5
6
7
8
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); //拷贝底层String
delete ps; //是否旧内存
ps = newp;
i=rhs.i;
return *this; //返回本对象
}

对于一个赋值运算符来说,正确工作非常重要的,即使将一个对象赋予它自身,也要能正确工作。一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。

定义行为像指针的类

引用计数的工作方式:

  • 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝析构函数递增共享的计数器。指出给定对象的状态又被一个新用户共享。
  • 析构函数递减计数器。指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
  • 拷贝赋值运算符递增右侧运算对象的计算器。递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。

简洁版:

  1. 除了拷贝构造函数外,其他的构造函数创建引用计数器,为1
  2. 拷贝构造函数–拷贝计数器,并递增引用计数器
  3. 析构函数递减引用计数器,为0,释放
  4. 拷贝赋值运算符– 递减左侧,递增右侧

交换操作

如果一个类定义了自己的swap,那么算法将使用类自定义版本,否则算法将使用标准定义的swap。

swap函数应该调用swap,而不是std::swap

在赋值运算符中使用swap

拷贝控制示例

虽然通常来说分配资源的类需要拷贝控制,但资源管理并不是一个类需要定义自己拷贝控制成员的唯一原因。一些类也需要拷贝控制成员的帮助来进行其他操作。

动态内存管理类

对象移动

标准容器、string、shared_ptr即支持移动,也支持拷贝;IO类和unique_ptr可以移动,不能拷贝。

右值引用

右值引用:就是必须绑定到右值的引用,通过&&获取右值引用。右值引用有一个重要的特性—只能绑定到将要销毁的对象。因此,我们可以自由的将一个右值引用的资源移动到另一个对象。

一个左值表达式表示一个对象的身份,一个右值表达式表示的是对象的值。

左值持久;右值短暂

  1. 所引用的对象将要销毁
  2. 该对象没有其他用户

上面的特性意味着:使用右值引用可以自由的接管所引用的对象的资源。

右值引用指向将要销毁的对象,因此,我们可以从绑定到右值引用的对象窃取状态。

###变量是左值

变量表达式是左值,带来的结果是,我们不能将右值引用绑定一个右值引用类型的变量上

1
2
int && rr1 = 42;
int &&rr2 = rr1; //错误,表达式rr1是左值

变量时左值,因为变量时持久的,知道离开作用域才被销毁。

变量时左值,因此,我们不能将一个右值引用绑定到一个变量上,即使这个变量时右值引用类型也不行。

标准库move函数

虽然不能将一个右值引用绑定到左值上,但我们可以显示的将一个左值转化为对应的右值引用类型。我们可以通过调用一个名为move的新标准函数来获得绑定到左值上的右值引用。

调用move意味着承诺:除了对rr1赋值或销毁外,我们不再使用它。在调用move后,我们不能对移后源对象的值做任何的假设。

我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

移动构造函数和移动赋值运算符

移动构造函数和移动赋值运算符类似拷贝操作,但他们从给定对象窃取资源,而不是拷贝资源。

类似拷贝构造函数,移动构造函数的第一个参数是该类型的一个引用,不同于拷贝构造函数,这个引用参数在移动构造函数中是一个右值引用,与拷贝构造函数一样,任何额为的参数必须有默认实参。

除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一种状态—销毁它是无害的。一旦资源完成移动,对象就不再指向被移动的资源。—-这些资源的所有权已经归属新创建的对象。

1
2
3
4
StrVec::StrVec(StrVec &&s)noexcept :elments(s.elements),first_free(s.first_free),cpa(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;
}

移动操作、标准容器和异常

我们必须在头文件的声明和定义中都指定noexcept。

移动赋值运算符

移动赋值运算符执行与析构函数和移动构造函数相同的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
StrVec::StrVec::operator=(StrVec && rhs) noexcept
{
if(this != rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}

return *this;
}

处理了自赋值的情况。判断是否是this,我们不能再使用右侧对象的资源之前就释放左侧运算对象的资源。

移动对象必须可析构

除了将移后源对象置位析构安全的状态之外,移动操作还必须保证对象任然是有效状态。一般来说,对象有效是指可以安全的为其赋值,或者可以安全的使用,而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。

在移动操作之后,移后源对象必须保持有效的,可析构的状态,但是用户不能对其值进行任何期望。

合成的移动操作

如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符、析构函数,编译器就不会为他们合成移动构造函数和移动赋值运算符。

只有一个类没有定义任何版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才为它合成移动构造函数和移动拷贝赋值运算符。与拷贝操作不同,移动操作永远不会隐式的定义为删除函数,但是如果我们现实的要求编译器生产=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除函数。

  1. 与拷贝构造函数不同,移动构造函数被定义为删除函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数或者是有类成员未定义自己的拷贝构造函数,且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
  2. 如果类成员的移动构造函数或移动赋值运算符被定义为删除的或者不可访问的,则类的移动构造函数或者移动赋值运算符被定义为删除的
  3. 类似拷贝构造函数,如果了类的析构函数定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
  4. 类似拷贝赋值运算符,如果类成员是const或引用,则类的移动赋值运算符被定义为删除的。

如果一个类定义了移动构造函数和移动赋值运算符,则类的合成拷贝构造函数和拷贝赋值运算符被定义为删除的。

移动右值,拷贝左值

如果没有移动构造函数,右值也被拷贝

移动迭代器

移动迭代器解引用生成右值引用,我么通过标准库的make_move_iterator函数将普通迭代器转换为移动迭代器。次函数接受一个迭代器参数,返回一个移动迭代器。

右值引用和成员函数

我们指出this的左值、右值属性的方式与定义const成员函数相同,即,在参数表后面防止一个引用限定符

1
Foo & operator=(const Foo&) &;

引用限定符必须同时出现在函数的声明和定义中。

重载和引用函数

就像一个成员函数可以根据是否是const来区分其重载版本,引用限定符也可以区分重载版本。