当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)、拷贝赋值运算符(copy-assignment operator)、移动构造函数(move constructor)、移动赋值运算符(move-assignment operator)和析构函数(destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作(copy control)。
1,拷贝构造函数——如果没有为一个类定义拷贝构造函数,编译器会为我们自动定义一个,称之为合成拷贝构造函数
class Foo {
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
}
string dots(10,‘.‘); //直接初始化
string s(dots); //直接初始化
string s2=dots; //拷贝初始化
string null_book="9-999-99999-9"; //拷贝初始化
string nines=string(100,‘9‘); //拷贝初始化
当使用直接初始化时,编译器使用普通函数匹配来选择与我们提供的参数最匹配的构造函数,当我们使用拷贝初始化时,编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
2,拷贝赋值运算符——合成拷贝
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
}
3,析构函数——合成析构函数不会delete一个指针数据成员
class Foo{
public:
~Foo(); //析构函数
}
三/五法则
1) 需要析构函数的类也需要拷贝和赋值操作
2) 需要拷贝操作的类也需要赋值操作,反之亦然。
使用default:当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,而对成员的类外定义使用=default,就像对拷贝赋值运算符所做的那样。
使用delete阻止拷贝和赋值,不能删除析构函数。
private拷贝控制,将其拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝。将拷贝控制成员声明为private,但不定义它们。
拷贝控制和资源管理:通常,管理类外资源的类必须定义拷贝控制成员。拷贝语义:1,类的行为像一个值;2,类的行为像一个指针,共享状态。
1,类的行为像一个值
Hasptr需要:
1,定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
2,定义一个析构函数来释放string
3,定义一个拷贝赋值运算符来释放对象当前的string,并从右侧运算对象拷贝string
class Hasptr{
public:
Hasptr(const std::string &s = std::string()):
ps(new std::string(s)), i(0){}
Hasptr(const Hasptr &p) :ps(new std::string(*p.ps)), i(p.i){}
Hasptr& operator=(const Hasptr &);
~Hasptr(){ delete ps; }
private:
std::string *ps;
int i;
};
Hasptr& Hasptr::operator=(const Hasptr &rhs)
{
auto newp = new string(*rhs.ps); //拷贝底层string,自赋值操作
delete ps;
ps = newp;
i = rhs.i;
return *this;
}
2,定义行为像指针的类
对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。我们的类仍然需要自己的析构函数来释放接受string参数的构造函数的内存。但是,析构函数不能单方面地释放关联的string。只有当最后一个指向string的Hasptr销毁时,它才可以释放string。
定义一个使用引用计数的类
class Hasptr{
public:
Hasptr(const std::string &s = std::string()):
ps(new std::string(s)), i(0),use(new std::size_t(1)){}
Hasptr(const Hasptr &p) :ps(new std::string(*p.ps)), i(p.i), use(p.use){ ++*use; }
Hasptr& operator=(const Hasptr &);
~Hasptr(){ }
private:
std::string *ps;
int i;
std::size_t *use; //用来记录有多少个对象共享*ps的成员
};
类指针的拷贝成员“篡改”引用计数
Hasptr::~Hasptr()
{
if (--*use == 0){
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
Hasptr& Hasptr::operator=(const Hasptr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
if (--*use == 0){
delete ps; //如果没有其他用户,释放本对象分配的成员
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
交换操作——swap,交换两个元素,swap函数应该调用swap,而不是std::swap
class Hasptr{
friend void swap(HasPtr&, HasPtr&);
}
_inline void swap(Hasptr &lhs, Hasptr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps); //交换指针,而不是string数据
swap(lhs.i, rhs.i); //交换int成员
}
每个swap调用应该都是未加限定的。即,每个调用都应该是swap,而不是std::swap。如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本。同样,在赋值运算符中使用swap
//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的string拷贝到rhs
Hasptr& Hasptr::operator=(const Hasptr &rhs)
{
swap(*this,rhs); //rhs现在指向本对象曾经使用的内存
return *this; //rhs被销毁,从而delete了rhs中的指针
}
对象移动:1,在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。2,使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类,这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动。
一,右值引用(rvalue reference)。通过&&而不是&来获得右值引用,右值只能绑定到一个将要销毁的对象。左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。右值所引用的对象将要被销毁,该对象没有其他用户。使用右值引用的代码可以自由地接管所引用的对象的资源。
int i=42;
int &r=i; //正确:r引用i
int &&rr=i; //错误:不能将一个右值引用绑定到一个左值上
int &r2=i*42; //错误:i*42是一个右值
const int &r3=i*42; //正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2=i*42; //正确;将rr2绑定到乘法结果上
标准库move函数,定义在头文件utility。
int &&rr3=std::move(rr1); //ok
move告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
移动构造函数和移动赋值运算符,我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
class StrVec{
public:
StrVec(StrVec&&) noexcept; //移动构造函数
}
StrVec::StrVec(SteVec &&s) noexcept
: elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//令s进入这样的状态—对其运行析构函数是安全的
s.elements=s.first_free=s.cap=nullptr;
}
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
//直接检测自赋值
if(this !=&rhs){
free(); //释放已有元素
elements=rhs.elements; //从rhs接管资源
first_free=rhs.first_free;
cap=rhs.cap;
//将rhs置于可析构状态
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
只有当一个类没有定义任何版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器才能移动这个成员:
//编译器会为X和hasX合成移动操作
struct X{
int i; // 内置类型可以移动
std::string s; //string定义了自己的移动操作
}
struct hasX{
X mem; //X有合成的移动操作
}
X x,x2=std::move(x); //使用合成的移动构造函数
hasX hx,hx2=std::move(hx); //使用合成的移动构造函数
另外,
hasY(hasY&&)=default; //hasY将有一个删除的移动构造函数
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作的情况类似。
1,移动右值,拷贝左值
2,如果没有移动构造函数,右值也被拷贝
原文地址:http://blog.csdn.net/qqlu_did/article/details/46315697