【C++ Allocator】 详解C++的空间配置器和vector的底层实现以及push_back()和empalce_back()的区别

空间配置器

用于管理动态内存分配和释放,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类型,因为这是给定好的(这里有点不确定)

完美解决

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值