STL空间配置器的理解
Qingyh Lv2

这里以自定义的vector类为例,谈谈为什么需要空间配置器。

自定义实现一个简单的vector

image
这里定义了三个成员变量,分别是_first(vector起始地址),_last(vector实际存放的末尾元素地址),_end(vector开辟的空间地址)。
使用模板类封装实现了vector,实现了以下函数:

  • vector构造函数,使用new开辟内存+构造对象
  • 析构函数,使用delete删除内存
  • 拷贝构造函数
  • 赋值构造函数
  • push_back()函数,从vector最后插入
  • pop_back()函数,从vector最后弹出
  • back(),获取末尾元素
  • empty(),判断是否为空
  • full(),判断是否满
  • size(),获取元素大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
template<typename T>
class vector
{
public:
vector(int size = 5)
{
_first = new T[size];
_last = _first;
_end = _first + size;
}
~vector()
{
delete[]_first;
_first = _last = _end = nullptr;
}
vector(const vector<T>& cp)//拷贝构造函数
{
int size = cp._end - cp._first;
_first = new T[size];
int len = cp._last - cp._first;
for (int i = 0; i < len; i++)//这里是深拷贝
{
_first[i] = cp._first[i];
}
_last = _first + len;
_end = _first + size;
}

vector<T>& operator=(const vector<T>& cp) //赋值构造函数
{
//1.防止自赋值
if (this == &cp)
return*this;
//2.删除原有空间
delete[]_first;

//创建新的空间并执行深拷贝
int size = cp._end - cp._first;
_first = new T[size];
int len = cp._last - cp._first;
for (int i = 0; i < len; i++)
{
_first[i] = cp._first[i];
}
_last = _first + len;
_end = _first + size;
return *this;
}
void push_back(const T& val)
{
//如果空间满了,则扩容
if (full())
expand();
*_last++ = val;
}
void pop_back()
{
//判断是否还有值
if (empty())
return;
--_last;
}
T back() const
{
//返回最后一个元素
if (empty())
throw "stack is empty"; //抛出异常
return *(_last - 1);
}
bool empty() const{ return _first == _last; } //判断是否为空
bool full() const { return _last == _end;} //判断是否已满
int size() const { return _last - _first; }//计算实际存储的数据大小
private:
T* _first; //指向数组起始的位置
T* _last; //指向数组中有效元素的后继位置
T* _end; //指向数组的结尾

void expand() //扩容操作
{
//1.重新构造
int size = _end - _first;
T* ptmp = new T[2 * size];
//2.深拷贝
for (int i = 0; i < size; i++)
{
ptmp[i] = _first[i];
}
//3.删除原来的内存空间
delete[]_first;


_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
}
};

可以实现vector简单的插入、弹出、扩容等操作。
运行下面的代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Test
{
public:
Test() { cout << "Test()" << endl; }
~Test() { cout << "~Test()" << endl; }
Test(const Test&)
{
cout << "拷贝构造" << endl;
}

Test& operator= (const Test& cp)
{
cout << "赋值构造" << endl;
return *this;
}
};

int main()
{
Test t1, t2, t3; //调用赋值构造函数,打印Test();
vector<Test> vec;//创建一个大小为size的空间,这里默认为5,并且这里会调用默认vector构造函数构造对象
cout << vec.size() << endl; //理论上的有效空间为0,但这里已经有数据了;
vec.push_back(t1); //这里调用的是打印的是赋值构造函数,该处原来有值(定义的时候生成的空对象)
vec.push_back(t2);
vec.push_back(t3);
cout << vec.size() << endl; //有效空间为3
vec.pop_back(); //调用析构函数
cout << vec.size() << endl; //有效空间为2
return 0;
}

image
存在以下问题
1.在初始化的时候,开辟内存和对象构造是一起执行的;但是有些时候我只是为了声明变量(如vector<Test> vec,在这里只是为了声明定义以Test类的vec数组,但是上图是开辟内存+构造对象),这样就会带来没必要的开销(声明时构造对象,出作用域时析构对象);
2.上述实现的vector的pop_back是采用的指针回退策略,但如果该指针指向的test对象指向了外部资源,单纯的指针回退会导致这一部分资源无法管理,导致空间泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Test
{
public:
Test()
{
_ps=new int[5];
}
~Test()
{
delete []_ps;
_ps=nullptr;
}

private:
int *_ps;
};

这里定义的Test类中是指向了外部对象的(_ps=new int[5];),如果仅仅是指针回退,而不析构对应的对象的话,会造成管理的内存资源泄露。

空间配置器实现

所以很自然的就是想到,将
内存的开辟和对象的构造分开;
内存的释放和对象的析构分开。
而空间配置器就是为了实现这样的功能,具体为,空间配置器实现了几个比较重要的函数:

  • allocate(),负责内存的开辟
  • deallocate(),负责内存的释放
  • construct(),负责对象的构造
  • destroy(),负责对象的析构

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//容器的空间配置其allocator 做四件事情  
//内存开辟/内存释放 对象构造/对象析构
template <typename T>
struct Allocator
{
T* allocate(size_t size) //负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}

void deallocate(void* p) //负责内存释放
{
free(p);
}

void construct(T* p, const T& val) //负责对象构造
{
new (p) T(val); //采用定位new的方式
}

void destroy(T* p) //负责对象析构
{
p->~T(); //~T()代表了T类型的析构函数
}
};

这样做的好处有,在声明对象的时候只是开辟内存空间,而不进行无意义的默认对象构造;并且在删除资源的时候,先执行对象的析构函数,确保对象指向的外部资源被释放后,才清除当前堆内存,步骤如下。
image

实现空间配置器后的vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/*
* 容器底层的内存开辟、内存释放,对象构造和析构,都通过allocator空间配置器来实现
*/
template<typename T,typename Alloc = Allocator<T>>
class vector
{
public:
vector(int size = 10)
{
//只开辟内存空间
_first = _allocator.allocate(size);
_last = _first;
_end = _first + size;
}
~vector()
{
//先析构容器有效元素
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p); //把内存中的有效元素进行析构操作
}
//然后释放_first指向的堆内存
_allocator.deallocate(_first);
_first = _last = _end = nullptr;
}
vector(const vector<T>& cp)
{
int size = cp._end - cp._first;
//分配堆内存
_first = _allocator.allocate(size);
int len = cp._last - cp._first;
for (int i = 0; i < len; i++)
{
//逐个根据cp的值构造对象
_allocator.construct(_first + i, cp._first[i]);
}
_last = _first + len;
_end = _first + size;
}

vector<T>& operator=(const vector<T>& cp)
{
if (this == &cp)
return*this;

//把内存中的对象进行析构操作
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
//释放原来堆上的内存
_allocator.deallocate(_first);

int size = cp._end - cp._first;
//申请新的内存空间
_first = _allocator.allocate(size);
int len = cp._last - cp._first;
for (int i = 0; i < len; i++)
{
//逐个根据cp的值构造对象
_allocator.construct(_first + i, cp._first[i]);
}
_last = _first + len;
_end = _first + size;
return *this;
}
void push_back(const T& val)
{
if (full())
expand();
//根据val的值在_last位置上构造对象
_allocator.construct(_last, val);
_last++;
}
void pop_back()
{
if (empty())
return;
//析构_last位置上的对象
--_last;
_allocator.destroy(_last);

}
T back() const
{
if (empty())
throw "stack is empty"; //抛出异常
return *(_last - 1);
}
T& operator [] (int index)
{
if (index < 0 || index >= size())
throw "index out of range Exception";
return _first[index];
}
bool empty() const { return _first == _last; }
bool full() const { return _last == _end; }
int size() const { return _last - _first; }
private:
T* _first; //指向数组起始的位置
T* _last; //指向数组中有效元素的后继位置
T* _end; //指向数组的结尾
Alloc _allocator; //定义容器的空间配置器对象

void expand()
{
int size = _end - _first;
//分配原来2倍大小的内存空间
T* ptmp = _allocator.allocate(2 * size);
for (int i = 0; i < size; i++)
{
//逐个根据_first[i]的值构造对象
_allocator.construct(ptmp+i, _first[i]);
//逐个析构_first+i对象
_allocator.destroy(_first+i);
}
//删除原有堆内存空间
_allocator.deallocate(_first);
_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
}
};

image
从上图可以看到,实现了自定义的空间配置器之后,在定义vector<Test> vec; 时就不会出现没必要的对象构造了。

关于空间配置器的作用就先讲解到这了,空间配置器的实现可以参考。
5 千字长文+ 30 张图解 | 陪你手撕 STL 空间配置器源码

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 31k