【C++ Primer】拷贝控制

  拷贝控制操作包括拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。如果一个类没有定义所有这些拷贝控制成员,编译器会为它定义缺失的操作(尽量明确定义对象拷贝、赋值、移动或者销毁时做什么)。

一、拷贝、赋值与销毁

1、构造函数、默认构造函数和拷贝构造函数

(1)构造函数

   构造函数与其他函数不同:构造函数和类同名,没有返回类型。
  构造函数与其他函数相同:构造函数也有形参表(可为void)和函数体。 (参数表为void的构造函数为默认构造函数)
  构造函数构造类对象的顺序是:
1、内存分配,构造函数调用的时候 隐士\显示的初始化各数据。
2、执行构造函数的运行。

(2)默认构造函数

   合成的默认构造函数:当类中没有定义构造函数(注意是构造函数)的时候,编译器自动生成的函数。但是我们不能过分依赖编译器,如果我们的类中有复合类型或者自定义类型成员,我们需要自己定义构造函数。
   自定义的默认构造函数:

A(): a(0) {}  
A(int i = 1): a(i) {}  

可能疑问的是第二个构造函数也是默认构造函数么?是的,因为参数中带有默认值。

(3)拷贝构造函数

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

public:  
    Foo();   //默认构造函数  
    Foo(const Foo&); //拷贝构造函数  
};  

  拷贝构造函数的第一个参数是必须是自身类类型的引用类型,如果不是引用会无限递归。构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作就是保证每个对象的数据成员具有合适的初始值。

(4)合成拷贝构造函数

  合成拷贝构造函数:我们没有为类定义拷贝构造函数,编译器会帮我们定义一个。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝。
使用

(5)拷贝初始化和直接初始化

   直接初始化:编译器使用普通的函数匹配,来选择与我们提供的参数最匹配的构造函数。
  拷贝初始化:将右侧的对象拷贝到正在创建的对象中,通常使用拷贝构造函数来完成,如果需要的话还要进行类型转换。
  调用拷贝构造函数 除了=还有:

  • 将一个对象作为实参传递给一个非引用类型的形参。
  • 从一个返回类型为非引用的函数返回一个对象。
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
      拷贝构造函数不应该是explicit的。每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝;内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但合成拷贝狗杂函数会逐元素地拷贝一个数组类型的成员。当初化标准库容器或是调用其insert或push成员时,容器会对其元素进行拷贝初始化,相对的,用emplace成员创建的元素都进行直接初始化。

(6)参数和返回值

   拷贝构造函数的参数必须是引用:
  为了调用拷贝构造函数,我们必须拷贝实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此循环,所以构造函数被用来初始化非引用类类型参数,解释了为什么拷贝构造函数自己的参数一定是引用类型。
  返回值被用来初始化调用方的结果。

2、深拷贝与浅拷贝

  浅拷贝: obj1.memberPtr和obj2.memberPtr可能指向同一块堆内存, 这样obj1如果修改了这块内存, 那么obj2的数据也会被改变(特别是析构的时候)。例如:
这里写图片描述
  深拷贝: 当进行对象拷贝的时候, 不是进行简单的对象赋值, 而是把堆内存中的数据也一一单独再划一块地方赋值存储, 实现对象之间两不影响的效果, 就叫做深拷贝。
  当类中有指针或者动态分配的内存时,尤其会产生这类问题。例子如下:

#include <bits/stdc++.h>
using namespace std;
class Array
{
public:
	Array(int count) 
	{ 
		m_iCount = count;
		m_pArr = new int[m_iCount];//申请动态内存。
		for (int i = 0; i < m_iCount; ++i)
			m_pArr[i] = i;
		cout << "Array" << endl; 
	}
	Array(const Array &arr) 
	{ 		
		m_iCount = arr.m_iCount; 
		//浅拷贝:
		//m_pArr = arr.m_pArr。m_pArr和arr.m_pArr指向相同的内存地址。
		//深拷贝:
		m_pArr = new int[m_iCount];//重新申请一段内存。
		for (int i = 0; i < m_iCount; ++i)
			m_pArr[i] = arr.m_pArr[i];//这时m_pArr和arr.m_pArr已经指向了不同的内存地址。
		cout << "Array &" << endl;
	}
	~Array()
	{
	 	delete [] m_pArr;
	 	m_pArr = nullptr; 
		cout << "~Array" << endl; 
	}
	inline void setCount(int count) { m_iCount = count; }
	inline int getCount(){ return m_iCount; }
	inline printAddr() { cout << "m_pArr : " << m_pArr << endl; }
private:
	int m_iCount;
	int *m_pArr;
};

int main(int argc, char const *argv[])
{
	Array arr1(5);
	Array arr2(arr1);
	arr1.printAddr();
	arr2.printAddr();
	system("pause");
	return 0;
}

3、拷贝赋值运算符

HasPtr& operator=(const HasPtr& t)
{
    i = t.i;
    ps = new string;
    *ps = *(t.ps);
}
HasPtr a,b;  
a = b;  //使用Sales_data的拷贝赋值运算符 

  和拷贝构造函数一样,如果类未定义拷贝赋值运算符,编译器会合成一个。赋值构造函数必须注意它的函数原型,参数通常是引用类型,返回值通常也是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。

3、析构函数

  析构函数执行的与构造函数执行的顺序相反,析构函数释放对象所使用的资源,并销毁对象的非static数据成员。
【Note】:
1)析构函数不接受参数,不能被重载,对于一个类只有唯一一个析构函数。
2)隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
3)当指向一个对象的引用或者指针离开作用域时,析构函数不会被执行

  在以下几种情况下会调用析构函数:

  • 变量在离开其作用域的时候;
  • 当一个对象被销毁时,其成员被销毁;
  • 容器被销毁时;
  • 当对指向它的指针应用delete运算符时被销毁;
  • 对于临时对象,当创建它的完整表达式结束时被销毁。

4、一个demo:

#include<iostream>
#include<string>
using namespace std;

class Str{
public:
	Str() = default;
	Str(string s) :str(s),pstr(new string()){ cout << "构造函数" << endl;  }
	Str(const Str& s){ 
		str = s.str;
		pstr = new string(*s.pstr);
		cout << "拷贝构造函数" << endl; 
	}
	Str& operator=(const Str& s){
		str = s.str;
		pstr = new string(*s.pstr);
		cout << "拷贝赋值运算符" << endl;
		return *this;
	}
	~Str(){
		delete pstr;
		cout << "析构函数" << endl;
	}
private:
	string str;
	string * pstr;
};
Str func(Str s){
	return s;
}
int main(){
	Str str1("aa");  //直接初始化
	Str str2 = str1; //拷贝初始化
	Str str3(str1);  //拷贝初始化
	Str str4;
	str4 = str1; //赋值初始化
	func(str1);//见拷贝初始化,调用2次拷贝构造函数和2次析构函数
	system("pause");
	return 0;
}

5、三/五法则、使用=default 、阻止拷贝:

(1)三/五法则

  如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
  如果一个类需要一个拷贝构造函数,几乎可以肯定的它也需要一个拷贝赋值运算符,反之亦然。
  无论是需要拷贝构造函数还是需要拷贝运算符都不意味着也需要析构函数。

(2)使用=default

  我们可以通过将拷贝控制成员定义为=default来显示地让编译器来为我们生成默认版本。
【Note】:
1)我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。

(3)阻止拷贝

  新标准下,我们可以通过将拷贝构造函数和赋值运算符定义为删除的函数来阻止拷贝和赋值。
【Note】:
1)=delete必须出现在函数第一次声明的时候。
2)析构函数不能是删除的成员。
3)如果一个类有数据成员不能默认构造、拷贝、复制或销毁、则对应的成员函数
将定义为删除的。

  删除一个类的析构函数或者类的某个成员的类型删除了析构函数,我们都不能定义该类的变量或临时对象。但可以动态分配这种类型,不能释放。

struct NoDtor{  
    NoDtor() = default;  //使用合成默认构造函数  
    ~NoDtor() = delete; //我们不能销毁NoDtor类型的对象  
};  
//NoDtor nd;//错误:NoDtor的析构函数时删除的  
NoDtor *p = new NoDtor(); //正确:但不能delete p  
//delete p; //错误:NoDtor的析构函数是删除的  

6、本节的demo:

#include <iostream>    
#include <string>    
#include <fstream>  
#include <list>  
#include <vector>   
#include <map>    
#include <set>  
#include <cctype>  
#include <algorithm>  
#include <utility>//保存pair的头文件。  
#include <memory>  
using namespace std;  
//具体操作时将类的声明置于头文件中。  
class Employee  
{  
public:  
    Employee();//默认构造函数。  
    Employee(string &s);//接受一个string的构造函数。     
    Employee(const Employee &) = delete;//不需要拷贝构造函数,雇员号不可能一样。将其声明为 =delete。  
    Employee &operator=(const Employee &) = delete;  
    int number(){return _number;}  
private:  
    string employee;  
    int _number;  
    static int O_number;//static静态成员数据在类内声明,但只可以在类外定义,在类外定义时可不加static。  
};  
  
int Employee::O_number = 0;  
Employee::Employee()//默认构造函数。 
{  
    _number = O_number++;  
}  
Employee::Employee(string& s)//接受一个string的构造函数。  
{  
    employee = s;  
    _number = O_number++;  
}  
  
void show(Employee a)  
{  
    cout<<a.number()<<endl;  
}  
int main(int argc, char**argv)    
{  
    Employee a, b, c;  
    show(a);//调用函数时需要拷贝一次。  
    show(b);//发生错误,不允许拷贝和赋值。
    show(c);  
    return 0;  
}   

二、拷贝控制和资源管理、交换操作

  通常管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,首先必须确定此类型对象的拷贝语义。一般来说有两种选择:
  可以定义拷贝操作,使类看起来像一个值或像一个指针。值和指针的区别是,值由自己的状态,拷贝一个像值的对象,副本和原对象完全独立,而指针则共享状态。
  当用标准库时,容器和string类的行为像一个值。而不出意外的,shared_ptr类提供类似指针的行为,像StrBlob类一样。IO类型和unique_ptr不允许拷贝和赋值,因此它们的行为既不像值也不像指针。

1、行为像值的类

  对于类资源的管理,每个对象都有一份自己的拷贝。
【Note】:
1)如果将一个对象赋予它自身,赋值运算符必须能正常工作。
2)大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
3)先将右侧运算对象拷贝到一个局部临时对象中,拷贝完后,左侧运算对象的现有成员就是安全的了。

#include <iostream>
#include <string>
using namespace std;

class HasPtr{
	friend ostream& print(std::ostream &os, const HasPtr&);
public:
	HasPtr(const string& s = string()) :ps(new std::string(s)), i(0) { cout << "构造函数" << endl; }
	HasPtr(const HasPtr& p) :ps(new string(*p.ps)), i(p.i){ cout << "拷贝构造函数" << endl; }
	HasPtr& operator=(const HasPtr&);
	~HasPtr(){
		delete ps;
		cout << "析构函数" << endl;
	}
private:
	string* ps;
	int i;
};
HasPtr& HasPtr::operator = (const HasPtr &p){
	auto newp = new string(*p.ps);//考给临时变量,万一=左值是自己就释放了。
	delete ps;
	ps = newp;
	i = p.i;
	cout << "拷贝赋值运算符" << endl;
	return *this;
}
ostream& print(std::ostream &os, const HasPtr& p){
	std::cout << "string:" << *p.ps << " int:" << p.i << std::endl;
	return os;
}

int main(){
	HasPtr p1;
	HasPtr p2("hehe");
	print(cout, p1);
	print(cout, p2);
	p1 = p2;
	print(cout, p1);

	system("pause");
	return 0;
}

2、行为像指针的类

  引用计数需要确定在哪里存放引用计数。计数器不能作为HasPtr对象的成员。一种方法是将计数器保存在动态内存中。当创建一个对象时,我们分配一个新的计数器。当拷贝或赋值对象时,拷贝指向计数器的指针。这种方式,副本和原对象都会指向相同的计数器。

#include <iostream>
#include <string>
using namespace std;

class HasPtr{
	friend ostream& print(std::ostream &os, const HasPtr&);
public:
	HasPtr(const string& s = string()) :ps(new std::string(s)), i(0), use(new size_t(1)){ cout << "构造函数" << endl; }
	HasPtr(const HasPtr& p) :ps(p.ps), i(p.i), use(p.use){
		++*use;
		cout << "拷贝构造函数" << endl;
	}
	HasPtr& operator=(const HasPtr&);
	~HasPtr();
private:
	string* ps;
	int i;
	size_t* use;
};
HasPtr& HasPtr::operator = (const HasPtr &rhs){
	++*rhs.use;  //递增右侧运算对象的引用计数
	if (--*use == 0){  //递减本对象的引用计数
		delete ps;
		delete use;
	}
	ps = rhs.ps;  //将数据从rhs拷贝到本对象
	i = rhs.i;
	use = rhs.use;
	return *this;
}
HasPtr::~HasPtr(){
	if (--*use == 0){  //如果引用计数变为0
		delete ps;     //释放string内存
		delete use;   //释放计时器内存
	}
	cout << "析构函数" << endl;
}
ostream& print(std::ostream &os, const HasPtr& p){
	std::cout << "string:" << *p.ps << " int:" << p.i << " use:" << *p.use << std::endl;
	return os;
}

int main(){
	HasPtr p1;
	HasPtr p2("hehe");
	print(cout, p1);
	print(cout, p2);
	p1 = p2;
	print(cout, p1);

	system("pause");
	return 0;
}

3、交换操作

  与拷贝控制成员不同,swap并不是必要的,但对于分配了资源的类swap是个优化手段。
  swap函数应该调用swap而不是std::swap。
  标准库swap对HasPtr管理的string进行了不必要的拷贝。如果一个类的成员有自己类型特定的swap函数,调用std:swap就是错误的。如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本。拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要做的工作。这时,公共的工作应该放在private工具函数中完成。
  对于那些与重排元素顺序的算法一起使用的类,定义swap是非常重要的。这类算法在需要交换两个元素时会调用swap。

4、本节的demo:

#include <iostream>    
#include <string>  
#include <vector>  
#include <algorithm>  
using namespace std;  

class Hasptr1  
{  
    friend void swap(Hasptr1 &,Hasptr1 &);  
    friend bool operator<(const Hasptr1 &s1,const Hasptr1 &s2);  
    friend void show(vector<Hasptr1> &vec);  
public:  
    //构造函数,初始化相关成员  
    Hasptr1(const string &s = string()):ps(new string(s)),i(0),use(new size_t(1)){}  
  
    //拷贝构造函数,将引用计数也拷贝过来,并且递增引用计数  
    Hasptr1(const Hasptr1 &p):ps(p.ps),i(p.i),use(p.use){++*use;}  
  
    //拷贝赋值运算符  
    Hasptr1 &operator= (const Hasptr1 &p1)  
    {  
        ++*p1.use;//首先递增右侧运算符对象的引用计数  
        if (--*use == 0)//递减本对象的引用计数,若没有其他用户,则释放本对象的成员  
        {  
            delete ps;  
            delete use;  
        }  
        ps = p1.ps;//进行拷贝  
        use = p1.use;  
        i = p1.i;  
        return *this;  
    }  
  
    //析构函数  
    ~Hasptr1()  
    {  
        if (*use == 0)//引用计数变为0,说明已经没有对象再需要这块内存,进行释放内存操作  
        {  
            delete ps;  
            delete use;  
        }  
    }  
private:  
    //定义为指针,是我们想将该string对象保存在动态内存中  
    string *ps;  
    size_t *use;//将计数器的引用保存  
    int i;  
};  
  
inline void swap(Hasptr1 &a,Hasptr1 &b)  
{  
    using std::swap;  
    swap(a.ps,b.ps);  
    std::swap(a.i,b.i);  
    cout<<"123";  
}  
  
bool operator< (const Hasptr1 &s1,const Hasptr1 &s2)  
{  
    cout<<"定义的 Operator< 被调用"<<endl;  
    return *s1.ps < *s2.ps;  
}  
  
void show(vector<Hasptr1> &vec)  
{  
    vector<Hasptr1>::iterator it1 = vec.begin();  
    for (it1; it1 != vec.end(); ++it1)  
    {  
        cout<<*(it1->ps)<<endl;  
    }  
}  
int main(int argc, char**argv)    
{  
    vector<Hasptr1> vec1;  
    Hasptr1 a("l");  
    Hasptr1 b("llll");  
    Hasptr1 c("lll");  
    vec1.push_back(a);  
    vec1.push_back(b);  
    vec1.push_back(c);  
    vector<Hasptr1>::iterator it1 = vec1.begin();  
    sort(vec1.begin(),vec1.end());  
    show(vec1);  
    system("pause");
    return 0;  
}    

三、拷贝控制示例

#include <bits/stdc++.h>
using namespace std;
class Message;
class Folder
{
public:
	Folder() = default;
	Folder(const Folder &);
	Folder &operator=(const Folder &);
	~Folder() = default;
	inline void AddMsg(Message* msg){ messages.insert(msg); }
	inline void RemMsg(Message* msg){ messages.erase(msg); }
private:
	set<Message*> messages;
};
class Message
{
	friend class Folder;
public:
	explicit Message(const string &str = ""):contents(str){}
	Message & operator=(const Message &);//拷贝赋值运算符。
	Message(const Message &);//拷贝构造函数。
    //移动构造函数。移动contents。
    Message(Message &&m): contents(std::move(m.contents)){ move_folders(&m); }
    Message & operator=(Message &&rhs);
	~Message(){ remove_from_Folders(); }//析构函数。
	void save(Folder &);  
	void remove(Folder &);//对Folder类的操作函数。
	void swap(Message &m1,Message &m2)void move_folders(Message *m);
private:
	string contents;
	set<Folder*> folders;
    void add_to_Folders(const Message&);
    void remove_from_Folders();//从folders中删除本message。
};

void Message::move_folders(Message *m)
{
    folders = std::move(m->folders);//使用set的移动赋值。
    for (auto f : folders)
    {
        f->RemMsg(m);//删除旧元素。
        f->AddMsg(this);//添加新元素。
    }
    m->folders.clear();//确保销毁是无害的。
}

void Message::save(Folder &f)
{
	folders.insert(&f);//将给定folder加入到我们的folders中。
	f.AddMsg(this);//将本message添加到给定folder中。
}

void Message::remove(Folder &f)
{  
    folders.erase(&f);//将给定folder在我们的folders中删除。
    f.RemMsg(this);//将本message在给定folder中删除。
} 

void Message::add_to_Folders(const Message &m)  
{  
    for (auto f : m.folders)  
    {  
        f->AddMsg(this);  
    }  
}  

void Message::remove_from_Folders()  
{  
    for (auto f : folders)  
    {  
        f->RemMsg(this);//所有包含此message的folder进行删除操作。 
    }  
}  

Message & Message::operator=(const Message &m)//拷贝赋值运算符。  
{  
    remove_from_Folders();//删除自身。  
    contents = m.contents;  
    folders = m.folders;  
    add_to_Folders(m);//将本message传入folder中。  
    return *this;  
}

Message & operator=(Message &&rhs)
{
    if(this != &rhs)
    {
        remove_from_Folders();
        contents = std::move(rhs.contents);
        move_folders(&rhs);
    }
    return *this;
}

void Message::swap(Message &m1,Message &m2)  
{  
    using std::swap;  
    //先将每个message从其folders中删除。  
    for(auto f : m1.folders)  
    {  
        f->RemMsg(this);//所有包含此message的folder进行删除操作。  
    }  
    for(auto f : m2.folders)  
    {  
        f->RemMsg(this);//所有包含此message的folder进行删除操作。  
    }  
    swap(m1.folders,m2.folders);  
    swap(m1.contents,m2.contents);  
    for (auto f : m1.folders)  
    {  
        f->AddMsg(this);//再进行添加操作。  
    }  
    for (auto f : m2.folders)  
    {  
        f->AddMsg(this);  
    }  
}  

int main(int argc, char const *argv[])
{
	Message m("ABC");
	return 0;
}

四、动态内存管理类

  StrVec的设计:vector的每个添加元素的成员函数会检查是否有空间容纳更多的元素。如果有,成员函数会在下一个可用位置构造一个对象。如果没有可用空间,vector就会重新分配空间:它获得新的空间,将已有元素移动到新空间,释放旧空间,并添加新元素。free成员,一旦元素被销毁,就调用deallocate来释放StrVec对象分配的内存空间,我们传递给deallocate的指针必须是之前某次allocate调用所返回的指针,因此在调用deallocate之前我们首先检查elements是否为空。
  在重新分配内存的过程中移动而不是拷贝元素。当拷贝一个string时,新string和原string是相互独立的。由于string的行为类似值,每个string对构成它的所有字符都会保存自己的一份副本。拷贝一个string必须为这些字符 分配内存空间。一旦将元素拷贝到新空间,就会立即销毁原string。所以,拷贝这些string中的数据是多余的。move标准库函数定义在utility中。两个要点,当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数。如果漏掉move操作,将使用string的拷贝构造函数。另外,不为move提供using声明。

#include <bits/stdc++.h>
using namespace std;
class StrVec
{
public:
	StrVec():elements(nullptr), first_free(nullptr), cap(nullptr){}
	//拷贝构造函数。
	StrVec(const StrVec &);
	//移动构造函数。
	StrVec(StrVec &&s) noexcept : elements(s.elements),first_free(s.first_free),cap(s.cap)
	{
		s.elements = s.first_free = s.cap = nullptr;
	}
	//拷贝赋值运算符。
	StrVec &operator=(const StrVec &);
	//移动赋值运算符。
	StrVec &operator=(StrVec &&rhs) noexcept;
	//为三个指针进行初始化,并将成员进行赋值。
    StrVec(initializer_list<string> lst)
    {   
        auto newdata = alloc_n_copy(lst.begin(), lst.end());    
        elements = newdata.first;    
        first_free = cap = newdata.second;    
    }
	~StrVec(){ free(); }
	void push_back(const string &);//拷贝元素。
	void push_back(string &&);//移动元素。
	size_t size() const{ return first_free - elements; }
	size_t capacity() const{ return cap - elements; }
	string *begin() const{ return elements; }
	string *end() const{ return first_free; }
private:
	static allocator<string> alloc;//声明但是未定义,静态成员必须在函数外定义。
	//没有空间容纳新元素,使用reallocate重新分配内存。
	void chk_n_alloc()
	{
		if(size() == capacity())
			reallocate();
	}
	pair<string*,string*> alloc_n_copy(const string*, const string*);
	void free();//销毁元素并释放内存。
	void reallocate();//获得更多内存并拷贝已有元素。
	string *elements;//指向数组首元素的指针。
	string *first_free;//指向数组第一个空闲元素的指针。
	string *cap;//指向数组尾后位置的指针。
};

//通过实参是右值还是左值确认调用那个版本。
void StrVec::push_back(const string &s)
{
	chk_n_alloc();//确保有空间容纳新元素。
	alloc.construct(first_free++, s);//使用未加1之前的值,递增构造对象。
}

void StrVec::push_back(string &&s)
{
	chk_n_alloc();
	alloc.construct(first_free++, std::move(s));
}

pair<string*,string*> 
alloc_n_copy(const string *b, const string *e)//将元素拷贝到新的内存中。
{
	auto data = alloc.allocate(e - b);
	return {data,uninitialized_copy(b,e,data)};//使用列表初始化返回。
}

void StrVec::free()
{
	if(elements)
	{
		//销毁旧元素。
		for_each(elements, first_free, [this](string &rhs){ alloc.destroy(&rhs); });  
		alloc.deallocate(elements, cap-elements);
	}
}

//拷贝构造函数。
StrVec::StrVec(const StrVec &s)
{
	auto newdata = alloc_n_copy(s.begin(),s.end());
	elements = newdata.first;
    first_free = cap = newdata.second;
}

//拷贝赋值运算符。
StrVec &StrVec::operator=(const StrVec &rhs)
{
	//调用alloc_n_copy分配内存。
	auto data = alloc_n_copy(rhs.begin(),rhs.end());
	free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

//移动赋值运算符。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
	//检查自赋值。
	if(this != &rhs)
	{
		free();//释放旧资源。
		//从rhs接管资源。
    	elements = rhs.elements;
    	first_free = rhs.first_free;
    	cap = rhs.cap;
    	//将rhs置于可析构状态。
    	rhs.elements = rhs.first_free = rhs.cap = nullptr;
	}
	return *this;
}

void StrVec::reallocate()
{  
	//分配当前大小两倍的空间。
    auto newcapacity = size() ? 2 * size() : 1;
    //分配新内存。
    auto first = alloc.allocate(newcapacity);
    //将数据从旧内存移动到新内存,使用移动迭代器。
    auto last = uninitialized_copy(
    	make_move_iterator(begin()),
    	make_move_iterator(end()),
    	first);
    free();//释放旧内存。
    //更新数据。
    elements = first;
    first_free = last;
    cap = elements + newcapacity;
}

int main(int argc, char const *argv[])
{
	StrVec sv;
	string s = "ABC";
	sv.push_back(s);
	sv.push_back("edf");
	cout << sv.size() << " " << sv.capacity() << endl;
	system("pause");	
	return 0;
}

五、对象移动

  新标准的移动对象的能力。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。这一特定源于IO类或unique_ptr这些类。它们包括被共享的资源,因此,这些类的对象不能拷贝只能移动。

1、左值、右值、右值引用详解

(1)左值、右值

  在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

(2)右值、将亡值

  在理解C++11的右值前,先看看C++98中右值的概念:C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。
  C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
  将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

(3)左值引用、右值引用

  左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
  右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
  左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

int &a = 2;       # 左值引用绑定到右值,编译失败
int b = 2;        # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2;  # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2;  # 常量左值引用绑定到右值,编程通过

  右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:

int a;
int &&r1 = c;             # 编译失败
int &&r2 = std::move(a);  # 编译通过

  下表列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
这里写图片描述

【Note】:
1)我们可以将一个右值引用绑定到表达式、字面值常量或者返回右值的表达式,但是不能将右值引用绑定到左值上。
2)右值引用指向将要被销毁的对象。
3)变量是左值,因此我们不能把右值引用直接绑定到变量上。
4)我们可以销毁一个移后源对象,也可以赋予其新值,但是不能使用一个移后源对象的值。
5)应该使用std::move而不是move,这样可以避免潜在的名字冲突。

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

(1)移动语义详解

  移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。举个栗子:
  问题一:如何将大象放入冰箱?答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。
  问题二:如何将大象从一台冰箱转移到另一台冰箱?普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?
  “移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。
  为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。

(2)移动构造函数

  移动构造函数类似于拷贝构造函数,第一参数是该类类型的一个引用,但这个引用参数在移动构造函数中是一个右值引用。
  与拷贝构造函数不同,移动构造函数不分配新内存;它接管内存并把对象中的指针都置为nullptr。最终,移后源对象会被销毁,意味着将在其上运行析构函数。
这里写图片描述

A(A && h) : a(h.a)  
{  
    h.a = nullptr; //还记得nullptr?  
}  

  可以看到,这个构造函数的参数不同,有两个&操作符, 移动构造函数接收的是“右值引用”的参数。
  移动构造函数何时触发? 那就是临时对象(右值)。用到临时对象的时候就会执行移动语义。这里要注意的是,异常发生的情况,要尽量保证移动构造函数 不发生异常,可以通过noexcept关键字,这里可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序。

(3)移动赋值操作符

  原理跟移动构造函数相同,这里不再多说:

A & operator = (A&& h)
{
	assert(this != &h);

	a = nullptr;
	a = move(h.a);
	h.a = nullptr;
	return *this;
}

【Note】:
1)不抛出异常的移动构造函数和移动赋值函数运算符必须标记为noexcept。
2)移后源对象必须可析构。

(4)合成的移动操作

  只有一个类没有定义任何拷贝控制成员(拷贝构造函数,拷贝赋值运算符,析构函数)时, 且类的所有非static成员都是可移动的, 此时编译器才会给该类合成移动构造函数和移动赋值运算符。
  当既有拷贝操作也有移动操作时,使用哪个?
  一条原则:移动右值,拷贝左值。即当右边是一个右值时, 就优先使用移动操作(也可以使用拷贝)。当右边是一个左值时, 只能使用拷贝操作(除非使用std::move将其转换)。
  必要时候, 右值也能被拷贝构造函数和拷贝赋值运算符拷贝。但是拷贝构造函数和拷贝赋值运算符的参数必须是const类型的引用才行, 如果不是就会出错(可以自己试试看看到底是什么情况)。

(5)赋值运算符实现拷贝赋值和移动赋值两种功能

  如果赋值运算符的形参是传值调用, 那么用实参初始化形参就需要调用拷贝构造函数(实参是左值)或移动构造函数(实参是右值)。那么可以用下面方式实现等价的拷贝赋值和移动赋值。(注意:下面的程序=操作符内使用的是自定义的swap, 因为标准库的swap需要类支持=操作符, 但是=操作符我们还没定义)。

3、右值和左值引用成员函数

  在类的成员函数后面加上& 或&& 可以限定该成员函数只能接受左值或右值的参数。同样可以避免对右值对象使用=赋值等操作。
这里写图片描述

参考:http://blog.csdn.net/ruan875417/article/details/44854189
http://blog.csdn.net/refuil/article/details/51547815
http://blog.csdn.net/hyman_yx/article/details/52044632
https://www.zhihu.com/question/22111546
http://blog.csdn.net/Jofranks/article/details/17438955
http://blog.csdn.net/u013480600/article/details/44151643

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值