一、vector概览
上一篇博客中我们介绍了string的简化实现,今天我们来介绍另一个STL当中比较重要的容器------vector。
private 字段:数据结构
在我们的实现中,vector作为通用容器需要兼容不同的数据/对象类型,所以我们需要用模板这个特性,为此,我们涉及data_字段为T*类型,其中T是模板名,内存中为数组所分配的内存长度是capacity_,实际长度是size_。
public 方法:对外接口
对外接口的实现上,重点考虑push_back()和pop_back()。的实现需要考虑左右值的兼容,为此,我们用到了完美转发、万能引用,还需要考虑扩容的逻辑。pop_back方法的实现比较简单,就是要记得调用容器中对象的析构函数。
Rule of five
我们的vector实现有对堆上的资源进行管理,所以需要rule of five,但是只实现析构函数;由于只使用index找值,且只调用push_back和pop_back,所以拷贝构造、移动构造、拷贝赋值、移动赋值可以都禁止掉。
二、vector实现
我们使用默认构造和默认成员初始化的办法,这样避免过度复杂化实现:
cpp
class vector {
public:
vector() = default;
private:
T* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
};
现在考虑实现析构函数,析构函数的作用有两个:1.逐个调用容器中对象的析构函数,清空容器;2.释放已分配的内存。这里注意clear()方法是STL vector的公有方法,所以单独拿出来。释放内存的实现使用operator detele,因为之前clear方法已经调用了析构函数了。
cpp
~vector() {
clear(); //清空容器
deallocate(); //释放内存
}
void clear() {
for (size_t i = 0; i < size_; i++) {
data_[i].~T();
}
size_ = 0;
}
private:
void deallocate() {
::operator delete(data_);
data_ = nullptr;
capacity_ = 0;
}
这里顺便实现一下allocate():注意我们的vector实现中构造与内存分配总是采取分离的策略,所以也使用operator new。operator new返回的类型是void*,所以需要类型转换
cpp
T& allocate() {
return static_cast<T*>(::operator new(capacity_ * sizeof(T)));
}
现在重头戏来了,让我们来实现push_back()方法。
push_back要考虑到左右值两种的情况。我们这里使用emplace_back作为统一的入口,用变参模板+完美转发+万能引用保留参数数目和值类别;emplace_back:在扩容后使用placement new原地构造对象。
cpp
void push_back(const T& value) {emplace_back(value);}
void push_back(T&& value) {emplace_back(std::move(value));}
template<typename ...Args>//变参模板
T& emplace_back(Args&&... args) {//万能引用
if (size_ == capacity_) {
reallocate(capacity_ == 0 ? 1 : capacity_ * 2);//扩容
}
new (data_ + size_) T(std::forward<Args>(args)...);//placement new
return data_[size_++]; //提前位移,为下一个emplace对象腾位置
}
现在来实现reallocate这个函数:
- 分配新内存
- 复制数据到新内存(placement new)
- 如果抛异常需要清空新内存的数据
- 清空原内存数据,释放内存
- 设置新的cap、sz,data指向新内存
cpp
void reallocate(size_t new_cap) {
T *new_data = allocate(new_cap); //分配新内存
size_t new_size = 0;
try {
for (; new_size < size_; new_size++) {//placement new + 移动优化
new (new_data + new_size) T(std::move_if_noexcept(data_[new_size]));
}
} catch (...) { //中间抛出异常时清空、释放
for (size_t i = 0; i < new_size; i++) {
new_data[i].~T();
}
::operator delete(new_data(i));
throw; //rethrow,向上传播异常
}
clear();
deallocate();
size_ = new_size;
capacity_ = new_cap;
data_ = new_data;
}
接下来实现pop_back(),popback比较简单,就是将最后一个元素从data中删去,调析构、size--即可。我们顺便实现back方法,pop_back一般会配合back使用。
cpp
T& back() {return data_[size_-1];}
const T& back() const {return data_[size_-1];} //const对象的情况要特别处理
void pop_back() {
if (size_ > 0) {
data_(--size_).~T();
}
}
现在只剩下一些比较简单的方法还没实现:
cpp
T& operator[](size_t index) noexcept {return data_[index];}
const T& operator[](size_t index) const noexcept {return data_[index];}
size_t size() {return size_;}
size_t capacity() {return capacity_;}