空间配置器
用于管理动态内存分配和释放,STL 容器类(如 std::vector
, std::list
, std::map
等)都使用配置器来管理内存。它有非常重要的特点:
- 容器的内存开辟和对象构造要分离开
- 容器的对象析构和内存释放要分离开
这样能够高效的插入元素以及删除元素
vector
STL中典型的容器vector采用标准空间配置器进行初始化容器,对象在元素中的构造,元素的析构,以及容器内存的释放,运用了模板、定位new等技术
代码实现,ps:建议自己手敲两遍, 加深自己对模板、new、malloc、free、指针、拷贝构造以及赋值运算符重载函数的理解,个人认为这是非常好的代码示例的学习,运用了多个技术
#include <iostream>
template<typename T>
class Allocator
{
public:
// 没有实现构造(不需要),自动采用默认构造
// 内存开辟
T* allocate(size_t size) //size_t 通常指usigned long long
{
// 使用malloc开辟内存,而不是new,因为new除了开辟内存还会构造无效的对象,其次还会为了兼容C,并且new和delete带来的异常处理的开销
// static_cast类型的安全转换
return static_cast<T*>(malloc(sizeof(T) * size));
}
// 内存释放
void deallocate(T* p)
{
// 直接free即可,小问题:为什么free传入p就可以释放指定大小的内存呢
free(p);
}
// 元素也就是对象的构造
void construct(T* p, const T& val)
{
// 采用了定位new,在已分配的指定的内存区域上构造一个对象,使用的是拷贝构造,这个拷贝构造比较重要,等会会涉及很多知识
new (p) T(val);
}
// 析构元素,调用元素的析构方法即可
void destroy(T* p)
{
delete p->~T();
}
};
// 这儿需要传入模板参数类型,后者已经给出默认的空间配置器了,所以这就是为什么我们使用的时候只用
// vector<int> 而不是 vector<int, Allocator<T>>
template<typename T, typename Alloc = Allocator<T>>
class MyVector
{
public:
// 通常对于拥有成员变量的类,都要自己实现一下默认构造,对成员变量初始化,所以Allocator就没有,不用给
MyVector(): first_(nullptr), last_(nullptr), end_(nullptr) {}
MyVector(size_t size = 10)
{
first_ = alloc_.allocate(size);
last_ = first_;
// 注意 first_ + size 是很巧妙的,因为first_是 T*,所以 + size,实则是 + size个T对象的位数
end_ = first_ + size;
}
//没有拷贝,即只允许在初始化后进行reserve
void reserve(size_t size = 10)
{
if (!empty()) return;
// 释放之前分配的内存(如果有的话)
if (first_ != nullptr)
{
// 销毁现有的对象
for (T* p = first_; p != last_; ++p) {
alloc_.destroy(p);
}
// 释放内存
alloc_.deallocate(first_);
}
// 分配新内存
first_ = alloc_.allocate(size);
last_ = first_;
end_ = first_ + size;
}
~MyVector()
{
for (T* p = first_; p != last_; p++)
{
alloc_.destroy(p);
}
alloc_.deallocate(first_);
first_ = last_ = end_ = nullptr;
}
// 左值引用参数的拷贝构造
MyVector(const MyVector<T>& lhs)
{
int size = lhs.end_ - lhs.first_;
int len = lhs.last_ - lhs.first_;
first_ = alloc_.allocate(size);
for (int i = 0; i < len; ++i)
{
alloc_.construct(first_ + i, lhs.first_[i]);
}
last_ = first_ + len;
end_ = first_ + size;
}
//左值引用参数的赋值运算符重载函数,注意要先析构原先的元素和释放原来的内存
MyVector operator=(const MyVector<T>& lhs)
{
if (this == lhs) return this;
for (T* p = first_; p < last_; p++)
{
alloc_.destroy(p);
}
alloc.deallocate(first_);
int size = lhs.end_ - lhs.first_;
int len = lhs.last_ - lhs.first_;
first_ = alloc_.allocate(size);
last_ = first_ + len;
end_ = first_ + size;
for (int i = 0; i < len; i++)
{
alloc_.construct(first_ + i, lhs.first_[i]);
}
return *this;
}
// 这儿的参数可以接收已经构造好的对象和临时对象,但val是左值,所以在construct会调用对象的左值拷贝构造
// 实则也可以传入对象构造所需的参数,则是另一种写法,后面会给出
void push_back(const T& val)
{
if (full())
{
expand();
}
alloc_.construct(last_, val);
last_++;
}
void pop_back()
{
if (empty()) return;
--last_;
alloc_.destroy(last_); //注意last_处是没有元素,所以需要先--,在析构即可
}
bool full() const {return last_ == end_};
bool empty() const {return frist_ = last_;}
int size () const {return last_ - first_};
int capacity() const {return end_ - first_;}
private:
T* first_;
T* last_;
T* end_;
Alloc alloc_;
// 这是扩容,注意这里和赋值刚好相反,因为要全部拷贝到新开辟的内存区域中,所以是先拷贝再析构原来的元素和释放原来的内存
void expand()
{
int size = end_ - first_;
int len = last_ - first_;
T* newFirst = alloc_.allocate(2 * size); // 2倍扩容,实际vector底层就是这样的
for (int i = 0; i < len; i++)
{
alloc_.construct(newFirst + i, first_[i]);
}
for (int i = 0; i < len; i++)
{
alloc_.destroy(first_ + i);
}
alloc.deallocate(first_);
first_ = newFirst;
last_ = first_ + len;
end_ = first_ + size;
}
};
以上代码有个引申的点:push_back接收的参数实参类型可以是已经构造好的对象或者临时对象,但由于val肯定是左值,所以只会调用左值引用参数的拷贝构造函数,那么存在两个问题:
1:如何调用右值引用参数的拷贝构造函数呢,难不成还要写一个void push_back(T&& val),可以但太冗余了,当然也可以引用折叠,所以主要是第二个问题
2:如果传入的是构造对象所需的参数呢
如何解决,涉及以下内容
push_back()和emplace_back()
两者在插入对象构造所需的参数时
class Test1 {
public:
Test1(int value) : value(value) {
std::cout << "Test1(int)" << std::endl;
}
Test1(const Test1& other) : value(other.value) {
std::cout << "Test1(const Test1&)" << std::endl;
}
Test1(Test1&& other) noexcept : value(other.value) {
std::cout << "Test1(Test1&&)" << std::endl;
other.value = 0;
}
private:
int value;
};
int main()
{
vec1.push_back(10);
vec1.emplace_back(10);
return 0;
}
push_back:在插入前会先构造出对象,然后通过左值或右值拷贝构造到容器末尾,利用了定位new运算符
emplace_back:直接在容器末尾构造对象,使用了定位new运算符,并且使用了可变参数模板来处理传入多个隐式参数的情况
当我查看源码时,发现C++11以后的push_back都是由emplace_back实现的,绷不住了,估计其他也一样
那我们来看看传入构造所需的参数时,push_back也就是emplace_back是怎么实现的
通过可变参模板实现的,代码如下:
Allocator中:
template<typename... types>
void construct(T* p, type&&... args)
{
// 精华所在:args若只是一个参数,并且是T类型的那么根据其是左值还是右值,直接调用相应的T类型的左值拷贝或者右值拷贝
// 若args是T类型造函数所需的一系列参数,那么也会调用T的构造函数进行对象的构造
new (p) T(std::forward<types>(args)...);
}
Myvector中:
template<typename... Types>
void emplace_back(Types&&... args) //引用折叠
{
if (full())
{
expand();
}
// 完美转发,这些都是配合使用的,非常精妙
alloc_.construct(last_, std::forward<Types>(args)...);
}
此外,要使用引用折叠,必须使用新的模板参数,不能用之前的T类型,因为这是给定好的(这里有点不确定)
完美解决