拷贝控制

当定义一个类时,我们显示地或隐示地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数(copy constructor)拷贝赋值运算符(copy-assignment operator)移动构造函数(move constructor)移动赋值运算符(move-assignment operator)析构函数(destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称
这些操作为拷贝控制操作(copy control)
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作的默认定义会导致灾难。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。
示例一:

#include <string>
#include <vector>
#include <iostream>

class Company
{
public:
	Company();
	Company(std::string, int);
	Company(const Company& com);
	~Company();
	Company &operator=(const Company&com);
private:
	std::string m_name;
	int m_road;
};

Company::Company(){
	m_name = "";
	m_road = 0;
	std::cout << "Default Constructor called." << std::endl;
}
Company::Company(std::string name, int road) :m_name(name), m_road(road){
	std::cout << "Parameter Constructor called." << std::endl;
}

Company::~Company(){
	std::cout << "Deconstructor called." << std::endl;
}

Company::Company(const Company&com){
	m_name = com.m_name;
	m_road = com.m_road;
	std::cout << "Copy Constructor called." << std::endl;
}
Company &Company::operator=(const Company&com){
	m_name = com.m_name;
	m_road = com.m_road;
	std::cout << "Overload = called." << std::endl;
	return *this;
}

int main(){
	std::string name = "OmniVision";
	int road = 56;
	Company acom;
	/* 以下两句等价 */
	Company bcom(acom);     /* 此处调用拷贝构造函数 */
	//Company bcom = acom;  /* 此处调用拷贝构造函数 */

	Company ccom(name,road);

	std::cout << "-----------------------------------------" << std::endl;
}

运行的结果是:

ellipse
图1 示例一结果

示例二:

#include <string>
#include <vector>
#include <iostream>

class Company
{
public:
	Company();
	Company(std::string, int);
	Company(const Company& com);
	~Company();
	Company &operator=(const Company&com);
private:
	std::string m_name;
	int m_road;
};

Company::Company(){
	m_name = "";
	m_road = 0;
	std::cout << "Default Constructor called." << std::endl;
}
Company::Company(std::string name, int road) :m_name(name), m_road(road){
	std::cout << "Parameter Constructor called." << std::endl;
}

Company::~Company(){
	std::cout << "Deconstructor called." << std::endl;
}

Company::Company(const Company&com){
	m_name = com.m_name;
	m_road = com.m_road;
	std::cout << "Copy Constructor called." << std::endl;
}
Company &Company::operator=(const Company&com){
	m_name = com.m_name;
	m_road = com.m_road;
	std::cout << "Overload = called." << std::endl;
	return *this;
}

int main(){
	std::string name = "OmniVision";
	int road = 56;
	Company acom;
	Company bcom;  /* 此处调用默认构造函数 */
	bcom = acom;   /* 此处调用重载赋值运算符 */
	Company ccom(name, road);

	std::cout << "-----------------------------------------" << std::endl;
}

运行的结果是:

ellipse
图2 示例二结果

以上两段代码旨在展示创建对象bcom时调用函数的差异。

三/五法则
如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然——如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。

在类中实现自己的swap()函数

#include <iostream>
#include <string>

class HasPtr
{
public:
	HasPtr(std::string *val = new std::string, int i = 0) :m_ps(val), m_i(i){};
	HasPtr(const HasPtr& ahp);  /* 对m_ps指向的string,每个HasPtr对象都有自己的拷贝 */
	HasPtr& operator=(const HasPtr& ahp);
	~HasPtr();
	friend void swap(HasPtr &, HasPtr&);
	int getVal(){ return m_i; }
	std::string getStr(){ return *(this->m_ps); }
private:
	std::string *m_ps;
	int m_i;
};

inline HasPtr& HasPtr::operator=(const HasPtr& ahp){
	std::string astr = *ahp.m_ps;
	delete this->m_ps;  /* 释放旧内存 */
	this->m_ps = new std::string(astr);  /* 从右侧新创建的对象拷贝数据到本对象 */
	this->m_i = ahp.m_i;
	return *this;  /* 返回本对象 */
}

HasPtr::HasPtr(const HasPtr& ahp){
	m_ps = new std::string(*ahp.m_ps);
	m_i = ahp.m_i;
}

HasPtr::~HasPtr()
{
	delete m_ps;
}

inline void swap(HasPtr &lhs, HasPtr&rhs){
	using std::swap;
	/* 下面两个swap()是std::swap(),而不是自己定义的友元函数swap() */
	swap(lhs.m_i, rhs.m_i);  /* 交换int成员 */
	swap(lhs.m_ps, rhs.m_ps);/* 交换指针,而不是string数据 */
}

int main(){
	HasPtr chp(new std::string("OmniVision"), 3);
	HasPtr ahp;
	ahp = chp; /* 此处只会调用拷贝赋值运算符,不会调用拷贝构造函数 */
	HasPtr bhp(new std::string("Himax"), 2);

	/* 交换前的数据 */
	std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
	std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;

	std::cout << "------------------------------------------" << std::endl;

	/* 交换;此处的swap()是自己定义的友元函数,而不是std::swap() */
	swap(ahp, bhp);

	/* 交换后的数据 */
	std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
	std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
}

使用拷贝并交换实现自赋值异常安全的赋值运算符:

ellipse
图1 拷贝并交换

上面的代码可修改为(两段代码只有拷贝赋值运算符部分不同,其它地方完全相同):

#include <iostream>
#include <string>

class HasPtr
{
public:
	HasPtr(std::string *val = new std::string, int i = 0) :m_ps(val), m_i(i){};
	HasPtr(const HasPtr& ahp);  /* 对m_ps指向的string,每个HasPtr对象都有自己的拷贝 */
	HasPtr& operator=(HasPtr aph);
	~HasPtr();
	friend void swap(HasPtr &, HasPtr&);
	int getVal(){ return m_i; }
	std::string getStr(){ return *(this->m_ps); }
private:
	std::string *m_ps;
	int m_i;
};

HasPtr& HasPtr::operator=(HasPtr aph){
	swap(*this, aph);
	return *this;
}

HasPtr::HasPtr(const HasPtr& ahp){
	m_ps = new std::string(*ahp.m_ps);
	m_i = ahp.m_i;
}

HasPtr::~HasPtr()
{
	delete m_ps;
}

inline void swap(HasPtr &lhs, HasPtr&rhs){
	using std::swap;
	/* 下面两个swap()是std::swap(),而不是自己定义的友元函数swap() */
	swap(lhs.m_i, rhs.m_i);  /* 交换int成员 */
	swap(lhs.m_ps, rhs.m_ps);/* 交换指针,而不是string数据 */
}

int main(){
	HasPtr chp(new std::string("OmniVision"), 3);
	HasPtr ahp;
	ahp = chp; /* 此处会先调用拷贝构造函数生成chp的副本(该副本即是拷贝赋值运算符的形参),再调用拷贝赋值运算符用chp的副本给ahp赋值 */
	HasPtr bhp(new std::string("Himax"), 2);

	/* 交换前的数据 */
	std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
	std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;

	std::cout << "------------------------------------------" << std::endl;

	/* 交换;此处的swap()是自己定义的友元函数,而不是std::swap() */
	swap(ahp, bhp);

	/* 交换后的数据 */
	std::cout << "ahp: " << ahp.getStr() << " : " << ahp.getVal() << std::endl;
	std::cout << "bhp: " << bhp.getStr() << " : " << bhp.getVal() << std::endl;
}

上面两段代码的输出都是:

ellipse
图3 程序运行结果

拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符示例

#include <iostream>
#include <utility>
#include <string>

class Ref
{
public:
	Ref(std::string astr = "", int *i = nullptr) :m_str(astr), m_i(i){}
	Ref(const Ref&aref); /* 拷贝构造函数 */
	Ref& operator=(const Ref&aref); /* 拷贝赋值运算符 */
	Ref(Ref&&aref)noexcept; /* 移动构造函数 */
	Ref& operator=(Ref&&aref)noexcept; /* 移动赋值运算符 */
	~Ref();

private:
	std::string m_str;
	int *m_i;
};

/* 拷贝构造函数 */
Ref::Ref(const Ref&aref){
	int *i = new int;
	this->m_i = i;
	memcpy(this->m_i, aref.m_i, 1);
	this->m_str = aref.m_str;
}
/* 拷贝赋值运算符 */
Ref& Ref::operator=(const Ref&aref){
	/* 申请内存,并将aref中指针的值拷贝到i */
	int *i = new int;
	memcpy(i, aref.m_i, 1);
	/* 由于在释放内存前对aref进行了拷贝,这样可以正确处理自赋值了 */
	delete this->m_i;
	this->m_i = nullptr;
	/* 使用aref进行赋值 */
	this->m_i = i;
	this->m_str = aref.m_str;

	return *this;
}
 /* 移动构造函数 */
Ref::Ref(Ref&&aref)noexcept{
	std::cout << "移动构造函数" << std::endl;

	this->m_str = aref.m_str;
	this->m_i = aref.m_i;
	/* 将成员m_i置于nullptr的状态,对aref运行析构函数是安全的 */
	aref.m_i = nullptr;
}
/* 移动赋值运算符 */
Ref& Ref::operator=(Ref&&aref) noexcept{
	std::cout << "移动赋值运算符" << std::endl;
	/* 直接检测自赋值 */
	if (this != &aref)
	{
        /* 释放已有元素 */
		delete this->m_i;
		this->m_i = nullptr;
		/* 从aref接管资源 */
		this->m_i = aref.m_i;
		this->m_str = aref.m_str;
		/* 将aref置于可析构状态 */
		aref.m_i = nullptr;
	}
	return *this;
}

Ref::~Ref(){
	std::cout << "Destructor called." << std::endl;
	delete this->m_i;
	this->m_i = nullptr;
}

int main(){
	Ref aref;
	Ref bref = std::move(aref);
	Ref cref;
	cref = std::move(aref);

	system("pause");
}
ellipse
ellipse
ellipse

右值引用
通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,我们可以自由地将有一个右值引用的资源“移动”到另一个对象中。
我们不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

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绑定到乘法结果上

返回左值引用给的函数,连同赋值、下标、解引用和前置递增/递减运算丰富,都是返回左值的表达式的例子。可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久;右值短暂
左值具有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,所以:

  • 所引用的对象将要被销毁
  • 该对象没有其它用户
    这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
    变量是左值
    变量可以看作是只有一个运算对象而没有运算符的表达式。类似其它任何表达式,变量表达式也有左值、右值属性。变量表达式都是左值。带来一个惊人的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上:
int &&rr1 = 42;    // 正确:字面常量是右值
int &&rr2 = rr1;   // 错误:表达式rr1是左值!
ellipse
ellipse
ellipse

代码示例说明:

#include <vector>
#include <iostream>
#include <algorithm>    // std::sort

class Foo
{
public:
	Foo() = default;

	Foo & operator=(const Foo&)&;  // 只能向可修改的左值赋值
    Foo &retFoo();                 // 返回一个引用;retFoo调用是一个左值   
    Foo retVal();                  // 返回一个值;retVal调用是一个右值

    Foo sorted() && ;              // 可用于可改变的右值
    Foo sorted()const &;           // 可用于任何类型的Foo

    void setValue(int i) { this->m_vec.push_back(i); }

    ~Foo() = default;

private:
	std::vector<int> m_vec;
};


Foo & Foo::operator=(const Foo & rts) &
{
    this->m_vec = rts.m_vec;
    return *this;
}

// 返回一个引用;retFoo调用是一个左值  
Foo & Foo::retFoo()
{
    return *this;
}
// 返回一个值;retVal调用是一个右值
Foo Foo::retVal()
{
    return *this;
}

// 针对右值对象使用,因此可以原址排序
Foo Foo::sorted() &&
{
    std::sort(m_vec.begin(), m_vec.end());
    return *this;
}

// 针对const左值、或左值使用,两种情况都无法进行原址排序
Foo Foo::sorted() const &
{
    Foo ret(*this);  // 拷贝一个副本
    std::sort(ret.m_vec.begin(), ret.m_vec.end());

    return ret;
}


int main()
{
    Foo afoo;
    afoo.setValue(5);
    afoo.setValue(3);
    afoo.setValue(9);
	
    auto it1 = afoo.retVal().sorted();    // 调用sorted的右值版本,afoo本身的值不变
    auto it2 = afoo.retFoo().sorted();    // 调用sorted的左值版本,afoo本身的值不变
    // 程序运行的结果是:
    // it1: 3 5 9
    // it2: 3 5 9
    // afoo仍然是5 3 9

    // afoo.retVal() = it1;               // 此句报错,未定义向右值赋值的赋值运算符
    afoo.retFoo() = it1;                  // 此句正确,定义了向左值赋值的赋值运算符
	
	system("pause");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值