C++Primer学习(13.5 动态内存管理类)

13.5 动态内存管理类
某些类需要在运行时分配可变大小的内存空间。这种类通常可以(并且如果它们确实可以的话,一般应该)使用标准库容器来保存它们的数据。例如,我们的strBlob类使用一个 vector来管理其元素的底层内存。
但是,这一策略并不是对每个类都适用:某些类需要自己进行内存分配。这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。
例如,我们将实现标准库 vector 类的一个简化版本。我们所做的一个简化是不使用模板,我们的类只用于string。因此,它被命名为strVec。
StrVec 类的设计
回忆一下,vector类将其元素保存在连续内存中。为了获得可接受的性能,vector预先分配足够的内存来保存可能需要的更多元素(参见9.4节,第317页)。vector的每个添加元素的成员函数会检查是否有空间容纳更多的元素。如果有,成员函数会在下一个可用位置构造一个对象。如果没有可用空间,vector就会重新分配空间:它获得新的空间,将已有元素移动到新空间中,释放旧空间,并添加新元素。
我们在 strVec类中使用类似的策略。我们将使用一个allocator来获得原始内存(参见12.2.2节,第427页)。由于allocator分配的内存是未构造的,我们将在需要添加新元素时用allocator的construct成员在原始内存中创建对象。类似的,当我们需要删除一个元素时,我们将使用destroy成员来销毁元素。
每个strvec有三个指针成员指向其元素所使用的内存:
(1)elements,指向分配的内存中的首元素
(2)first_free,指向最后一个实际元素之后的位置。
(3)cap,指向分配的内存末尾之后的位置
图13.2说明了这些指针的含义。
在这里插入图片描述
除了这些指针之外,strVec还有一个名为alloc的静态成员,其类型为allocator。
allocator 是 C++ 标准库中的一个模板类 allocator 针对 string 类型的实例化版本,用于为 string 对象分配和管理内存。
alloc成员会分配strVec使用的内存。我们的类还有4个工具函数:
(1)allocncopy会分配内存,并拷贝一个给定范围中的元素。
(2)free会销毁构造的元素并释放内存。
(3)chk_n_alloc保证strVec至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc会调用reallocate来分配更多内存。
(4)reallocate在内存用完时为strVec分配新内存。
虽然我们关注的是类的实现,但我们也将定义vector接口中的一些成员。
StrVec 类定义
有了上述实现概要,我们现在可以定义strVec类,如下所示:

//类vector类内存分配策略的简化实现
class StrVec
{
public:
	StrVec()://allocator成员进行默认初始化
	elements(nullptr),first free(nullptr),cap(nullptr){}
	StrVec(const StrVec&);//拷贝构造函数
	StrVec &operator=(const StrVec&);//拷贝赋值运算符
	~strVec();//析构函数
	void push_back(const std::string&);//拷贝元素
	size_t size()const {return first_free-elements;)
	size_t capacity()const(return cap-elements;}
	std::string*beqin()const{return elements;}
	std::string*end()const{return first free;)
private:
	Static std::allocator<std::string>alloc;//分配元素
	//被添加元素的函数所使用
	void chk_n_alloc()
	{if(size()==capacity())reallocate();}
	//工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
	std::pair<std::string*,std::string*>alloc_n_copy
	(const std::string*,const std::string*);
	void free();//销毁元素并释放内存
	void reallocate();//获得更多内存并拷贝已有元素;
	std::string *elements;//指向数组首元素的指针
	std::string *first free;//指向数组第一个空闲元素的指针
	std::string *cap;//指向数组尾后位置的指针
}

返回类型 std::pair<std::string*, std::string*> alloc_n_copy(const std::string*,const std::string*);
std::pair 是 C++ 标准库中的一个模板类,用于将两个不同或相同类型的值组合成一个对象。这里组合的两个值都是指向 std::string 的指针。
通常,在这个函数的应用场景中,第一个指针可能指向分配的内存区域的起始位置,第二个指针指向分配的内存区域中最后一个有效元素之后的位置,类似于容器的 begin() 和 end() 迭代器的概念。
类体定义了多个成员:
(1)默认构造函数(隐式地)默认初始化alloc并(显式地)将指针初始化为nullptr,表明没有元素。
(2)size成员返回当前真正在使用的元素的数目,等于first_free-elements。
(3)capacity成员返回 strVec可以保存的元素的数量,等价于cap-elements。
(4)当没有空间容纳新元素,即cap==first_free时,chk_n_alloc会为 StrVec重新分配内存。
(5)begin 和end 成员分别返回指向首元素(即elements)和最后一个构造的元素之后位置(即first_free)的指针。
使用 construct
函数push_back调用chk_n_alloc 确保有空间容纳新元素。如果需要,chk_n_alloc会调用reallocate。当chk_n_alloc返回时,push_back 知道必有空间容纳新元素。它要求其allocator成员来construct新的尾元素:

void StrVec::push_back(const string&s)
{
	chk_n_alloc();//确保有空间容纳新元素
	//在first_free指向的元素中构造s的副本
	alloc.construct(first_free++,s);
}

当我们用 allocator分配内存时,必须记住内存是未构造的(参见12.2.2节,第428页)。为了使用此原始内存,我们必须调用construct,在此内存中构造一个对象。传递给construct的第一个参数必须是一个指针,指向调用allocate所分配的未构造的内存空间。剩余参数确定用哪个构造函数来构造对象。在本例中,只有一个额外参数,类型为string,因此会使用string的拷贝构造函数
值得注意的是,对construct的调用也会递增first_free,表示已经构造了一个新元素。它使用后置递增(参见4.5节,第131页),因此这个调用会在first_free当前值指定的地址构造一个对象,并递增first_free 指向下一个未构造的元素。
alloc_n_copy 成员
我们在拷贝或赋值strVec时,可能会调用alloc_n_copy成员。类似 vector,我们的 strVec类有类值的行为(参见13.2.1节,第453页)。当我们拷贝或赋值 strVec时,必须分配独立的内存,并从原strVec对象拷贝元素至新对象。
alloc_n_copy成员会分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中。此函数返回一个指针的pair(参见11.2.3节,第379页),两个指针分别指向新空间的开始位置和拷贝的尾后的位置:

pair<string*,string*>StrVec::alloc_n_copy(const string *b,const string *e)
{
	//分配空间保存给定范围中的元素
	auto data=alloc.allocate(e-b);
	//初始化并返回一个pair,该pair由data和uninitialized_copy 的返回值构成
	return {data,uninitialized_copy(b,e,data)};
}

allocncopy用尾后指针减去首元素指针,来计算需要多少空间。在分配内存之后它必须在此空间中构造给定元素的副本。
它是在返回语句中完成拷贝工作的,返回语句中对返回值进行了列表初始化(参见6.3.2节,第 203页)。返回的pair的first 成员指向分配的内存的开始位置;second成员则是uninitialized_copy的返回值,此值是一个指针,指向最后一个构造元素之后的位置。
free 成员
free成员有两个责任:首先destroy元素,然后释放strVec自己分配的内存空间。for 循环调用allocator的destroy成员,从构造的尾元素开始,到首元素为止逆序销毁所有元素:

void StrVec::free()
{
	//不能传递给deallocate一个空指针,如果elements为0,函数什么也不做
	if (elements)
	{
		//逆序销毁旧元素
		for(auto p=firstfree;p!=elements;/*空*/)
		alloc.destroy(--p);
		alloc.deallocate(elements,capelements);
	}
}

destroy函数会运行 string的析构函数。string的析构函数会释放 string 自己分配的内存空间。
一旦元素被销毁,我们就调用 deallocate 来释放本 strVec 对象分配的内存空间。我们传递给 deallocate的指针必须是之前某次 allocate 调用所返回的指针。因此,在调用 deallocate之前我们首先检査elements 是否为空。
拷贝控制成员
实现了allocncopy和ree成员后,为我们的类实现拷贝控制成员就很简单了。拷贝构造函数调用alloc_n_copy:

StrVec::StrVec(const StrVec &s)
{
	//调用alloc_n_copy分配空间以容纳与s中一样多的元素
	auto newdata = alloc_n_copy(s.begin(),s.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}

并将返回结果赋予数据成员。alloc_n_copy的返回值是一个指针的pair。其 first成员指向第一个构造的元素,second 成员指向最后一个构造的元素之后的位置。由于alloc_n_copy 分配的空间恰好容纳给定的元素,cap也指向最后一个构造的元素之后的位置。
析构函数调用free:

StrVec::~StrVec(){free();}

拷贝赋值运算符在释放已有元素之前调用alloc_n_copy,这样就可以正确处理自赋值了:

StrVec &StrVec::operator=(const StrVec &rhs)
{
	// 调用 alloc_n_copy分配内存,大小与 rhs 中元素占用空间一样多
	auto data=alloc_n_copy(rhs.begin(),rhs.end());free();
	elements =data.first;
	first_free =cap=data.second;
	return *this;
}

类似拷贝构造函数,拷贝赋值运算符使用alloc_n_copy的返回值来初始化它的指针。
在重新分配内存的过程中移动而不是拷贝元素
在编写reallocate成员函数之前,我们稍微思考一下此函数应该做什么。它应该
(1)为一个新的、更大的string 数组分配内存
(2)在内存空间的前一部分构造对象,保存现有元素
(3)销毁原内存空间中的元素,并释放这块内存
观察这个操作步骤,我们可以看出,为一个strVec重新分配内存空间会引起从旧内存空间到新内存空间逐个拷贝string。虽然我们不知道string的实现细节,但我们知道 string 具有类值行为。当拷贝一个string时,新string和原 string 是相互独立的。改变原string不会影响到副本,反之亦然。
由于 string 的行为类似值,我们可以得出结论,每个string 对构成它的所有字符都会保存自己的一份副本。拷贝一个string 必须为这些字符分配内存空间,而销毁一个string 必须释放所占用的内存。
拷贝一个 string就必须真的拷贝数据,因为通常情况下,在我们拷贝了一个string之后,它就会有两个用户。但是,如果是reallocate拷贝strVec中的string,则在拷贝之后,每个 string只有唯一的用户。一旦将元素从旧空间拷贝到了新空间,我们就会立即销毁原 string。
因此,拷贝这些string中的数据是多余的。在重新分配内存空间时,如果我们能避免分配和释放string的额外开销,strVec的性能会好得多。
移动构造函数和 std::move
通过使用新标准库引入的两种机制,我们就可以避免string的拷贝。首先,有一些标准库类,包括 string,都定义了所谓的“移动构造函数”。关于string 的移动构造函数如何工作的细节,以及有关实现的任何其他细节,目前都尚未公开。但是,我们知道移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。而且我们知道标准库保证“移后源”(moved-from)string仍然保持一个有效的、可析构的状态。对于 string,我们可以想象每个string都有一个指向char 数组的指针。可以假定string的移动构造函数进行了指针的拷贝,而不是为字符分配内存空间然后拷贝字符。
我们使用的第二个机制是一个名为move 的标准库函数,它定义在 utility 头文件中。目前,关于 move 我们需要了解两个关键点。首先,当reallocate 在新内存中构造string 时,它必须调用move 来表示希望使用string 的移动构造函数,原因我们将在13.6.1节(第470页)中解释。如果它漏掉了move调用,将会使用string的拷贝构造函数。其次,我们通常不为move提供一个using声明(参见3.1节,第74页),原因将在18.2.3节(第706页)中解释。当我们使用move时,直接调用std::move 而不是move.
reallocate 成员
了解了这些知识,现在就可以编写reallocate成员了。首先调用allocate分配新内存空间。我们每次重新分配内存时都会将strVec的容量加倍。如果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;
}

for循环遍历每个已有元素,并在新内存空间中construct一个对应元素。我们使用dest 指向构造新 string的内存,使用elem 指向原数组中的元素。我们每次用后置递增运算将 dest(和elem)推进到各自数组中的下一个元素。
construct的第二个参数(即,确定使用哪个构造函数的参数(参见12.2.2节,第428页))是move返回的值。调用move返回的结果会令construct 使用string 的移动构造函数。由于我们使用了移动构造函数,这些string管理的内存将不会被拷贝。相反,我们构造的每个string都会从elem指向的string 那里接管内存的所有权。
在元素移动完毕后,我们调用free销毁旧元素并释放strVec原来使用的内存。string成员不再管理它们曾经指向的内存:其数据的管理职责已经转移给新strvec内存中的元素了。我们不知道旧strVec内存中的string包含什么值,但我们保证对它们执行 string的析构函数是安全的。
剩下的就是更新指针,指向新分配并已初始化过的数组了。first_free和cap指针分别被设置为指向最后一个构造的元素之后的位置及指向新分配空间的尾后位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值