13-拷贝控制

2016-12-26  本文已影响0人  龙遁流

13.1 拷贝,赋值与销毁

以上这些操作,必须明白定义与不定义会对类的操作产生何种影响,变编译器定义的合成版本未必符合类设计的初衷。

13.1.1 拷贝构造函数

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

必须是引用类型参数,因为在调用非引用参数的函数时,会拷贝实参,而拷贝实参又需要调用拷贝构造函数,那么会无休止的调用下去。

Foo(const Foo&);

合成拷贝构造函数会将其参数的成员(非static)逐个拷贝到正在创建的对象中。

直接初始化:使用普通的函数匹配,选择参数最匹配的构造函数。

拷贝初始化:将对象或者可以转换为相同类型的对象拷贝到正在创建的对象中。

1,使用=运算符定义变量

2,将对象作为实参传递给一个非引用类型

3,从非引用类型返回类型的函数里返回一个对象

4,使用初始值列表初始化一个数组中的元素或一个聚类中的成员

13.1.2 拷贝赋值运算符

Foo& operator=(const Foo&);

如果运算符是一个成员函数,其左侧对象就绑定到隐式的this参数。

合成拷贝赋值运算符:将右侧运算对象的每个成员(非static)赋予左侧运算对象的对应成员

13.1.3 析构函数

~Foo();

在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照在类中出现的顺序进行的;而在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序逆序销毁。

当一个对象被销毁时会自动调用其析构函数。当指向一个对象的引用会指针离开作用域是,析构函数不会执行。

合成析构函数:空的函数体

13.1.4 三/五法则

如果一个类需要自定义析构函数(一般是销毁动态分配的内存),则可能也需要拷贝构造函数和拷贝赋值运算符。

需要拷贝操作的类也需要赋值操作,反之亦然,但未必需要析构函数。

13.1.5 使用=default

使用=default修饰拷贝控制成员,编译器将生成相应成员的合成版本。=default在类内则隐式的声明为内联。

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

Foo(const Foo&) =default;

只能用来修饰具有合成版本的成员函数。

13.1.6 阻止拷贝

将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,即,虽然声明了,但是不可以使用它们。

Foo(const Foo&) = delete;//阻止拷贝

Foo& operator=(const Foo&) = delete;//阻止赋值

=delete必须出现在第一次声明的时候;可以对任何函数指定=delete

对于删除了析构函数的类型,不可以定义此类型的变量或成员,但可以动态分配这种类型,但是无法释放它们。

合成的拷贝控制成员可能是删除的

如果类有数据成员不能被默认构造,拷贝,赋值,复制或销毁(delete或者private修饰),则此类对应的合成成员函数被定义为删除的。(引用成员或无法默认构造的const成员)

通过声明但不定义private的拷贝构造函数或拷贝赋值运算符,试图拷贝或赋值的操作在编译阶段被标记为错误;成员函数或友元函数中的拷贝或赋值会导致链接错误。(旧的方法)

13.2 拷贝控制和资源管理

管理类外的类必须定义拷贝控制成员。

13.2.1 行为像值的类

对于类管理的资源,每个对象都有一份拷贝。

赋值操作会销毁左侧运算对象的资源,之后从右侧运算对象拷贝数据,必须确保自赋值是异常安全的(可先将右侧运算对象的数据拷贝到零时对象中)。

13.2.2 行为像指针的类

多个此类的对象共享同一份数据(使用智能指针或引用计数管理)

13.3 交换操作

void swap(Foo &lhs, Foo &rhs){

using std::swap;//若没有类型自定义的swap版本,则调用std的版本

swap(lhs.h, rhs.h);//类型自定义的版本

}

拷贝并交换技术

Foo& Foo::operator=(Foo rhs){

swap(*this, rhs);

return *this;

}

参数并不是引用,在函数执行完后会释放。可自动处理自赋值的情况,且是异常安全的。

13.6 对象移动

当一个对象拷贝后就立即销毁,此时使用移动操作可以提高性能。

标准库容器,string,shared_ptr类支持移动和拷贝,IO类和unique_ptr类只支持移动。

13.6.1 右值引用

即,必须绑定到右值的引用。只能绑定到将要销毁的对象,故可以将右值引用的资源移动到另一个对象中。

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

int &&r = i*42;

返回左值引用的函数,连同赋值,下标,解引用和前置递增递减运算符,都返回左值;

返回非引用类型的函数,连同算数,关系,位以及后置递增递减运算符,都生成右值,可以用const的左值引用和右值引用绑定。

左值有持久的状态,右值要么是字面值常量,要么是表达式求值过程中创建的临时变量。

右值意味着:该对象将被销毁;该对象没有其他用户。故使用右值引用的代码可以自由的接管引用的对象的资源。

int &&rr1 = 42;

int &&rr2 = rr1;//错误:rr1是右值引用类型的变量,是左值

#include <utility>

int &&rr3  =std::move(rr1);//move函数获得绑定到左值上的右值引用。

move意味着希望像处理右值一样处理一个左值,即,除了对rr1赋值和销毁外,代码不会再使用它。

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

从给定对象“窃取”而不是拷贝资源。

移动拷贝构造函数第一个参数是该类类型的右值引用,其他的参数必须都具有默认实参。

StrVec::StrVec(StrVec &&s) noexcept:e(s.e), f(s.f){s.e = s.f = nullptr;}

1,移动操作不应该抛出异常,noexcept通知标准库移动操作是安全的的,无需标准库做额外的操作

2,成员初始化器中接管s中的资源

3,函数体中使对s进行析构是安全的

noexcept在声明和定义中都需要指定。

StrVec &StrVec::operator=(StrVec &&rhs) noexcept{}

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

当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会合成移动构造函数或移动赋值运算符。

可以移动:内置类型,类类型定义了相应的移动操作。

移动操作不会隐式的定义为删除的函数,但显式定义=default的移动操作,且编译器不能移动所有成员,则编译器将移动操作定义为删除的函数。

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

如果一个类既有移动构造函数,又有拷贝构造函数,则使用普通的函数匹配规则来选择调用。如果没有移动构造函数或移动赋值运算符,右值会被拷贝。

移动迭代器

移动迭代器的解引用运算符生成一个右值引用,make_move_iterator(origin_iterator);函数将普通的迭代器转换为一个移动迭代器,会调用相应的西东构造函数或移动赋值运算符操作。

13.6.3 右值引用和成员函数

成员函数提供移动版本:

void push_back(const X&);//拷贝

void push_back(X&&);//移动

引用限定符

Foo &operator=(const Foo&) &;//只能向可修改的左值赋值

引用限定符可以是&和&&,分别指出this可以指向一个左值或右值,只能用于非static的成员函数,且必须同时出现在声明和定义中。

一个函数可以同时用const和引用限定,但引用限定符必须跟随在const之后。

引用限定符可以区分重载版本,并且可以和const综合起来区分。

当定义多个具有相同名字和相同参数列表的成员函数时,必须所有函数都加上引用限定符或都不加。

上一篇下一篇

猜你喜欢

热点阅读