C++ Primer学习笔记-----第十三章:拷贝控制

之前写的没保存,以后在写。

5.动态内存管理类
在这里插入图片描述
StrVec类定义

class StrVec
{
public:
	StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){}
	StrVec(const StrVec&);
	StrVec& operator=(const StrVec&);
	~StrVec();

	void push_back(const 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;
	void chk_n_allc(){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;
};

使用construct:用allocator分配内存,内存是未构造的,要使用原始内存,必须调用construct构造一个对象

void StrVec::push_back(const string& s)
{
	chk_n_alloc();						//确保有空间容纳新元素
	alloc.construct(first_free++,s);	//在first_free指向的元素中构造s的副本
}
construct的第一个参数是一个指针,指向调用allocate所分配的未构造的内存空间。剩下参数确定用哪个构造函数来构造对象。

alloc_n_copy成员

类似vector,StrVec类有类值的行为,当拷贝或赋值StrVec时,必须分配独立的内存,并从原StrVec对象拷贝元素到新对象。

pair<string*,string*> StrVec::alloc_n_copy(const string* b,const string* e)
{
	auto data = alloc.allocate(e - b);			//e-b获得要分配的数量,allocate返回分配内存的第一个地址指针
	return {data,uninitialized_copy(b,e,data)};	//pair返回新空间开始位置,拷贝的尾后的位置
}

free成员

free成员有两个责任:首先destroy元素,然后释放StrVec自己分配的内存空间。

void StrVec::free()
{
	if(elements)	//先检查指针释放为空
	{
		for(auto p = first_free; p!=elements;)		//逆序销毁旧元素
			alloc.destroy(--p);
		alloc.deallocate(elements,cap - elements);	//不能传给deallocate空指针
	}
}
deallocate的指针必须是之前某次allocate调用所返回的指针。

拷贝控制成员

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

析构函数
StrVec::~StrVec(){free();}

拷贝赋值运算符
StrVec& StrVec::operator=(const StrVec& rhs)
{
	auto data = alloc_n_copy(rhs.begin(),rhs.end());	//先拷贝元素,可解决自赋值的问题
	free();												//释放自身数据
	elements = data.first;								//赋值
	first_free = cap = data.second;
	return *this;
}

在重新分配内存的过程中移动而不是拷贝元素

在编写reallocate成员函数之前,先思考一下此函数应该做什么:
1.为了一个新的,更大的string数组分配内存。
2.在内存空间的前一部分构造对象,保存现有元素。
3.销毁原内存空间中的元素,并释放这块内存。

从上可以看出,为一个StrVec重新分配内存空间会引起从旧内存空间到新内存空间逐个拷贝string。
由于string的行为类似值,每个string对构成它的所有字符都会保存自己的一份副本。拷贝一个string必须为这些字符分配内存空间
而销毁一个string必须释放所占用的内存。

因此,在本例中,拷贝这些string中的数据是多余的,在重新分配内存空间时,如果能避免分配和释放string的额外开销,StrVec的
性能会好很多。

移动构造函数和std::move

通过使用新标准库引入的两种机制,我们就可以避免string的拷贝。
首先,有一些标准库类,包括string,都定义了所谓的“移动构造函数”。
移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。(说白了就是改变指针的指针而已)

第二个机制是一个名为move的标准库函数,定义在utility头文件中。
关于move需要了解两个关键点:
首先,当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数(原因后面解释)。
	如果它漏掉了move调用,将会使用string的拷贝构造函数。
其次,通常不为move提供一个using声明,当使用move时,直接调用std::move而不是move。

reallocate成员

现在可以编写reallocate函数了。首先调用allocate分配新的内存空间。新空间容量加倍,如果StrVec为空,分配容纳一个元素的空间。
void StrVec::reallocate()
{
	auto newcapacity = size() ? 2*size() : 1;			//准备更大的空间
	auto newdata = alloc.allocate(newcapacity);			//分配新内存
	//将数据从旧内存移动到新内存
	auto dest = newdata;								//指向新数组中下一个空隙位置
	auto elem = elements;								//指向就数组中下一个元素
	for(size_t i = 0; i != size(); ++i)
		alloc.construct(dest++, std::move(*elem++));	//让新内存的对象指针 指向 旧内存指针指向的对象
	free();												//移动完,释放旧内存
	//更新数据结构,执行新元素
	elements = newdata;						//新对象中第一个元素					
	first_free = dest;						//新对象中第一个可以用位置
	cap = elements + newcapacity;			//新对象的容量
}

6.对象移动

我们的StrVec类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的
更好的方式是移动元素。
使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能共享的资源(如指针或IO缓冲),
因此这些类型的对象补鞥呢拷贝但可以移动。

在旧C++标准中,没有直接的方法移动对象。因此,即使不必拷贝对象的情况下,也不得不拷贝。
类似的,在旧版本的标准库中,容器中所保存的类型必须是可拷贝的。
但在新标准中,可以用容器保存不可拷贝的类型,只要它们能被移动即可。

6.1.右值引用

为了支持移动操作,新标准引入了一种新的引用类型:右值引用(必须绑定到右值的引用),通过&&来获得右值引用。
右值引用有一个重要性质:只能绑定到一个将要销毁的对象。
一般而言:一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

类似任何引用,一个右值引用是某个对象的另一个名字而已。常规引用又称为左值引用。
int i = 1;
int& r = i;					
int&& rr = i;				//错误:不能将一个右值引用绑定到一个左值上
int& r2 = i*2;				//错误:i*2是一个右值
const int& r3 = i*2;		//正确:const引用可以绑定一个右值
int&& rr2 = i*2;			//正确

返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运算符,都是返回左值的表达式的例子。
可以将一个左值引用绑定到这类表达式的结果上

返回非引用类型的函数,连同算术、关系、位以及后置递增递减运算符,都生成右值。
不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或一个右值引用绑定到这类表达式上。

左值持久,右值短暂

左值有持久的状态,右值要么是字面量,要么是在表达式求值过程中创建的临时对象。
右值引用只能绑定到临时对象,得知:
1.所引用的对象将要被销毁
2.该对象没有其他用户
意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

变量是左值

变量可以看做只有一个运算对象而没有运算符的表达式,类似其他表达式,变量表达式也有左值/右值属性。
变量表达式都是左值。带来的结果是,不能将一个右值引用绑定到右值引用类型的变量上:
int&& r1 = 4;
int&& r2 = r1;		//错误:r1是左值

标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但可以显示地将一个左值转换为对应的右值引用类型。
通过调用move的新标准库函数来获得绑定到左值上的右值引用。
int&& r3 = std::move(r1);
std::move调用之后,除了对r1赋值或销毁它外,将不再使用它。

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

为StrVec类定义移动构造函数,实现从一个StrVec到另一个StrVec的元素移动而非拷贝:
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中的内存。
接管内存后,将给定对象中的指针都置为nullptr。(这也是为什么移动之后不要再使用原对象了)
这样就完成了从给定对象的移动操作,此对象将继续存在。最终移后源对象会被销毁,意味着将在其上运行析构函数。
StrVec的析构函数在first_free上调用deallocate。如果忘记了改变s.first_free,则销毁移后源对象就会释放掉
我们刚刚移动的内存。

移动操作、标准库容器和异常

由于移动操作“窃取”资源,通常不分配任何资源。因此,移动操作通常不会抛出任何异常。
当编写一个不抛出异常的移动操作时,应该将此事通知标准库。除非标准库知道我们的移动构造函数不会抛出异常,否则
它会认为移动给我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。

一种通知标准库的方法是在构造函数中指明noexceptnoexcept是新标准引入的。
noexcept出现在参数列表和初始化列表开始的冒号之间:
class StrVec
{
public:
	StrVec(StrVec&&) noexcept;
};
StrVec::StrVec(StrVec&& s) noexcept :/*成员初始化器*/
{/*构造函数体*/}
头文件的声明和定义中(如果定义在类外)都指定noexcept

在这里插入图片描述
移动赋值运算符

移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,
我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
StrVec& StrVec::operator=(StrVec&& rhs) noexcept
{
	if(this != &rhs)	//直接检查自赋值
	{
		free();							//释放已有元素
		elements = rhs.elements;		//从rhs接管资源
		first_free = rhs.first_free;
		cap = rhs.cap;
		rhs.elements = rhs.first_free = rhs.cap = nullptr;  	//将rhs置于可析构的状态
	}	
	return *this;
}

我们进行检查的原因是此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源
之前就释放左侧运算对象的资源(可能是相同的资源)。

移动源对象必须可析构

在这里插入图片描述

合成的移动操作:文字太多,上图

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

6.3.右值引用和成员函数

除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。
这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式:
拷贝:一个版本接受一个指向const的左值引用
移动:第二个版本接受一个指向非const的右值引用

例如,定义了push_back的标准库容器提供两个版本:
一个版本有一个右值引用参数,另一个版本有一个const左值引用。假定X是元素类型
void push_back(const X&);		//拷贝:绑定到任意类型的X
void push_back(X&&);			//移动:只能绑定到类型X的可修改的右值

一般来说不需要为函数操作定义接受一个const X&&或是一个(普通的)X&参数的版本。
当我们希望从实参“窃取”数据时,通常传递一个右值引用。为了达到这个目的,实参不能是const的。
类似的,从一个对象进行拷贝的操作不应该改变该对象。因此不需要定义接受一个普通的X&参数的版本。

作为一个更具体的例子,将StrVec类定义另一个版本的push_back:
class StrVec
{
public:
	void push_back(const string&);	//拷贝元素
	void push_back(string&&);		//移动元素
	//其他成员的定义,如前
};
void StrVec::push_back(const string& s)
{
	chk_n_alloc();
	alloc.construct(first_free++,s);
}

void StrVec::push_back(string&& s)
{
	chk_n_alloc();
	alloc.construct(first_free++,std::move(s));	//右值引用版本调用std::move
}

右值引用版本调用move来将其参数传递给construct。如前所述,construct函数使用其第二个和随后的实参的类型来确定使用
哪个构造函数。由于move返回一个右值引用,传递给construct的实参类型是string&&。因此会使用string的移动构造函数构造
新元素。当我们调用push_back时,实参类型决定了新元素时拷贝还是移动到容器中:
StrVec vec;
string s = "some string";
vec.push_back(s);				//调用push_back(const string&)
vec.push_back("oh!my god!");	//调用push_back(string&&), 从"oh!my god!"创建的string是右值
注意:"oh!my god!"是字符串字面值,类型是const char*字符数组,不是string,会隐式转换为string。

右值和左值引用成员函数

通常,我们在一个对象上调用成员函数,而不管该对象时一个左值还是右值。例如:
string s1="a",s2="b";
auto n = (s1+s2).find('a');  //(s1+s2)是一个右值
s1+s2 = "hi";				 //对右值赋值
在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库仍然允许向右值赋值。但是我们可能希望在自己
的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即,this指向的对象)是一个左值
指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符:
class Foo
{
public:
	Foo& operator=(const Foo&) &;	//只能向可修改的左值赋值
};
Foo& Foo::operator=(const Foo& rhs) &
{
	/*do some work*/
	return *this;
}
引用限定符可以是&&&,分别指出this可以指向一个左值或右值。类似const的限定符,引用限定符只能用于(非static)成员函数,
且必须同时出现在函数的声明和定义中。
对于&限定的函数,只能将它用于左值;对于&&限定的函数,只能用于右值。例如:
Foo& retFoo();	//返回一个引用,retFoo调用时一个左值
Foo retVal(); 	//返回一个值,retVal调用是一个右值(假定是右值)
Foo i,j;
i = j;
retFoo() = j;
retVal() = j;	//错误:retVal()返回一个右值
i = retVal();

一个函数可以同时用const和引用限定:在此情况下,引用限定必须跟在const限定符之后
class Foo
{
public:
	Foo someMem() const &;
};

重载和引用计数

引用限定符也可以区分重载版本。可以综合引用限定符和const来区分一个成员函数的重载版本。

class Foo
{
public:
	Foo sorted() &&;			//可用于可改变的右值
	Foo sorted() const &;		//可用于任何类型的Foo
	//其他成员的定义
private:
	vector<int> data;
};
Foo Foo::sorted() &&		//本对象为右值,可以原址排序
{
	sort(data.begin(),data.end());
	return *this;
}
Foo Foo::sorted()const&		//本对象是const或是一个左值,哪种情况都不能对其进行原址排序
{
	Foo ret(*this);							//拷贝一个副本
	sort(ret.data.begin(),ret.data.end));	//排序副本
	return ret;								//返回副本
}

如果定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加:
class Foo
{
public:
	Foo sorted() &&;
	Foo sorted() const;	//错误:必须加上引用限定符,或者把上面的引用限定符去掉
};
总结:如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值