某些类需要在运行时分配可变大小的内存空间。这种类通常可以使用标准库容器来保存它们的数据。但某些类需要自己进行内存分配,这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。
StrVec类的设计
我们将使用一个allocator来获得原始内存。由于allocator分配的内存是未构造的,我们将在需要添加新元素时用allocator的construct成员在原始内存中创建对象。当我们需要删除一个元素时,我们将使用destroy来销毁元素。
每个StrVec有三个指针成员指向其元素所使用的内存:
1、elements,指向分配的内存中的首元素。
2、first_free,指向最后一个实际元素之后的位置。
3、cap,指向分配的内存末尾之后的位置。
StrVec还有一个名为alloc的静态成员,其类型为allocator<string>。alloc成员会分配StrVec使用的内存,我们的类还有4个工具函数:
1、alloc_n_copy会分配内存,并拷贝一个给定范围中的元素。
2、free会销毁构造的元素并释放内存。
3、chk_n_alloc保证StrVec至少有容纳一个新元素的空间。如果没有空间添加新元素,chkj_n_alloc会调用reallocate来分配更多内存。
4、reallocate在内存用完时为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 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_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; //指向数组尾后位置的指针
};
使用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);
}
alloc_n_copy成员
我们在拷贝StrVec时,可能会调用alloc_n_copy成员。类似vector,我们的StrVec类有类值的行为,当我们拷或赋值StrVec时,必须分配独立的内存,并从原StrVec对象拷贝元素至新对象。
alloc_n_copy成员会分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中。此函数返回一个指针的pair,两个指针分别指向新空间的开始位置和拷贝的尾后位置:
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)};
}
free成员
free成员有两个责任:首先destroy元素,然后释放StrVec自己分配的内存空间。
void StrVec::free()
{
//不能传递给deallocate一个空指针,如果elements为0,函数什么也不做
if(elements) {
//逆序销毁旧元素
for(auto p=first_free;p!=elements; /*空*/)
alloc.destroy(--p);
alloc.deallocate(elements,cap-elements);
}
}
拷贝控制成员
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;
}
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;
}
在重新分配内存的过程中移动而不是拷贝元素
在重新分配内存空间时,拷贝这些数据是多余的,我们需要避免分配和释放string的额外开销,而采用移动数据的方法,这样会提高类的性能。
移动构造函数和std::move
有一些标准库类,定义了所谓的“移动构造函数”。还有一个名为move的标准库函数,它定义在utility头文件中,我们必须调用move来表示希望使用string的移动构造函数,我们通常不为move提供一个using声明,当我们使用move时,直接调用std::move而不是move。
reallocate成员
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;
}