08.vector

目录

1. vector的介绍和使用

1.1 vector的介绍

1.2 vector的使用

1.2.1 初始化

1.2.2 vector iterator

1.2.3 vector 空间增长

1.2.4 vector增删查改

1.2.5 vector迭代器失效问题

2. vector模拟实现

2.1 构造函数、size、capacity

2.2 reserve

2.3 push_back

2.4 begin、end

2.5 析构函数

2.6 [ ]重载

2.7 insert

2.8 erase

2.9 resize

2.10 拷贝构造

3.练习

3.1 std::vector::iterator 没有重载下面哪个运算符( )


1. vector的介绍和使用

        vector和string不同,string后有 \0,可以更好的兼容C,string有很多它专用的接口,vector的比大小没什么用,总的来说,vector和string各有其使用价值。

1.1 vector的介绍

  std::vector 是 C++ 标准模板库 (STL) 提供的一个动态数组容器,位于头文件 <vector> 中。它是一个能够动态调整大小的数组,提供了非常方便的动态存储机制和丰富的接口。相比传统的 C 风格数组,std::vector 更加安全、灵活且功能强大。

特性:

  1. 动态大小

    • vector 的大小是动态的,可以随着元素的插入或删除自动扩展或收缩。
    • 内部实现利用动态内存分配,会根据需要重新分配内存,但代价是拷贝已有数据。
  2. 随机访问

    • 提供与数组类似的随机访问功能,可以通过下标访问元素,时间复杂度为 O(1)。
  3. 连续存储

    • vector 内部存储的元素是连续的,意味着可以直接传递给需要数组指针的 C 函数。
  4. 丰富的成员函数

    • 支持插入、删除、排序、查找等操作,使用方便。
  5. 类型安全

    • vector 是模板类,支持强类型约束,不同类型的 vector 之间不会混淆。

使用场景:

  • 动态大小数组的需求。
  • 需要频繁随机访问。
  • 数据存储需要保持连续性(如传递给 C 风格函数)。

1.2 vector的使用

1.2.1 初始化

vector 支持多种初始化方法:

vector<int> v1;                   // 空vector
vector<int> v2(5);                // 大小为5的vector,默认初始化为0
vector<string> v3(5, "***");            // 大小为5,所有元素初始化为10
vector<int> v4 = {1, 2, 3, 4, 5}; // 使用列表初始化
vector<int> v5(v4);               // 拷贝构造
1.2.2 vector iterator

std::vector 提供了以下几种迭代器:

1.2.3 vector 空间增长
容量空间接口说明
size获取数据个数
capacity

获取容量大小

empty判断是否为空
resize改变vector的size
reserve改变vector的capacity

        当添加元素导致 size > capacity 时,vector 会分配更大的内存空间,将旧元素复制到新空间,并释放旧空间。每次分配的容量通常是当前容量的 2 倍。但是vs下是按照1.5倍增长的,g++是按2倍增长的,所以不要固化的认为都是2倍。

        在某些情况下,频繁的内存重新分配会影响性能。如果你能预估到容器的最终大小,可以通过以下两种方法提前分配空间:

  1. reserve 会预先分配内存,但不会改变 size() 的值。

优点:

  • 避免频繁的内存分配,提高性能。
  • capacity 会被设置为指定值,但 size 不会改变

    2. resize 会改变 size(),并填充默认值。

两种方法的区别:

  • resize 会真正改变 size,并初始化新元素。
  • reserve 只改变容量,不改变大小。
1.2.4 vector增删查改
push_back胃插
pop_back尾删
find查找
insert在pos之前插入
erase删除某位置的数据
swap交换两个空间
operator[]像数组一样访问
clear清空整个vector

效率:

  • 尾部插入/删除最快,时间复杂度为 O(1)O(1)O(1)。
  • 中间插入/删除较慢,时间复杂度为 O(n)O(n)O(n)。
  • 查找元素时间复杂度为 O(n)O(n)O(n)

        使用下标运算符 []at() 方法访问元素时,在vs系列编译器中,debug模式下,at() 和 operator[] 都是根据下标获取任意位置元素的,在debug模式下两者都会去做边界检查。当发生越界行为时,at 是抛异常,operator[] 内部的assert会触发。std::vector::operator[] 本身不会进行边界检查,越界访问会导致未定义行为。

如果我们像删除一个数组中的3,但是不知道在哪个位置,怎么办?

    vector<int> v;
    auto pos = find(v.begin(), v.end(), 3);

    if(pos != v.end())
    {
        v.erase(pos);
    }
    for(auto e : v)
    {
        cout << e << " ";
    }
    cout << endl;

但如果有好几个3呢,要求删除所有的3,这就涉及迭代器失效的问题了:

1.2.5 vector迭代器失效问题

        当我们对 std::vector 进行某些修改操作时,比如插入、删除或者重新分配内存,可能会导致迭代器失效,进而引发未定义行为。以下是有关 std::vector 迭代器失效的详细说明:

迭代器失效是指一个原本指向容器元素的迭代器,在容器被修改后,可能变得无效。使用失效的迭代器会导致程序运行错误甚至崩溃。

对于 std::vector 来说,以下操作可能导致迭代器失效:

  1. 内存重新分配:

    • std::vector 扩容时,底层存储数据的内存地址会重新分配。
    • 所有现有的迭代器、指针和引用都会失效。
int main()
{
    vector<int> v{1,2,3,4,5,6};

    auto it = v.begin();

    v.resize(100,8);
    v.reserve(100);
    v.insert(v.begin(), 0);
    v.push_back(6);
    v.assign(100,8);

    while(it != v.end())
    {
        cout << *it << endl;
        ++it;
    }
    cout << endl;
    return 0;
}

        以上操作都有可能会导致vector扩容,也就是说vector底层旧空间被释放掉,而在打印时,it还使用的是释放前的旧空间,在对it进行操作时,实际操作的是一块已经被释放的空间,从而引起崩溃。

解决方式:在以上操作后,如果想继续通过迭代器操作vector中的元素,需要给it重新赋值。

    2.元素插入或删除:

  1. 插入或删除元素可能导致部分迭代器失效。
  2. 特别是插入或删除位置后的迭代器可能受到影响。
int main()
{
    vector<int> v{1,2,2,3,4};
    auto it = v.begin();
    while(it != v.end())
    {
        if(*it % 2 == 0)
        {
            v.erase(it);
        }
        ++it;
    }
    return 0;
}

        比如我们想删除其中的偶数,当删掉第一个2以后,后面的2、3、4向前移动,此时it已经指向第二个2了,但是还是走了++it,导致忽略了第二个2。

        erase删除某位置的元素后,pos位置之后的元素会向前搬移,没有导致底层空间的改变,理论上迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效。

        要注意在linux下,g++编译器对上面的代码不会失效,g++下不检查,对迭代器失效的检测并不是非常严格,处理也没有vs极端。建议使用如下可移植的代码:

int main()
{
    vector<int> v{1,2,2,3,4};
    auto it = v.begin();
    while(it != v.end())
    {
        if(*it % 2 == 0)
        {
            it = v.erase(it);
        }
        else
        {
            ++it;
        }
    }
    return 0;
}

其中接收了erase的返回值,也就是下一个位置。

在g++下:

  1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了,虽然可以运行,但是结果肯定是错误的。
  2. erase删除的不是最后一个元素,迭代器不会失效,空间还是原来的空间
  3. erase删除最后一个元素,删除之后it超过了end,此时迭代器无效,++it导致程序崩溃。

    3.string下的失效

        与vector类似,string在插入+扩容操作+erase后,迭代器也会失效,不能再访问。

void teststring()
{
    string s("hello");
    auto it = s.begin();

    //s.resize(20,'*');
    while (it != s.end())
    {
        cout << *it;
        ++it;
    }
    cout << endl;

    it = s.begin();
    while (it != s.end())
    {
        it = s.erase(it);
        //s.erase(it);
        ++it;
    }
}

        如果将  //s.resize(20,'*'); 取消注释,代码会崩溃,因为resize到20的空间会扩容,扩容之后it指向之前的旧空间被释放,迭代器失效,后序打印时,再访问it指向的空间程序就会崩溃。

        如果只删除数据,而不接收它返回的新位置,程序就会崩溃。


2. vector模拟实现

因为vector是个模板类,可以存int,char,string等,所以要用类模板

2.1 构造函数、size、capacity

namespace zzy
{
    template<class T>
    class vector
    {
    public:
        typedef T* iterator;

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

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

        size_t size() const
        {
            return _finish - _start;
        }
    private:
        iterator _start;
        iterator _finish;
        iterator _endofstorage;
    };
}

2.2 reserve

        void reserve(size_t n)
        {
            if(n > capacity())
            {
                size_t sz = size();
                T* tmp = new T[n];
                if(_start)
                {
                    memcpy(tmp, _start, sizeof(T)*sz);
                    delete[] _start;
                }
                _start = tmp;
                _finish = _start + sz;
                _endofstorage  = _start + n;
            }
        }

为什么不可以直接写size(),而是要把size的值保存成sz?

        因为在最后_finish = _start + sz  如果我们的sz是size(),会调用我们上面实现的size函数,其中的_finish指向的还是原来的空间,而_start已经被 _start = tmp 改变了,指向的是新空间,会出错。

如果我们使用memcpy,运行下面的代码会出现什么问题呢?

int main()
{
    zzy::vector<std::string> v;
    v.push_back("11111");
    v.push_back("22222");
    v.push_back("33333");
    return 0;
}

 由于memcpy是浅拷贝,会将空间中内容原封不动的拷贝到另一段空间中。

比如:在插入11111后,_start会指向空间“1”,插入22222期间需要扩容,假如开辟的新空间为“2、3”,如果采用memcpy,空间“2”将会被“1”覆盖,空间地址就被替换了,现有的空间变成了“1、3”中间的2号空间就丢失了。

而在delete后,旧空间被释放,_start就变成了野指针,因为它指向了指向了一块被释放的空间,再往里插入时,再扩容就拷贝不到了。

结论:如果对象中涉及到资源管理时,不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,不然会引起内存泄露甚至程序崩溃。

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

2.3 push_back

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

2.4 begin、end

    public:
        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;            
        }

2.5 析构函数

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

2.6 [ ]重载

        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.7 insert

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

        当扩容时,开辟了新空间,旧空间被释放,pos如果不更新就是野指针,start 和 finish 都指向新空间。所以在代码中使用了相对位置,保留了位置差,扩容后更新pos到新空间。

记住:insert以后迭代器可能会失效(扩容)

所以 insert 以后就不要使用这个形参迭代器了,因为它可能失效了。

v.insert(p, 300);

高危行为:
*p += 10;

2.8 erase

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

2.9 resize

        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;
                }
            }
        }

为什么val不直接给0?

因为T不一定是int型,可能是各种类型,所以T()的本质是匿名对象。

由于resize的实现,引申出两种额外的构造函数:

        explicit vector(size_t n, const T& val = T())
        {
            resize(n, val);
        }

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

此处可以发现:类模板里的成员函数仍可以是函数模板。

在使用时我发现

vector<int> v(10, 1);

想调用下面的构造函数,因为10默认是int,而我们的第一个实现是size_t ,类型不匹配。解决办法:

vector<int> v(10u, 1);

u代表unsigned int

2.10 拷贝构造

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

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

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

 

3.练习

3.1 std::vector::iterator 没有重载下面哪个运算符( )

        A.==

        B.++

        C.*

        D.>>

        vector底层是以当前类型的指针作为迭代器,对于指针而言,能够进行操作的方法都支持,如==,++,*,而>>运算符并没有重载。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值