【C++修炼之路】vector 模拟实现-LMLPHP

一、读源码

以下代码取自 stl30 的 stl_vector.h ,以下分析均在不更改并发布源码的前提条件下进行。

class vector {
public:
 // ...
protected:
  typedef simple_alloc<value_type, Alloc> data_allocator;
  iterator start;
  iterator finish;
  iterator end_of_storage;
  void insert_aux(iterator position, const T& x);
  void deallocate() {
    if (start) data_allocator::deallocate(start, end_of_storage - start);
  }

vector 的成员替换为迭代器:start, finish, end_of_storage

构造:

vector() : start(0), finish(0), end_of_storage(0) {}
vector(size_type n, const T& value) { fill_initialize(n, value); }
vector(int n, const T& value) { fill_initialize(n, value); }
vector(long n, const T& value) { fill_initialize(n, value); }
explicit vector(size_type n) { fill_initialize(n, T()); }

了解成员函数功能

void reserve(size_type n) {
    if (capacity() < n) {
        const size_type old_size = size();
        iterator tmp = allocate_and_copy(n, start, finish);
        destroy(start, finish);
        deallocate();
        start = tmp;
        finish = tmp + old_size;
        end_of_storage = start + n;
    }
}

void push_back(const T& x) {
    if (finish != end_of_storage) {
        construct(finish, x);
        ++finish;
    }
    else
        insert_aux(end(), x);
}


reserve

start = tmp;
finish = tmp + old_size;
end_of_storage = start + n;

这部分其实就是扩容的步骤,start 为起始位置,finish 加上了原先的 old_sizeend_of_storage 加上扩容后的容量 n

push_back

很明显 finish != end_of_storage 意思为空间如果够,则插入元素,并将 ++finsh ,将 size 位置偏移。

综上,得到成员的关系图:

【C++修炼之路】vector 模拟实现-LMLPHP

vector 是根据 template <class T, class Alloc = alloc> 类模板实现的,其中 Alloc 其实就是 空间配置器:Alloctor,空间配置器只负责开辟空间,但不关注空间的初始化,所以需要 定位 new 表达式 来对空间进行初始化:

template <class T>
    inline void destroy(T* pointer) {
    pointer->~T();
}

template <class T1, class T2>
    inline void construct(T1* p, const T2& value) {
    new (p) T1(value);
}

stl 选用空间配置器,会让效率更高,但是由于不使用 new, delete 操作符,对空间上的值进行赋值可能会释放随机值导致崩溃,所以空间上值的处理和释放就需要用定位 new 了。

二、成员变量

看了源码,仿着源码开始造轮子。

template<class T>
class vector
{
    private:
    iterator _start; 
    iterator _finish;
    iterator _endofstorage;
    // iterator _start = nullptr; // 可以给缺省值,可以减少初始化列表初始化
    // iterator _finish = nullptr;
    // iterator _endofstorage = nullptr;
};

三、默认成员函数

1、构造

无参构造

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

迭代器区间构造

template<class InputIterator>
    vector(InputIterator start, InputIterator last)
    :_start(nullptr)
        ,_finish(nullptr)
        ,_endofstorage(nullptr)
    {
        while (start != last)
        {
            push_back(*start);
            ++start;
        }
    }

初始化列表初始化成员变量,不初始化编译器可能帮助初始化,但是最好不要依赖编译器。

类模板中,可以嵌套使用模板,通过模板可以使用其他类型的迭代器初始化。

n个值构造

// 复用 resize
vector(size_t n, const T& val = T()) // 不初始化会有随机值
	:_start(nullptr)
	, _finish(nullptr)
	, _endofstorage(nullptr)
{
	resize(n, val);
}

// n 个 val 构造
vector(size_t n, const T& val = T())
    :_start(nullptr)
        , _finish(nullptr)
        , _endofstorage(nullptr)
    {
        reserve(n); // n 个 val 初始化
        for (size_t i = 0; i < n; i++)
        {
            push_back(val);
        }
    }

缺省参数 val 若不提供值,则 T() 为各个类型的默认构造:

为了兼容自定义类型初始化需要调用构造函数,模板对内置类型进行了升级,对内置类型增加了默认构造函数。

内置类型的默认构造函数不显示显现,当不给值时:int() ,编译器会隐式处理默认值,如 int 为 0,bool 为 false 等。

通过这种方式,对于任意类型,都能保证对对象的初始化。

根据源码中的办法,可以针对 int 类型写函数重载:

vector(int n, const T& val = T())
    :_start(nullptr)
        , _finish(nullptr)
        , _endofstorage(nullptr)
    {
        reserve(n); // n 个 val 初始化
        for (size_t i = 0; i < n; i++)
        {
            push_back(val);
        }
    }

2、析构

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

3、拷贝构造

自己负责空间的开辟和释放

vector(const vector<T>& v)
{
    _start = new T[v.size()]; // size 和 capacity 都可以
    // memcpy(_start, v._start, sizeof(T) * v.size()); // 内置类型可以,自定义类型不可以
    for (size_t i = 0; i < v.size(); i++)
    {
        _start[i] = v._start[i]; // 借助赋值重载进行深拷贝
    }
    _finish = _start + v.size();
    _endofstorage = _start + v.size();
}

memcpy 在vector中存在自定义类型的拷贝时,会造成浅拷贝,例如 vector<vector<int>

【C++修炼之路】vector 模拟实现-LMLPHP

vector<vector<int>> copy 的 _start 指向的 vector<int> 类型的数组上的成员的 _start 和原先的对象指向的空间相同,当析构时,对空间析构后,空间上为随机值,再次析构就会报错。

所以,针对vector上自定义类型数据 的拷贝需要利用循环,调用赋值重载来进行深拷贝 ,这样对于自定义类型,就会调用类型本身的赋值重载,以达到深拷贝的目的。

push_back 进行拷贝构造

vector(const vector<T>& v)
    :_start(nullptr)
        ,_finish(nullptr)
        ,_endofstorage(nullptr)
    {
        reserve(v.size()); // 先初始化,再开空间
        for (const auto& e : v) // const auto& & 为了提高效率,加上 const 是因为参数给的是 const vector<T>& v,无法被修改
        {
            push_back(e);
        }
    }

此处使用 & 可以提高效率,也无需担心是否浅拷贝,因为 push_back 接口中,具有帮助拷贝类型完成深拷贝的接口。

借助构造完成拷贝构造

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

vector(const vector<T>& v)
    :_start(nullptr)
        , _finish(nullptr)
        , _endofstorage(nullptr)
    {
        vector<T> tmp(v.begin(), v.end()); // 迭代器区间构造
        swap(tmp);
    }

4、赋值重载

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

传参时调用拷贝构造,随后将 *this 和 tmp 交换

四、访问

1、[ ] 重载

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

    return _start[pos];
}

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

    return _start[pos];
}

2、迭代器

vector 的迭代器是原生指针:

typedef T* iterator;
typedef const T* const_iterator;

iterator begin()
{
    return _start;
}

iterator end()
{
    return _finish;
}

const_iterator begin() const
{
    return _start;
}

const_iterator end() const
{
    return _finish;
}

五、容量

1、capacity

size_t capacity() const
{
    return _endofstorage - _start;
}

2、size

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

3、reserve

void reserve(size_t n)
{
    if (n > capacity())
    {
        size_t sz = size(); // 先拷贝,否则计算 _finish 时,最后结果仍为上一次结果
        T* tmp = new T[n];
        if (_start)
        {
            // memcpy(tmp, _start, sz * sizeof(T)); // 当 vector<vector<int>> 时,深拷贝
            for (size_t i = 0; i < sz; i++)
            {
                tmp[i] = _start[i]; // 借助赋值重载进行深拷贝
            }
            delete[] _start;
        }

        _start = tmp;
        _finish = _start + sz; // 由于 _start 改变,所以 _finish 也需要改变
        _endofstorage = _start + n;
    }
}

reserve 有两个问题:

(1)finish 指向空间错误

当 reserve 后,原先空间被释放,若 _finish = _start + size()size() = _finish - _start ,将其带入计算,此刻 _finish 并没有改变指向。

尤其是第一次扩容,如果按照上面的写法,_finish 在扩容后仍然是 nullptr

【C++修炼之路】vector 模拟实现-LMLPHP

所以,需要提前拷贝 size() ,并在扩容后计算 _finish = _start + sz

(2)memcpy 拷贝自定义类型浅拷贝

道理和拷贝构造的原理相同,在扩容后,需要拷贝数据到新的空间,当对于 vector<vector<int> 类型拷贝时,需要使用深拷贝。所以也需要使用循环,利用赋值重载进行深拷贝。

4、resize

三种情况:

  1. n < size,删除数据
  2. n >= size && n <= capacity,填充数据
  3. n > capacity,扩容,填充数据
void resize(size_t n, const T& val = T())
{
    if (n > capacity())
    {
        reserve(n);
    }

    // 如果 n 大于原本的 size()
    if (n > size())
    {
        // _start +n 为最后 _finish 应在位置
        while (_finish < _start + n)
        {
            *_finish = val;
            ++_finish;
        }
    }
    else
    {
        _finish = _start + n; // 否则删除数据
    }
}

六、增删查改

1、insert

iterator insert(iterator pos, const T& x)
{
    assert(pos >= _start && pos <= _finish);

    if (_finish == _endofstorage)
    {
        size_t len = pos - _start; // 记录原先 pos 的长短

        size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newcapacity);

        // 解决pos迭代器失效问题
        pos = _start + len; // 让开始位置加上 len
    }

    iterator end = _finish - 1; // 不用移动 \0
    while (end >= pos)
    {
        *(end + 1) = *end;
        --end;
    }

    *pos = x; // 调用 string 的赋值重载,完成深拷贝
    ++_finish;

    return pos;
}

当容量满后,需要扩容,当扩容后就会引起 内部迭代器失效

由于扩容,pos 指向的位置是释放的空间,所以,当往后修改 pos 位置时,就会访问到已经被析构的空间,造成程序错误。

【C++修炼之路】vector 模拟实现-LMLPHP

所以需要记录 pos 的位置:size_t len = pos - start,在扩容后,将 pos = _start + len 。此刻 pos 位置才是正确的。

同理,外部迭代器也会失效,当进行插入元素后,原先的迭代器位置可能失效了(扩容指向新空间),这时对这个迭代器操作可能会出错。

【C++修炼之路】vector 模拟实现-LMLPHP

扩容后,it 仍然指向被释放的空间,所以在 insert 后需要用返回值接收:

【C++修炼之路】vector 模拟实现-LMLPHP

所以,stl 规定,insert 后要返回 pos 位置的迭代器,并且在外面接收,尽量避免外部迭代器失效。

2、erase

iterator erase(iterator pos)
{
    assert(pos >= _start && pos < _finish); // pos != _finish
    iterator begin = pos + 1;
    while (begin < _finish)
    {
        *(begin - 1) = *begin;
        ++begin;
    }
    --_finish;

    return pos;
}

当前 erase 的实现不会引起内部迭代器失效,但是可能会引起外部迭代器失效,以防万一,所以返回删除的下一个位置,即 pos

针对 vs 和 linux 下迭代器失效做一下总结

3、push_back

void push_back(const T& x)
{
    //if (_finish == _endofstorage)
    //{
    //	size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    //	reserve(newcapacity);
    //}

     不是定位 new ,所以可以直接赋值
    //*_finish = x;
    //++_finish;

    insert(end(), x);
}

4、pop_back

void pop_back()
{
    assert(_finish > _start);
    --_finish;
}
07-16 06:30