C++内存篇(四):写一个类似vector的简单动态内存管理类
2020-04-27 本文已影响0人
wangawu121
动态内存管理类
某些类在运行时分配可变大小的内存空间,这种类如果可以的话应该使用标准容器库来保存它的数据。例如用一个vector来管理其底层内存。
但是有些类却需要自己进行内存分配,这样就必须定义自己的拷贝控制成员来管理内存分配。拷贝控制成员包括:
- 拷贝构造函数
- 拷贝赋值赋值运算符(即=运算符)
- 析构函数
下面来实现vector的一个(极度)简化版本,该版本只针对string元素,故将它命名为StrVec
vector的内存分配方法
让我们先回顾一下vector的内存分配方式:
- 预先分配足够的内存,将元素保存在连续的内存中
- 添加元素的成员函数检查是否有空间容纳更多的元素
- 如果有,在下一个可用位置构造新对象
- 如果没有空间了,vector将重新分配空间:获得一块新的更大空间,将原有元素放入新空间,并释放旧空间。
设计自己的类
我们在内存分配上采取和vector类似的策略。
我们来看看我们需要什么类成员来完成一个类似vector的行为:
公有成员
1、关于构造、析构和拷贝赋值的
-
默认构造函数:
StrVec()
-
三个拷贝控制成员:拷贝构造函数、拷贝赋值运算符、析构函数
StrVec(const StrVec&) //拷贝构造函数 StrVec& operator=(const StrVec&) //拷贝赋值运算符 ~StrVec() //析构函数
2、其他功能
void push_back(const string&) //拷贝元素
size_t size() const //返回当前存储着多少个元素
size_t capacity() const //返回当前这个类拥有的内存可以放多少个元素
string* begin() const //返回当前用来存放元素的内存块的首地址
string* end() const //返回当前用来存放元素的内存块的第一个空闲地址
私有成员
1.关于内存控制的
-
内存分配器:
因为直到主程序结束前我们可能一直需要内存分配器来完成一些功能,所以它需要是一个静态变量。
(静态变量一旦声明就会直到主程序结束才被销毁)
static std::allocator<std::string> alloc //用来给该类分配一块string类型的内存
-
一个检查是否还有剩余空间、如果没有就重新分配内存的函数
void chk_n_alloc()
-
用于重新分配内存的函数
void reallocate()
-
分配空间并接受参数拷贝其他内存上的对象的函数:
pair<string*, string*> alloc_n_copy(const string*, const string*)
alloc_n_copy接受两个string指针,分别是要拷贝的string的内存块首地址和尾地址,返回值是一个pair,包含为新分配的内存首地址和末地址。
-
销毁元素并释放内存的函数:
free()
-
重新分配内存:
chk_n_alloc()
检测到空间不够时调用它,获得一块更大的内存,并把原来的对象移动到上面。我们使用move
来完成移动而不是拷贝,如果使用拷贝我们需要把旧的对象拷贝到新的更大的内存上,再释放原有的内存,这样浪费了一遍构造对象过程,而如果使用移动,我们会让原有内存上的对象直接去管理新的内存而不用再构造它们,然后同样会释放原有内存。 -
一些标记内存位置的成员:
string* elements; //指向被分配的内存的首地址
string* first_free; //指向数组第一个空闲元素的指针
string* cap; //指向被分配的内存的末地址后一位
image-20200427222101809.png
代码
#include<iostream>
#include<utility>
using namespace std;
//动态内存管理类
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 std::allocator<std::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; //指向数组尾后位置的指针
};
//___________________________内存控制___________________________________
//alloc_n_copy:
//参数为拷贝对象的首地址和末地址
//返回值为新分配的内存首地址和末地址
pair<string*,string*>
StrVec::alloc_n_copy(const string* b, const string* e) {
//分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
//初始化并返回一个pair
//该pair由data和uninitialized_copy的返回值
//(该返回值是一个指针),指向最后一个构造元素的位置
//uninitialized_copy以指针data为首地址,把内存地址b到内存地址e上的对象拷贝到data这里
return { data, uninitialized_copy(b,e,data) };
}
void StrVec::push_back(const string& s){
chk_n_alloc(); //确保有空间容纳新元素
//再first_free指向的元素中构造s的副本
alloc.construct(first_free++, s);
}
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;
}
//析构函数:调用free
StrVec::~StrVec() { free(); }
//拷贝赋值运算符
StrVec &StrVec::operator=(const StrVec &rhs){
//调用alloc_n_copy分配内存,大小与rhs中元素占用空间一样多
auto data = alloc_n_copy(rhs.begin(), rhs.end());
elements = data.first;
first_free = cap = data.second;
return *this;
}
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)
//调用move会使construct使用string的移动构造函数而不是拷贝构造函数
alloc.construct(dest++,std::move(*elem++));
//移动完元素就释放旧空间
free();
//更新数据结构,执行新单元
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}