C++ STL标准库解析|⭐️vector容器的空间配置器allocator

书接上回:C++ STL标准库解析|简单实现STL容器vector代码和容器空间配置器

之前在文末,我们三大灵魂拷问,空间配置器是什么?为什么要空间配置器?没有会发生什么呢?
本篇文章我们就从这三个问题入手,然后实现空间配置器。

本节导读:

没有空间配置器会发生什么?

擅自调用构造函数

我们来测试这样一个事件,定义一个类,然后我们声明一下存储Test类的vector容器,然后输出看打印:
在这里插入图片描述
我们竟然构造了10个Test对象,并且在最后进行了析构,这显然不是我们期望的。

正常的逻辑应该是,它应该是开辟10个存放Test对象的容器才对,他不应该擅作主张得对类进行构造函数调用。

这是为什么呢?
我们来看vector容器对构造函数:

    vector(int size = 10) {
        _first = new T[size];
        _last = _first;
        _end = _first + size;
    }

我们使用了new!众所周知,new不仅仅为我们开辟内存空间,他还会为我们调用对应类类型的构造函数!所以这显然是不符合期望的。

我们可以想到一个很简单的处理思路:
在构造函数中需要把内存开辟和随想构造分开处理

擅自调用析构函数

我们看到,我们把Test类对象析构了10次,但是我们的场景应该是这样的:
在这里插入图片描述
我们的数组开辟的空间可能很大,但是我们应该仅仅析构有效元素,而不是开辟多大空间就析构多少个,那也太离谱了。

这又是为什么呢?

    ~vector() {
    	delete[] _first;
    	_first = _last = _end =nullptr
    }

原来是因为我们直接用了delete!delete会自动去调用析构函数,所以我们开辟了多大的内存,这个vector就回去析构多少类对象。这是绝对不被允许的!

在析构函数中应该析构容器有效的元素,然后释放_first指向的堆内存

push_back和pop_back逻辑错误

我们进行一下测试:

int main () {
    Test t1, t2, t3;
    std::cout << "-------------------------" << std::endl;
    vector<Test> vec;
    vec.push_back(t1);  //先把这些对象推进去
    vec.push_back(t2);
    vec.push_back(t3);
    std::cout << "-------------------------" << std::endl;
    vec.pop_back();     
    std::cout << "-------------------------" << std::endl;

    return 0;
}

我们期望以上代码发生什么样的事情呢?

我们使用vec.push_back(t1);我们希望在容器第一个位置上构造一个新的具体的对象t1;
t2, t3以此类推。
我们使用vec.pop_back();我们希望析构容器的最后一个有效元素。

vec.push_back(t1)
但是代码明显不符合我们的预期,因为我们在vector<Test> vec的时候已经在容器的每一个位置上都放了一个未知的Test()对象,而我们现在vec.push_back(t1);操作,相当于在底层已有的Test()进行一个赋值操作,不符合预期!我们容器生成的时候底层应该只有内存而没有对象

vec.pop_back()
容器底层的类对象已经管理一个外部资源了,而我们现在的pop_back只是把last指针做了一个–。下一次我们进行push_back操作会直接覆盖原来的类对象,那原类对象管理的资源可不就成野指针了?因为他没有做正常的析构啊!

为了验证以上猜想,我们看一下代码执行结果:
在这里插入图片描述
很明显,我们可以看到,当我们调用pop_back()真正想要删除一个类对象的时候,却没有调用析构函数。所以说pop_back必须实现析构对象的操作,而且它还不能去释放那个内存。

总结:
其实最核心的问题就是,我们在容器空间的开辟和释放使用了new和delete。但是我们的需求又必须要求内存开辟和对象构造要分开;析构时只析构容器的有效元素,然后释放堆内存,pop_back必须析构对象,还不能释放内存。
如何解决?
答案就是写容器的空间配置器allocator

写自己的空间配置器

容器的空间配置器主要做四件事情:

  • 内存开辟
  • 内存释放
  • 对象构造
  • 对象析构
template<typename T>
class Allocator {

}

实现内存开辟

只搞个内存开辟,很明显就是用 malloc

    T* allocator(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);
}

在指定的地址里面进行对象的构造

实现对象析构

void destroy(T *p) {
	p->~T();
}

一定只进行对象的析构!~T()代表了T类型的析构函数

总结:
请其底层内存开辟、内存释放、对象构造和析构都应该通过空间配置器来实现

改造我们的vector容器

首先我要说明的是,我们的这个vector类模版的定义还是有点讲究的。

template <typename T, typename Alloc = Allocator<T>>
class vector {
}

这里的typename Alloc = Allocator<T>是一个模版参数的默认值设置,他的意思是当你在实例化模板时不显式地提供一个类型为 Alloc 的参数时,编译器会使用 Allocator<T>作为默认值

  1. typename Alloc:这里声明了一个模板参数,名为 Alloc,它表示一个类型。typename 关键字在这里是必需的,因为 Alloc 是一个类型名。如果没有 typename,编译器会将 Alloc 解释为一个非类型参数。
  2. Allocator<T>:这是一个类型表达式,表示一个分配器类型,用于为模板中的对象动态分配内存。在模板参数中,Alloc 被初始化为 Allocator<T>,意味着如果你不提供分配器类型作为模板参数,编译器会默认使用 Allocator<T> 作为分配器。

然后我们需要在成员属性里面添加空间配置器

class vector {
private:
    Alloc _allocator; //定义容器的空间配置器对象
}

重写构造函数

如果构造函数定义成:

vector(int size = 10, 
	   const Alloc &alloc = Allocator<T>)
	   :_allocator(alloc)

这样就允许用户传递自己定义的空间配置器,为了简单起见,我们这里就不写了。

首先需要明确的就是我们现在不能使用new来开辟空间了,而是将内存开辟和对象构造分开(借助刚才写的空间配置器):

    vector(int size = 10) {
        //_first = new T[size];
        //这里只做内存的开辟,底层是malloc
        _first = _allocator.allocate(size);
        _last = _first;
        _end = _first + size;
    }

重写析构函数

析构函数包括两部分,首先析构有效元素,然后释放堆上的内存。(之前直接用的delete,直接把堆上的所有内存都当成类对象来析构)

    ~vector() {
        //delete[] _first;
        for(T *p = _first; p != _last; ++p) {
            _allocator.destroy(p);  //把_first指针指向的数组的有效元素进行析构操作
        }
        _allocator.deallocate(_first); //释放堆上的内存
        _first = _last = _end =nullptr;
    }

重写拷贝构造

之前我们的拷贝构造就是很直接的 _first = new T[size];然后在for循环里面将当前指针指向源资源的指针,来进行有效元素的拷贝。

现在我们需要首先开辟内存,然后在有效空间范围内做类对象的构造:

    vector(const vector<T> &rhs) {
        int size = rhs._end - rhs._first;
        //_first = new T[size];
        _first = _allocator.allocate(size);
        int len = rhs._last - rhs._first;
        for (int i = 0; i < len; ++i) {
            //_first[i] = rhs._first[i];
            _allocator.construct(_first + i, rhs._first[i]);
        }
        _last = _first + len;
        _end = _first + size;
    }

重写赋值运算符

跟拷贝构造同理,前面再加上一个析构有效元素数组的操作(之前我们直接delete掉被赋值的原内存块)。

    vector<T>& operator=(const vector<T> &rhs) {
        if (this == rhs) return *this;
        
        //delete[]_first;
        for(T *p = _first; p != _last; ++p) {
            _allocator.destroy(p);  //把_first指针指向的数组的有效元素进行析构操作
        }
        _allocator.deallocate(_first); //释放堆上的内存

        int size = rhs._end - rhs._first;
        //_first = new T[size];
        _first = _allocator.allocate(size);
        int len = rhs._last - rhs._first;
        for (int i = 0; i < len; ++i) {
            //_first[i] = rhs._first[i];
            _allocator.construct(_first + i, rhs._first[i]);
        }
        _last = _first + len;
        _end = _first + size;

        return *this;
    }

重写push_back

之前使用的 *_last++ = val;
现在我们应该是在有效元素最后一个位置进行一个类对象的构造:

    void push_back(const T &val) {
        if (full()) expand();
        //*_last++ = val; _last指针指向的内存构造一个值为val的对象
        _allocator.construct(_last, val);
        _last++;
    }

重写pop_back

之前我们单纯的做了一个 --_last ,这是绝对不允许的,当我们pop_back,类对象应该被析构才符合预期:

    void pop_back() {
        if (empty()) return ;
        //--_last; 不仅要把_last--,还需要析构删除的元素
        --_last;
        _allocator.destroy(_last);
    }

重写内部扩容expand函数

这里我们要先对进行内存的开辟,然后对有效元素进行构造,再然后析构原来位置的类对象,最后释放原内存空间:

    void expand() { //容器的二倍扩容操作
        int size = _end - _first;
        //T *ptemp = new T[2 * size];
        T *ptemp = _allocator.allocate(2 * size);

        for (int i = 0; i < size; ++i) {
            //ptemp[i] = _first[i];
            _allocator.construct(ptemp + i, _first[i]);
        }
        //delete[] _first;
        for (T *p = _first; p != _last; ++p) {
            _allocator.destroy(p);
        }
        _allocator.deallocate(_first);
        _first = ptemp;
        _last = _first + size;
        _end = _first + 2 * size;
    }

实现效果

还记得之前发生了什么吗?

现在我们重新做一次测试,测试代码跟前面的一样:
在这里插入图片描述
前三个构造就是 t1 t2 t3 的构造函数

然后我们定义vector<Test> vec;我们只开辟空间,不进行类对象的构造。

把对象push_back进vector,调用的是空间配置器的construct方法,在指定内存上构造函数,这里调用的是拷贝构造。

最后调用pop_back是删除末尾元素,结果上也能看出我们析构了Test类对象,这里析构的是vector容器中的t3(该对象在堆上)。

最后程序结束,开始析构类对象,依次是vector中的t2, t1(他们两个在堆上)和之前我们初始化的 t3, t2, t1(他们都在栈上)。一共需要调用析构函数5次,符合预期。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值