从零实现一个完整的vector类:深入理解C++动态数组

从零实现一个完整的vector类:深入理解C++动态数组

在C++编程中,vector是最常用的容器之一,它提供了动态数组的功能。今天我们将从实现一个相对完整的vector类来深入理解vector的封装与使用,并在过程中解决各种实际问题。

整体代码框架

首先,让我们看看vector类的整体结构:(用这份伪代码进行理解性默写手撕可以加深对vector的理解)ps:完整源码在文章最下方

cpp

namespace ddd
{
    template<class T>
    class vector
    {
    public:
        // 迭代器相关
        typedef T* iterator;
        typedef const T* const_iterator;
        
        // 构造函数和析构函数
        vector();
        vector(int n, const T& val = T());
        template<class InputIterator>
        vector(InputIterator first, InputIterator last);
        vector(const vector<T>& v);
        vector<T>& operator=(vector<T> v);
        ~vector();
        
        // 容量相关
        size_t size() const;
        size_t capacity() const;
        void reserve(size_t n);
        void resize(size_t n, const T& val = T());
        
        // 元素访问
        T& operator[](size_t pos);
        const T& operator[](size_t pos) const;
        
        // 修改操作
        void push_back(const T& x);
        void pop_back();
        void swap(vector<T>& v);
        iterator insert(iterator pos, const T& x);
        iterator erase(iterator pos);
        
        // 迭代器方法
        iterator begin();
        iterator end();
        const_iterator cbegin() const;
        const_iterator cend() const;
        
    private:
        iterator _start;        // 指向数据块的开始
        iterator _finish;       // 指向有效数据的尾
        iterator _endOfstorage; // 指向存储容量的尾
    };
}

构造函数:创建vector的不同方式

默认构造函数

cpp

vector() : 
_start(nullptr)
, _finish(nullptr)
, _endOfstorage(nullptr)
{}

最简单的构造函数,用初始化列表创建一个空的vector,所有指针都初始化为nullptr。

指定大小和初始值的构造函数

cpp

vector(int n, const T& val = T()) {
    reserve(n);
    for(size_t i = 0; i < n; i++) {
        push_back(val);
    }
}

这个构造函数创建包含n个元素的vector,每个元素都初始化为val。如果val没有提供,就使用T类型的默认值(缺省参数)。

可能看不懂T():

T()表示调用模板参数T类型的默认构造。

迭代器范围构造函数

template<class InputIterator>
vector(InputIterator first, InputIterator last) {
    while(first != last) {
        push_back(*first);
        ++first;
    }
}

关键理解:比方说我们有一个数组arr,里面有arr[1,2,3,4,5]。

我们可以不用知道*first具体是什么,只需利用模板的威力:编译时只需传入对应指针,它就会为每种迭代器生成对应的代码,例如传入(arr,arr+5),它就自动将arr的内容用于构造一个内容基本一致的vector。

这个构造函数可以从任何迭代器范围创建vector,比如数组、list等其他容器的迭代器。

拷贝构造函数

cpp

vector(const vector<T>& v) {
    reserve(v.capacity());
    for(auto& e : v) {
        push_back(e);
    }
}

创建一个与参数v相同的vector,需要分配相同大小的内存并复制所有元素。

用法示例:v1(v2);即使用v2的内容直接构造一个对象v1。

这一个构造类似于赋值

析构函数:资源清理

cpp

~vector() {
    delete[] _start;
    _start = _finish = _endOfstorage = nullptr;
}

析构函数负责释放vector动态分配的内存,并将所有指针设为nullptr,防止悬空指针。

迭代器:让vector支持范围遍历

cpp

iterator begin() { return _start; }
iterator end() { return _finish; }
​
const_iterator cbegin() const { return _start; }
const_iterator cend() const { return _finish; }

end()返回的是_finish,而_finish指向最后一个元素的下一个位置(也就是说_finish和end()本质差不多)

容量操作:管理内存大小

获取大小和容量

size_t size() const { return _finish - _start; }
size_t capacity() const { return _endOfstorage - _start; }

通过指针相减来计算元素个数和总容量,既简单又高效。

size()可以理解为表示当前vector当中存储的数据量,capacity()表示vector可以存储的最大量

预留空间

cpp

void reserve(size_t n) {
    if (n > capacity()) {
        T* tmp = new T[n];
        size_t sz = size();
​
        for(size_t i = 0; i < sz; i++) {
            tmp[i] = _start[i];
        }
​
        delete[] _start;
        _start = tmp;
        _finish = _start + sz;
        _endOfstorage = _start + n;
    }
}

关键点:只有在请求的容量大于当前容量时才重新分配内存,避免不必要的(过多的)内存操作。

注意此操作会导致_start的位置有所改变。(此处不用纠结,后面会再次提到)

调整大小

cpp

void resize(size_t n, const T& val = T()) {
    if (n <= size()) {
        _finish = _start + n;  // 缩小size
    }
    else {
        reserve(n);            // 扩大容量
        while (_finish < _start + n) {
            *_finish = val;    // 用val填充新增位置
            ++_finish;
        }
    }
}

resize可以增大或减小vector的大小,新增的元素用val初始化。

如果说要增大空间,我们就直接复用reserve()

元素访问:像数组一样使用vector

cpp

T& operator[](size_t pos) {
    assert(pos < size());
    return _start[pos];
}

const T& operator[](size_t pos)const {
    assert(pos < size());
    return _start[pos];
}

通过重载[]运算符,vector可以像普通数组一样使用。assert确保不会越界访问,是一种强制检查,越界就直接报错并指出报错位置,也方便调试。

修改操作:动态改变vector内容

添加和删除元素

cpp

void push_back(const T& x) {
    insert(end(), x);  // 在末尾插入
}

void pop_back() {
    erase(end() - 1);  // 删除最后一个元素
}

在类内部,我们可以直接使用end()等价于_finish,因为end()返回的就是_finish

插入元素

cpp

iterator insert(iterator pos, const T& x) {
    assert(pos >= _start && pos <= _finish);
    if (_finish == _endOfstorage) {
        size_t len = pos - _start;        // 记录相对位置
        reserve(capacity() == 0 ? 4 : capacity() * 2);
        pos = _start + len;               // 重新计算位置
    }
    
    iterator end = _finish - 1;
    while (end >= pos) {
        *(end + 1) = *end;  // 向后移动元素
        --end;
    }
    *pos = x;               // 插入新元素
    ++_finish;
    return pos;
}

关键技巧:在扩容前用len记录插入位置的相对偏移量,然后再用新的_start+len来定位新的pos。原因:因为reserve会改变_start的地址,使原来的迭代器失效。

删除元素

cpp

iterator erase(iterator pos) {
    assert(pos >= _start && pos < _finish);
    iterator it = pos + 1;
    while (it < _finish) {
        *(it - 1) = *it;  // 向前移动元素
        ++it;
    }
    --_finish;
    return pos;
}

注意erase的断言条件与insert不同,因为不能删除end()位置(它没有元素)。

交换操作

cpp

void swap(vector<T>& v) {
    std::swap(_start, v._start);
    std::swap(_finish, v._finish);
    std::swap(_endOfstorage, v._endOfstorage);
}

交换操作只需交换三个指针,非常高效,常用于实现拷贝赋值运算符。

拷贝赋值运算符

cpp

vector<T>& operator= (vector<T> v) {
    swap(v);
    return *this;
}

巧妙设计:使用传值参数,让编译器自动创建临时副本,然后通过交换内容来实现赋值,既简单又安全。

测试我们的vector

cpp

void test_vector() {
    // 测试各种构造函数
    ddd::vector<int> v1;
    ddd::vector<int> v2(5, 10);
    
    int arr[] = {1, 3, 5, 7, 9};
    ddd::vector<int> v3(arr, arr + 5);
    
    // 测试修改操作
    v3.push_back(11);
    v3.insert(v3.begin() + 2, 99);
    v3.erase(v3.begin() + 1);
    
    // 测试容量操作
    v3.resize(10, 100);
    
    // 测试迭代器
    for(auto it = v3.begin(); it != v3.end(); ++it) {
        std::cout << *it << " ";
    }
}

int main() {
    test_vector();
    return 0;
}

总结

通过从头实现vector,我们深入理解了:

  1. 动态内存管理:new/delete的使用和内存分配策略

  2. 迭代器失效:插入删除操作对迭代器的影响

  3. 模板编程:如何编写通用的容器类

  4. 异常安全:通过swap技巧实现安全的赋值操作

  5. 性能优化:合理的扩容策略和内存管理

完整源码:

namespace ddd
​
{
​
  template<class T>
​
  class vector
​
  {
​
  public:
​
    // Vector的迭代器是一个原生指针
    
    typedef T* iterator;
    
    typedef const T* const_iterator;
    
    iterator begin(){
        return _start;
    }
    
    iterator end(){
        return _finish;
    }
    
    const_iterator cbegin() const{
    return _start;
    }
    
    const_iterator cend() const{
    return _finish;
    }
​
​
​
    // construct and destroy
    
    vector(){
        
    }
    
    vector(int n, const T& val = T()){
        reserve(n);
        for(size_t i=0;i<n;i++){
            push_back(val);
        }
    }
    
    template<class InputIterator>
    
    vector(InputIterator first, InputIterator last){
        while(first!=last){
            push_back(*first);
            ++first;
        }
    }
    
    vector(const vector<T>& v){
        reserve(v.capacity());
        for(auto& e:v){
            push_back(e);
        }
    }
    
    vector<T>& operator= (vector<T> v){
        swap(v);
        return *this;
    }
    
    ~vector(){
        delete[] _start;
        _start=_finish=_endOfstorage=nullptr;
    }
    
    // capacity
    
    size_t size() const{
    return _finish-_start;
    }
    
    size_t capacity() const{
    return _endOfstorage-_start;
    }
    
    void reserve(size_t n) {
        if (n > capacity()) {
            T* tmp = new T[n];
            size_t sz = size();
            if (_start) {
                for (size_t i = 0; i < sz; i++) {
                    tmp[i] = _start[i];
                }
    
                delete[] _start;
            }
​
​
            _start = tmp;
            _finish = _start + sz;
            _endOfstorage = _start + n;
        }
    }
    
    void resize(size_t n, const T& val = T()) {
    
        if (n <= size()) {
            _finish = _start + n;
        }
        else {
            reserve(n);
            while (_finish < _start + n) {
                //多余元素赋值(多出来的空间初始化)
                *_finish = val;
                ++_finish;
            }
        }
    }
​
​
    
    T& operator[](size_t pos) {
        assert(pos < size());
        return _start[pos];
    }
    
    const T& operator[](size_t pos)const
    {
        assert(pos < size());
        return _start[pos];
    }
​
​
    
    void push_back(const T& x) {
        insert(end(), x);
    }
    
    void pop_back() {
        erase(end() - 1);
    }
    
    void swap(vector<T>& v){
        std::swap(_start,v._start);
        std::swap(_finish,v._finish);
        std::swap(_endOfStoratge,v._endOfStorage);
    }
    
    iterator insert(iterator pos, const T& x) {
        assert(pos >= _start);
        assert(pos <= _finish);
        if (_finish == _endOfstorage) {
            size_t len = pos - _start;
            reserve(capacity() == 0 ? 4 : capacity() * 2);
            pos = _start + len;
        }//容量不足扩容
        //需要用len记录位置,是因为reserve后会改变原型_start的位置
        iterator end = _finish - 1;
        while (end >= pos) {
            *(end + 1) = *end;
            --end;
        }//向后挪动覆盖,腾出空间插入
        *pos = x;
        ++_finish;
    
    }
    
    iterator erase(iterator pos) {
        assert(pos >= _start);
        assert(pos < _finish);//此处没有=号了,因为insert是可以尾插,而erase必须有元素可删除
        iterator it = pos + 1;
        while (it < end()) {
            *(it -1 ) = *it;
            ++it;
        }//向前挪动覆盖
        --_finish;
        return pos;
    }
​
  private:
​
    iterator _start; // 指向数据块的开始
    
    iterator _finish; // 指向有效数据的尾
    
    iterator _endOfStorage; // 指向存储容量的尾
​
  };
​
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值