您的位置:首页 > 其它

你不知道的拷贝、赋值、移动构造函数

2018-01-24 21:24 369 查看
前言:

当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。拷贝和移动构造函数定义了当用同类型的而另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。这些操作都是拷贝控制操作。

拷贝构造函数

1.定义:如果一个构造函数的第一个参数是自身类类型的引用(你想想,如果参数不是引用类型,则为了调用拷贝构造函数,必须要拷贝实参,但为了拷贝实参,又需要定义拷贝构造函数,无限循环),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。

2.认清直接初始化和拷贝初始化

string s1(10, 'a');//直接初始化
string s2(s1);//直接初始化
string s3 = s1;//拷贝初始化
string s4 = "abc";//拷贝初始化
string s5 = string(10, 'a');//拷贝初始化
拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化不仅在使用=定义变量时会发生,还有以下情况:

<1>将一个对象作为实参传递给一个非引用类型的形参

<2>从一个返回类型为非引用类型的函数返回一个对象

<3>用花括号初始化一个数组中的元素或一个聚合类中的成员

3.拷贝初始化的限制

vector<int> v1(10);//正确。直接初始化
vector<int> v2 = 10;//错误。接受大小参数的构造函数是explicit(能够抑制函数定义的隐式转换)
void f(vector<int>);
f(10);//错误。不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10));//正确。从一个int直接构造一个临时vector
注意:vector的接受单一大小参数的构造函数是explicit的。

拷贝赋值运算符

接受一个本类型对象的赋值运算符版本。通常,拷贝赋值运算符的参数是一个const的引用,并返回指向本对象的引用。如果类未显示定义拷贝赋值运算符,编译器会自动合成一个。

阻止拷贝

1.大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符。但对于某些类来说,定义类时必须采用某种机制阻止拷贝或赋值。比如:iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。通过不定义拷贝控制成员是不能够阻止的,因为编译器会生成合成的版本。

2.我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数就是声明了一个函数,但不会以任何方式使用它们。在函数的参数列表后面加上=delete来定义删除的函数。

A(const A&) = delete;//阻止拷贝
A &operator=(const A()) = delete;//阻止赋值
3.我们不能删除析构函数,因为如果析构函数被删除,就无法销毁此类型的对象。对于一个删除了析构函数的类型,编译器不允许定义该类型的变量或创建该类的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。

本质上,如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

对象移动

1.新标准有一个主要的特性就是可以移动而非拷贝对象。在某些情况下,对象拷贝完成之后源对象就立即被销毁,这时,移动而非拷贝对象就会大幅度提升性能。

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

2.右值引用

<1>为了支持移动操作,新标准引入了一种新的引用类型--右值引用。所谓右值引用就i是必须绑定到右值的引用。通过&&来获得右值引用。右值引用的重要性质就是只能绑定到一个将要销毁的对象。

int i = 24;
int &&r1 = i;//错误。不能将右值引用绑定到左值上
int &&r2 = i * 24;//正确。可以将右值引用绑定到返回右值的表达式上
int &&r3 = r2;//错误。不能将右值引用绑定到一个变量上,即使这个变量是右值引用类型也不行


<2>右值引可以自由地接管所引用对象的资源。因为右值引用指向将要销毁的对象,所以,可以从绑定右值引用的对象“窃取”状态。

3.标准库move函数

虽然不能将一个 右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。通过标准库函数move可以实现(定义在头文件utility中)。

int i = 24;
int &r = i;
int &&r1 = move(r);
4移动构造函数(从给定对象窃取资源)

<1>移动构造函数的第一个参数是该类类型的一个引用,该引用是一个右值引用。其它额外的参数都必须有默认实参。

<2>与拷贝构造函数不同,移动构造函数不分配任何新内存,它接管源对象的内存。在接管内存之后,将源对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作。

class A
{
public:
A(A &&a) noexcept//移动操作不应抛出任何异常
: s1(a.s1), s2(a.s2), s3(a.s3)//成员初始化器接管a中的资源
{
a.s1 = a.s2 = a.s3 = nullptr;//进入安全状态,可以重新赋值和销毁
}
private:
string *s1;
string *s2;
string *s3;
};


5.移动赋值运算符

class A
{
public:
A& operator=(A &&a)	 noexcept//移动操作不应抛出任何异常
{
if (this != &a)
{
free(this);
s1 = a.s1;
s2 = a.s2;
s3 = a.s3;
a.s1 = a.s2 = a.s3 = nullptr;//进入安全状态,可以重新赋值和销毁
}
}
private:
string *s1;
string *s2;
string *s3;
};
注意:

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

定义了移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被认为删除的。

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

class A
{
public:
A() = default;
A(const A& a)
{
s = a.s;
}
private:
string s;
};

int main()
{
A a1;
A a2(a1);//调用拷贝构造函数
A a3(move(a1));//move(a1)返回一个绑定到a1的A&&,此时会将其转换为一个const A&,所以,是调用A的拷贝构造函数
system("pause");
return 0;
}
上面的类A有一个拷贝构造函数但未定义移动构造函数,此时,编译器不会合成移动构造函数。如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐