Detailed analysis of vector in STL

A vector is simply a mutable container of variable-sized arrays. Some of its commonly used interfaces are in: related documents of vector . The use of its interface is similar to string, so the focus is not on the use of the interface, but on unfamiliar interfaces and what needs to be paid attention to when using the interface. Let's start directly without further ado.

Test the expansion mechanism of vector

void TestVectorExpand()
{
    size_t sz;
    vector<int> v;
    sz = v.capacity();
    cout << "making v grow:\n";
    for (int i = 0; i < 100; ++i)
    {
        v.push_back(i);
        if (sz != v.capacity())
        {
            sz = v.capacity();
            cout << "capacity changed: " << sz << '\n';
        }
    }
}

The compilation result under vs:

From this we can know that it is about 1.5 times the size growth.

But the result of compiling under gcc is completely different from vs:

g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128

Summary : 1. When we have predicted the number of data to be stored in the vector in advance, we can use reserve to reserve space in advance, thus avoiding the problem of low efficiency due to capacity expansion.

2. It is more appropriate to expand the space by about 2 times at one time. If there is less expansion each time, it will lead to frequent expansion. If you expand too much, the space will not be used up, which will lead to waste of space.

Several items to note when using the interface:

Determine whether the member function needs to add const:

When a const object calls the operator[] function, it can only be read, not written. If it is called by an ordinary object, it can perform read and write operations. The size() interface can only read but not write. From this we can summarize a few points:

  1. Read-only interfaces use const. (size)

  1. Write-only interfaces use non-const. (push_back)

  1. Readable and writable interfaces use const+non-const. (operator[])

A difference between vector and string:

Then I would like to ask why the program will report an error? ?

Answer: Although ten data spaces have been opened up for v in advance, its size is only zero. When using the opreator[] function, the function itself will check whether there is an out-of-bounds access, and the condition is the location of the access space. Must be less than size. (assertion error)

Solution: 1. Do not use operator[] to access, but use push_back to write data.

2. Use resize(), because it will not only open space but also initialize, in a word, it is very easy to use!

Note: If you use the at() function to write, it will not work, and an exception will be thrown .

从上面这个图我们可以知道vector中没有实现find的接口,在我们后面的学习中list、dequeue等等容器它们都没有find接口,因为这种统一的接口在每个容器都实现就显得非常麻烦,所以std:中有一个统一的find为我们提供。

注意

1.迭代器区间是左闭右开!!!另外要记得包含头文件喔!!

2.使用的是类模板算法--复用,所有容器都可以使用!

#include <algorithm> 

这样设计的原因:

1.string设计的早(历史原因)

2.string有时候需要找子串(功能原因)

vecotor接口总结:

增:push_back 不直接头插(需要挪动数据,效率低,建议少用)偶尔头插用insert(v.begin(),val)

删:ppop_back 不直接头删(需要挪动数据,效率低,建议少用)偶尔头删(erase(v.begin())

查:用算法库find

改:迭代器+operator[]

vector的底层实现:

我们实现的是sgi版本的,虽然会简化源代码,但是该实现的功能我们都可以实现。

首先是vector的大框架:

#pragma once

namespace myvector
{
    template<class T>
    class vector
    {
    public:
        typedef T* iterator;
        iterator begin()
        {
            return _start;
        }

        iterator end()
        {
            return _finish;
        }

        T& operator[](size_t pos)
        {
            assert(pos < size());
            return _start[pos];
            //return *(_start + pos);
        }
        vector()
            :_start(nullptr)
            ,_finish(nullptr)
            ,_endofstorge(nullptr)
        {}

        void reserve(size_t n)
        {
            //它不缩容
            if (n > capacity())
            {
                
                //先扩容
                T* tmp = new T[n];
                //将原先的数据拷贝到新空间,如果没有数据则不需要拷贝
                if (_start != nullptr)
                {
                    memcpy(tmp, _start, sizeof(T) * size());
                    //拷贝完释放旧空间
                    delete[] _start;
                }

                _start = tmp;
                _finish = _start +size() ;
                _endofstorge = _start + n;
            }
            
        }

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

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

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

    private:
        iterator _start;
        iterator _finish;
        iterator _endofstorge;
    };

    void test_vector1()
    {
        vector<int> vv;
        vv.push_back(1);
        vv.push_back(2);
        vv.push_back(3);
        vv.push_back(4);
        for (size_t i = 0; i < vv.size(); ++i)
        {
            cout << vv[i] << " ";
        }
        cout << endl;
        vector<int>::iterator it = vv.begin();
        while (it != vv.end())
        {
            cout << *it << " ";
            ++it;
        }
        cout << endl;

    }


}

在代码运行的过程中我们发现程序崩溃了原因出现在push_back()函数:

此时的_finish是nullptr不能进行解引用,那归很结底原因出现在了reserve中。

因为size()接口代表的是_finish-_start,由于_start在上一行代码被更新成了一个新的地址,所以在调用size()的时候正好把_start新地址给删除了,所以_finish找不到新的地址,所以它为nullptr。

解决办法:

  1. 优先更新_finish的地址

                _finish = tmp + size();
                _start = tmp;
                _endofstorge = _start + n;
  1. 保存旧的size的大小

void reserve(size_t n)
        {
            //它不缩容
            if (n > capacity())
            {
                size_t oldsize = size();
                //先扩容
                T* tmp = new T[n];
                //将原先的数据拷贝到新空间,如果没有数据则不需要拷贝
                if (_start != nullptr)
                {
                    memcpy(tmp, _start, sizeof(T) * oldsize);
                    //拷贝完释放旧空间
                    delete[] _start;
                }
                //_finish = tmp + size();
                _start = tmp;
                _finish = tmp + oldsize;
                _endofstorge = _start + n;
            }
            
        }

实现resize函数的接口我们要分三种情况:

  1. 当n>capacity()时,我们需要扩容。

  1. 当n>=size() &&n<=capacity()时,我们需要填充数据。

  1. 当n<size()时,我们需要删除数据。

void resize(size_t n,T val =T())
        {
            if (n > capacity())
            {
                reserve(n);
            }
            else if (n < size())
            {
                _finish = _start + n;
            }
            else
            {
                while (size() < n)
                {
                    *_finish = val;
                    ++_finish;
                }
            }
        }

insert导致的迭代器失效问题:

    void insert(iterator pos, const T& val)
        {
            if (_finish == _endofstorge)
            {
                size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
                reserve(newcapacity);
            }

            //挪动数据
            iterator end = _finish - 1;
            while (end >= pos)
            {
                *(end + 1) = *end;
                --end;
            }
            *pos = val;
            ++_finish;
        }

void test_vector2()
    {
        vector<int> vv;
        vv.push_back(1);
        vv.push_back(2);
        vv.push_back(3);
        vv.push_back(4);
        
        vv.insert(vv.begin(), 0);
        
        for (size_t i = 0; i < vv.size(); ++i)
        {
            cout << vv[i] << " ";
        }
        cout << endl;
    }

当我们运行test_vector2的时候结果出现了随机值:

这是什么原因呢?(野指针问题)

解决方法的代码:

if (_finish == _endofstorge)
            {
                int len = pos - _start;
                size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
                reserve(newcapacity);
                pos = _start + len;
            }

现在让我们看一下下面的代码:

请问it迭代器失效了吗???

让我们看结果:

很显然失效了,失效的原因就是野指针的问题。由于插入数据导致异地扩容,迭代器失效。虽然我们之前在insert内部解决了迭代器失效的问题,为什么这边it还是会失效呢??

    void insert(iterator pos, const T& val)
        {
            assert(pos >= _start);
            assert(pos < _finish);
            if (_finish == _endofstorge)
            {
                int len = pos - _start;
                size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
                reserve(newcapacity);
                pos = _start + len;
            }

虽然我们在代码中修改了pos的值,但是我们传入的是形参,形参的改变不会影响实参。所以即使我们在内部解决了迭代器失效的问题,但是在外部问题根本没有被解决

总结当我们使用迭代器某个位置进行插入时,不管原来的空间有没有发生异地扩容出现野指针的问题,我们统一的认为在插入操作过后迭代器位置失效!!!所以说迭代器失效后能不使用则不使用!!!

erase导致的迭代器失效的问题:

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

在vs2013下面会报断言错误(其他编译器可能不会报错),在g++下面编译正常运行。

那么到底迭代器是失效还是不失效呢??接下来我们采取一个极端情况:删除最后一个数据

这时我们发现pos位置指向了_finish的位置,该位置处在失效的位置。

我们再举一个例子例子:

    void test_vector3()
    {
        std::vector<int> vv;
        vv.push_back(1);
        vv.push_back(2);
        vv.push_back(3);
        vv.push_back(4);

        //删除所有的偶数
        std::vector<int>::iterator it = vv.begin();
        while (it != vv.end())
        {
            if (*it % 2 == 0)
            {
                vv.erase(it);
            }
            ++it;
        }

        for (auto e : vv)
        {
            cout << e << " ";
        }
        cout << endl;
    }
    

程序报错,原因:运行++it的时候会有assert检查,程序报错。

在g++下面运行的结果是这样的:

三种不同的数据对应着三种不同的结果,运行可能不报错但是结果不对。

第一个发生段错误的原因:挪动数据的原因,导致it不能等于end使循环终止,造成越界访问。

第二个结果正确的原因是凑巧,it刚好都指向偶数,正好删除正确。

第三个结果错误的原因是挪动数据再加上++it正好跳过了一个2,所以结果不对。

解决方案:在g++上我们可以这样修改:

while (it != vv.end())
        {
            if (*it % 2 == 0)
            {
                vv.erase(it);
            }
            else
            {
                ++it;
            }
            
        }

这样确实解决了因挪动数据且++it的操作错过了删除数据或者是越界访问,但此代码虽然在g++上得到解决,那么在vs上能够得到完美的解决吗??看vs的结果:

为什么会出现这种情况??因为在vs库里的vector的迭代器不像我们模拟实现的那样是个原生指针,它的迭代器使用类封装起来的,检查要求也更加的严格。我们模拟实现的和g++底层的vector差不多,都是sgi版本的,而vs底层用的是pj版本的,所以g++不报错,vs会报错。

erase迭代器失效真正的解决办法:

我们从这张图就可以知道erase返回了一个迭代器!!!!

所以我们可以通过接收返回值解决问题:

std::vector<int>::iterator it = vv.begin();
        while (it != vv.end())
        {
            if (*it % 2 == 0)
            {
                it=vv.erase(it);
            }
            else
            {
                ++it;
            }
            
        }

以上的解决方案都可以在vs和g++都可以跑,所以我们认为erase(it)失效了,更新他以后再去访问。

总结:

迭代器失效的原因:

  1. 挪动数据再++it迭代器使原来迭代器的位置不正确了。

  1. 极端情况下删除最后一个位置的时候,迭代器不再是有效数据的迭代器位置了。

总之无论在什么平台什么代码下,erase之后迭代器失效!!!!!!!!!!

vector中深拷贝:

拷贝构造的传统写法:

//拷贝构造的传统写法
        vector(vector<T>& v)
            :_start(nullptr)
            , _finish(nullptr)
            , _endofstorge(nullptr)
        {
            reserve(v.capacity());
            for (auto& e : v)
            {
                push_back(e);
            }
        }

拷贝构造的现代写法:

首先我们需要抓一个壮丁:

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

然后我们再和壮丁交换空间:

    //拷贝构造的现代写法
        vector(vector<T>& v)
            :_start(nullptr)
            , _finish(nullptr)
            , _endofstorge(nullptr)
        {
            vector<T> tmp(v.begin(), v.end());
            swap(tmp);
        }

赋值重载:

当拷贝构造写好的时候我们就可以直接写出赋值重载,当我们传入形参的时候,形参会调用一次拷贝构造创建出新的空间,然后我们就可以直接交换空间了。(现代写法)

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

模板匹配问题:

vector(size_t 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)
            :_start(nullptr)
            , _finish(nullptr)
            , _endofstorge(nullptr)
        {
            while (first != last)
            {
                push_back(*first);
                ++first;
            }
        }

当我们运行test_vector5时报了个间接寻址的错误,主要原因是因为当我们使用vector<int>vv(5,1)初始化的时候想调用的是第一个构造函数,但是默认匹配给我们的是第二个构造函数。

原因:5和1是int类型,第一个默认构造的n是size_t的类型,如果要匹配我们需要隐式类型的转换。

第二个的构造函数的参数都是InputInterator类型,不需要转换直接默认InputInterato就是int。编译器就默认调用的是第二个构造函数,当调用第二个构造函数的时候会进行整型的解引用,所以会导致非法的间接寻址。

解决办法:

  1. 我们可以将第一个构造函数n的类型改成int,这样就保证了参数类型传的一样,编译器直接默认调用,但是库里面参数类型就是size_t,所以我们不建议这样修改。

  1. 我们传参数的时候可以进行强制类型转换,这样对于用户不方便。

  1. 使用库里面的方法:函数重载


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

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

深拷贝中存在的问题:

void test_vector6()
    {
        vector<string>v;
        v.push_back("1111111111111111111");
        v.push_back("1111111111111111111");
        v.push_back("1111111111111111111");
        v.push_back("1111111111111111111");
        v.push_back("1111111111111111111");
        for (auto e : v)
        {
            cout << e << " ";
        }
        cout << endl;
    }

当我们运行以上代码的时候,我们模拟实现的vector程序结果错误,这是为什么呢??

这是因为当我们插入第五个数据的时候要进行扩容,如果vector涉及深拷贝的话,我们之前实现的代码就有问题了,现在我们通过画图来解释说明一下:

当我们用reserve开辟好一段新空间后,我们将旧数据拷贝到新空间使用的是memcpy函数,此函数是一个浅拷贝,使我们新空间的各个_start指向了旧空间中_start指向的一段空间。当我们使用memcpy结束后使用了delete函数,调用delete不仅会释放原本的旧空间,还会释放旧空间中指向的空间(调用各个部分的析构函数),所以我们新空间中各个部分的_start指向的空间也就被释放了,这样也就造成野指针的问题,打印出来的也就是混乱的数据了。

由此我们也会知道为什么c++更支持用new和delete而不用malloc,calloc和free,原因就是new出来的对象会调用它的构造函数,delete会调用对象的析构函数。虽然此情况delete是程序崩溃,但我们new出来的新空间里面的数据都是被初始化好的不是吗??

那么该如何解决这种问题呢?

其实也很简单,就是将旧空间的数据拷贝到新空间使之成为一个深拷贝,让新空间的各个_start分别指向独立的空间,我们以用用我们之前的赋值重载来解决这个问题:

void reserve(size_t n)
        {
            //它不缩容
            if (n > capacity())
            {
                size_t oldsize = size();
                //先扩容
                T* tmp = new T[n];
                //将原先的数据拷贝到新空间,如果没有数据则不需要拷贝
                if (_start )
                {
                    //memcpy(tmp, _start, sizeof(T) * oldsize);
                    //拷贝完释放旧空间
                    for (int i = 0; i < oldsize; ++i)
                    {
                        tmp[i] = _start[i];
                    }
                    delete[] _start;
                }
                //_finish = tmp + size();
                _start = tmp;
                _finish = tmp + oldsize;
                _endofstorge = _start + n;
            }
            
        }

讲到这vector就已经全部结束了,创作不易请多多支持!

Guess you like

Origin blog.csdn.net/m0_69005269/article/details/128601554