• C++(第八篇):vector类容器(介绍、使用、模拟实现及迭代器失效问题)


    📒博客主页:要早起的杨同学的博客
    🎉欢迎关注🔎点赞👍收藏⭐️留言📝
    📌本文所属专栏: 【C++拒绝从入门到跑路】
    ✉️坚持和努力从早起开始!
    💬参考在线编程网站:🌐牛客网🌐力扣
    🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!

    一. STL - vector 的介绍

    文档介绍:vector - C++ Reference (cplusplus.com)

    1. vector 是表示可变大小数组的序列容器。
    2. 就像数组一样,vector 也采用的连续存储空间来存储元素。也就是意味着可以采用下标对 vector 的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
    3. 本质讲,vector 使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector 并不会每次都重新分配大小。
    4. vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是倍数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。因此,vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
    5. 与其它动态序列容器相比(deques, lists and forward_lists), vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起 lists 和 forward_lists 统一的迭代器和引用更好。

    二. vector 的使用(常用接口介绍)

    template < class T, class Alloc = allocator<T> > class vector; // generic template
    
    • 1

    2.1 常见构造

    构造函数:

    constructor接口说明
    vector ();(⭐)无参构造(构造一个没有元素的空容器,size = 0
    vector (size_type n, const value_type& val = value_type());构造一个包含 n 个元素的容器,元素值为 val
    vector (const vector& x); (⭐)拷贝构造
    template vector (InputIterator first, InputIterator last);(函数模板)使用迭代器进行初始化构造 [first,last)

    value_type:The first template parameter (T) 第一个模板参数,vector 中存储数据的类型

    👉Example1:

    #include 
    #include 
    
    int main ()
    {
    	std::vector<int> v1;                                // 构造一个没有元素的空容器
    	std::vector<int> v2 (4,100);                       // 4个值为100的整数
    	std::vector<int> v3 (v2.begin(), v2.end()); // 迭代器构造
    	std::vector<int> v4 (v3);                       // 拷贝构造
        
    	// 迭代器构造函数也可以使用数组来进行构造,传的区间是左闭右开
        // 因为指向数组空间的指针是天然的迭代器
    	int arr[] = {16,2,77,29};
        std::vector<int> v5 (arr, arr + 4 );
    	// std::vector v5 (arr, arr + sizeof(arr) / sizeof(int) );
        
        // v1 : []
        // v2: [100,100,100,100]
        // v3 : [100,100,100,100]
        // v4: [100,100,100,100]
        // v5 : [16,2,77,29]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    👉Example2:不实用

    void test2()
    {
    	// 用其它容器的迭代器初始化,只要数据的类型可以匹配上
    	string s("hello");
    	vector<char> v(s.begin(), s.end());
        //打印
    	for (auto& e : v)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.2 迭代器及遍历操作

    Iterators接口说明
    begin(iterator / const_iterator)返回指向第一个元素的迭代器
    end返回指向最后一个元素的下一个位置的迭代器
    rbegin(reverse_iterator / const_reverse_iterator)反向迭代器(可以反向遍历对象)
    rend反向迭代器
    范围forC++11 支持更简介的范围 for 的新遍历方式(底层其实是被替换成迭代器,所以支持迭代器就支持范围 for)

    image-20220514091129038

    正向迭代器其实就是一个原生指针 T*

    反向迭代器不是原生指针,因为原生指针是无法实现 rit++ 倒着走的,是一个被封装的类对象,重载了++运算符,实现了 rit++ 倒着走

    👉Example:

    void test1()
    {
    	vector<int> v;
    	v.push_back(1);
    	v.push_back(2);
    	v.push_back(3);
    	v.push_back(4);
    
    	// 遍历vector
    	// 1. []运算符
    	for (size_t i = 0; i < v.size(); i++)
    	{
    		cout << v[i] << " ";
    	}
    	cout << endl;
    
    	// 2. 迭代器
    	vector<int>::iterator it = v.begin();
    	while (it != v.end())
    	{
    		cout << *it << " ";
    		it++;
    	}
    	cout << endl;
    
    	// 3. 范围for
    	for (auto& e : v)
    	{
    		cout << e << " ";
    	}
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.3 容量操作

    Capacity接口说明
    size返回容器中有效元素个数
    resize调整容器的有效元素大小(size)
    capacity返回分配的存储容量大小(即有效元素的最大容量)
    reserve调整容器的容量大小(capacity)
    empty判断容器是否为空

    👉resize :

    resize 函数介绍:开空间 + 初始化

    注意value_type() 是缺省值,是调用 value_type 类型的默认构造函数构造的一个匿名对象

    void resize (size_type n, value_type val = value_type());
    
    • 1

    比如:

    void test3()
    {
        vector<char> v;
        v.resize(10); // v中的10个有效元素全是缺省值'\0'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    👉reserve :

    reserve 函数介绍:只是开空间

    如果确定知道需要用多少空间,用 reserve 可以缓解 vector 增容的代价问题。

    void reserve (size_type n);
    
    • 1

    注意:下面这种是错误❌用法

    void test3()
    {
    	vector<int> v;
    	v.reserve(10); // 有效元素个数 size = 0,只是开了空间 capacity = 10
    	
        /* error
        for (size_t i = 0; i < 10; i++)
    	{
    		v[i] = i; // 违规访问
    	}
    	*/
        
        // 正确做法
        for (size_t i = 0; i < 10; i++)
        {
            v.push_back(i);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    图示说明:

    image-20220511211314643

    👉补充

    如果不断往 vector 中尾插数据,容器会随着数据的增多而发生扩容,通过验证发现:

    容量 capacity() 在 VS2019 下是按 1.5 倍增长的,在 Linux g++ 下是按 2 倍增长的


    2.4 访问操作

    Element access接口说明
    operator[]可以像数组一样访问 vector(一般物理地址是连续的才支持)
    back返回容器中最后一个元素的引用
    front返回容器中第一个元素的引用

    [] 运算符重载函数返回容器中位置 n 处元素的引用

    reference operator[] (size_type n);
    const_reference operator[] (size_type n) const;
    // 函数内部会检查越界
    
    • 1
    • 2
    • 3

    2.5 修改操作

    Modifiers接口说明
    push_back(⭐)在末尾添加一个元素,有效元素个数加1
    pop_back(⭐)删除最后一个元素,有效元素个数减1
    insert在指定迭代器位置的元素之前插入新元素来扩展容器
    erase从容器中删除单个元素(迭代器位置)或一系列元素(迭代器区间 [first,last)
    swap交换两个容器的内容
    clear清空容器中所有有效元素,使容器大小 size 为 0

    👉insert和erase :

    insert 函数介绍:

    • 返回 指向第一个新插入元素的迭代器

    •   iterator insert (iterator position, const value_type& val); // 插入单个元素
        // 传值传参,形参改变不会影响实参
      
      • 1
      • 2

    erase 函数介绍:

    • 返回 指向被删除元素下一个位置的迭代器

    •   iterator erase (iterator position); // 删除单个元素
        // 传值传参,形参改变不会影响实参
      
      • 1
      • 2

    注意:这里会涉及到迭代器失效的问题!

    void test4()
    {
    	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    	vector<int> v(arr, arr + 10);
    	auto pos1 = v.insert(v.begin(), 50); // 头插
    	auto pos2 = v.erase(v.begin()); // 头删
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    👉swap :

    swap 函数介绍:用 x 的内容交换当前容器的内容,x 是同类型的另一个对象。两个容器大小可能不同。

    void swap (vector& x);
    
    • 1

    比如:

    vector<int> v1(2, 10); // v1 = [10, 10]
    vector<int> v2(5, 50); // v2 = [50, 50, 50 ,50 ,50]
    v1.swap(v2);
    // v1 = [50, 50, 50 ,50 ,50]
    // v2 = [10, 10]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.6 vector 迭代器失效问题(⭐重要)

    vector迭代器主要作用是让算法能不用关心底层数据结构。底层实际是一个指针或者是对指针进行封装,vector迭代器就是一个指针。

    ​ 迭代器失效就是迭代器底层对应指针所指向的空间被销毁了,导致使用了一块已经被释放了的空间。造成的后果就是程序奔溃,不使用并非一定会导致程序奔溃。

    一般都是扩容发生深拷贝时,开辟新空间,旧空间被释放了,但是迭代器没有进行更新,还指向原来的旧空间。

    会导致底层空间改变的操作,都可能导致迭代器失效。简单来说,导致扩容发生深拷贝。如resize,reserve,insert,push_back函数等等。

    下面来测试两种导致迭代器失效的操作:

    1. 指定位置之前插入元素操作 – insert

      👉情况一

      void test6()
      {
      	// 迭代器失效问题 -- 类似野指针问题
      	vector<int> v;
      	v.push_back(1);
      	v.push_back(2);
      	v.push_back(3);
      
      	vector<int>::iterator pos = find(v.begin(), v.end(), 2);
      	if (pos != v.end())
      	{
      		v.insert(pos, 20); // 在pos之前插入20
              
              /*
              * 如果想要继续通过迭代器操作 vector 中的元素,需要给 pos 重新赋值
              * 即 pos = v.insert(pos, 20); // 函数返回指向新插入元素的迭代器
              */
      	}
      
      	// 在insert之后,pos迭代器位置就失效了,不能再去访问了 -- insert时增容导致的
      	cout << *pos << endl;
      	*pos = 200;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

      运行结果

      • VS2019下验证,程序崩溃
      • Linux下验证,虽然没有崩溃(和具体实现、增容机制以及检查机制有关),但也认为失效了

      画图分析

      image-20220513205910718


      👉情况二

      那么我们提前把空间开好,在同一块空间下,又是什么情况呢?

      void test7()
      {
      	// 迭代器失效问题 -- 类似野指针问题
      	vector<int> v;
      
      	v.reserve(5); // 提前开好空间,防止后续插入元素时发生扩容
      
      	v.push_back(1);
      	v.push_back(2);
      	v.push_back(3);
      
      	vector<int>::iterator pos = find(v.begin(), v.end(), 2);
      	if (pos != v.end())
      	{
      		v.insert(pos, 20); // 在pos之前插入20
              
              /*
              * 如果想要继续通过迭代器操作 vector 中的元素,需要给 pos 重新赋值
              * 即 pos = v.insert(pos, 20); // 函数返回指向新插入元素的迭代器
              */
      	}
      
      	// 在insert时空间足够,没有增容,但我们依旧认为pos迭代器位置失效了,不能再去访问了
          // 这里的失效是指pos位置的意义变了,pos不再指向原来的值
      	cout << *pos << endl;
      	*pos = 200;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

      运行结果

      • VS2019下验证,程序崩溃(VS会进行检查)
      • Linux下验证,虽然没有崩溃(和具体实现、检查机制有关),但也认为失效了

      画图分析

      image-20220513205945093

      👉总结

      指定位置之前插入元素操作(insert)导致迭代器失效的两种场景:

      1. insert 插入元素时增容,pos 还指向已被释放的空间(非法空间),所以 pos 位置迭代器失效
      2. insert 插入元素时没有增容,但 pos 位置意义变了,不再指向原来的值,所以 pos 位置迭代器失效

      vector 插入元素过程中可能会发生扩容,挪动数据,因此当前迭代器和后面所有元素的迭代器都失效
      失效后,我们都不要去访问 pos 位置,否则可能出现各种意外情况


    1. 指定位置元素的删除操作 – erase

    👉情况一

    void test8()
    {
    	// 迭代器失效问题 -- 类似野指针问题
    	vector<int> v;
    	v.push_back(1);
    	v.push_back(2);
    	v.push_back(3);
    
    	vector<int>::iterator pos = find(v.begin(), v.end(), 2);
    	if (pos != v.end())
    	{
    		v.erase(pos); // 删除pos位置的元素
            /*
            * 如果想要继续通过迭代器操作 vector 中的元素,需要给 pos 重新赋值
            * 即 pos = v.erase(pos); // 返回指向被删除元素下一个位置的迭代器
            */
    	}
    
    	// 在erase之后,pos迭代器位置就失效了,不能再去访问了
    	// 这里的失效是指pos位置的意义变了,pos不再指向原来的值
    	cout << *pos << endl;
    	*pos = 200;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果

    • VS2019下验证,程序崩溃(VS会进行检查)
    • Linux下验证,虽然没有崩溃(和具体实现、检查机制有关),但也认为失效了

    分析

    erase 删除 pos 位置元素后,pos 位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但 pos 位置的意义变了;如果 pos 刚好是最后一个元素,删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么 pos 也是失效了。

    因此删除 vector 中任意位置上元素时,都认为该位置迭代器失效了。

    image-20220513174104848

    👉总结

    vector 删除元素过程中会牵扯到移动数据,因此当前迭代器和后面所有元素的迭代器都失效

    👉样例:删除容器中的所有偶数

    void test9()
    {
    	vector<int> v;
    	v.push_back(1);
    	v.push_back(2);
    	v.push_back(3);
    	v.push_back(4);
    	v.push_back(5);
    
    	auto it = v.begin();
    	while (it != v.end())
    	{
    		if (*it % 2 == 0)
                // 如果想要继续通过迭代器操作 vector 中的元素,需要给 it 重新赋值
    			it = v.erase(it);
    		else 
                it++;
    	}
    
    	for (auto& e : v)
    		cout << e << " ";
    	cout << endl;
        // result : 1 3 5
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    1. 总结

      vector 插入或删除元素会导致当前迭代器和后面所有元素的迭代器失效。

    2. 迭代器失效解决办法:在使用前,对迭代器重新赋值即可。

      如果想要继续通过迭代器操作 vector 中的元素,需要给 pos 重新赋值。


    2.7 补充:其它接口

    库文件 中:

    函数名称接口说明
    find查找元素(注意这个是算法模块实现,不是 vector 的成员函数,这样其它容器也可以用)

    若找到,返回指向该元素的迭代器,若没有找到,返回 last (开区间)

    // 传迭代器区间 [first, last) 和要查找的元素
    template <class InputIterator, class T>
       InputIterator find (InputIterator first, InputIterator last, const T& val);
    {
    	while (first!=last) 
        {
    		if (*first==val) 
                return first;
    		++first;
    	}
    	return last;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    👉Example:

    void test3()
    {
    	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
    	vector<int> v(arr, arr + 10);
    
    	vector<int>::iterator pos = find(v.begin(), v.end(), 5); // find函数返回迭代器
    	if (pos != v.end()) // 判断是否查找到
    	{
    		pos = v.insert(pos, 50); // 在5前面插入50
    	}
    
    	for (auto& e : v)
    	{
    		cout << e << " ";
    	}
        // result : 1 2 3 4 50 5 6 7 8 9 10
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    三. 源码剖析:vector 的底层结构

    image-20220513212059445

    我们先来看看源码,SGI 版本 STL - vector 的实现:

    template <class T, class Alloc = alloc>
    class vector {
    public:
      typedef T value_type;
      typedef value_type* iterator; // 指向数组空间的指针T*是天然的d
      // ...
    protected:
      // ...
      iterator start;
      iterator finish;
      iterator end_of_storage; // 成员变量是三个T*类型的指针
      // ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们再来看一下源码中 push_back 尾插接口的实现:

    image-20220513205641152

    定位 new 表达式在实际中一般是配合内存池使用。因为 vector 的空间是从内存池来的,内存池分配出的空间是没有初始化的,该空间存放的是哪个自定义类型对象,就使用定位 new 表达式显示调用其构造函数进行初始化。


    四. vector 的模拟实现

    4.1 vector 的结构

    #pragma once
    
    #include
    #include
    #include
    #include // assert
    #include // memcpy
    using namespace std;
    
    namespace yzy
    {
    	/*
    	* T: 模板参数,指vector中存放数据的类型
    	*/
    	template<class T>
    	class vector
    	{
    	public:
    		/*******************************************************/
    		// 迭代器
    		// iterator是内嵌类型,在vector类域里面定义的类型
    		// 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; }
    
    	private:
    		iterator _start;          // 指向数组的开始
    		iterator _finish;         // 指向最后一个有效元素的下一个位置
    		iterator _end_of_storage; // 指向空间结束的下一个位置
    
    	public:
    		/*******************************************************/
    		// 默认成员函数
    		vector(); // 无参构造函数
    		template<class InputIterator>
    		vector(InputIterator first, InputIterator last); // 使用迭代器初始化构造		
            vector(const vector<T>& v); // 拷贝构造(深拷贝)		
    		void swap(vector<T>& v) // 交换两个容器的内容
    		vector<T>& operator=(vector<T> v) // 赋值运算符重载(深拷贝)		
    		~vector();
    
    		/*******************************************************/
    		// 容量操作
    		size_t size() const { return _finish - _start; } // 有效元素个数
    		size_t capacity() const { return _end_of_storage - _start; } // 容量大小
    		bool empty() const { return size(); } // 判空
    		void reserve(size_t n); // 调整容器的容量大小(capacity)
    		void resize(size_t n, const T& val = T()); // 调整容器的有效元素大小(size)
            
    		/*******************************************************/
    		// 访问操作,[]运算符重载
    		T& operator[](const size_t pos);
    		T& operator[](const size_t pos) const;
            
    		/*******************************************************/
    		// 修改操作
    		iterator insert(iterator pos, const T& val = T()); // 在pos迭代器位置前插入元素
    		iterator erase(iterator pos); // 删除pos迭代器位置的元素 
            void push_back(const T& x); // 尾插
    		void pop_back(); // 尾删
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    4.2 vector 的迭代器

    vector 的正向迭代器就是一个原生指针 T*

    namespace yzy
    {
    	/*
    	* T: 模板参数,指vector中存放数据的类型
    	*/
    	template<class T>
    	class vector
    	{
    	public:
    		/*******************************************************/
    		// 迭代器
    		// iterator是内嵌类型,在vector类域里面定义的类型
    
    		// 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; }
    
    	private:
    		iterator _start;          // 指向数组的开始
    		iterator _finish;         // 指向最后一个有效元素的下一个位置
    		iterator _end_of_storage; // 指向空间结束的下一个位置
            
    	public:
            // ...
            // ...
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    4.3 一些成员函数的实现

    ① 默认成员函数

    👉 构造函数:

    // 无参构造
    vector()
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {}
    
    // 使用迭代器进行初始化构造 [first,last)
    // 注意:若使用vector的 iterator 做形参,会导致初始化的迭代器区间[first,last)只能是vector的迭代器
    /* 
    * 类模板的成员函数模板,可以自己定义模板参数,比如:
    * 这样写的好处是可以传其它容器的迭代器(string、list ...)
    * 而不是仅限于用 vector 自己的迭代器 iterator,
    * 只要解引用后数据的类型能够和 vector 数据的类型匹配。
    */
    template<class InputIterator>
    vector(InputIterator first, InputIterator last) // [first,last)
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
        while (first != last)
        {
            push_back(*first);
            first++;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    测试:

    image-20220516095412854

    👉 拷贝构造函数:传统写法

    /* 拷贝构造(深拷贝)的2种传统写法 */
    
    // 拷贝构造(深拷贝),传统写法1
    // v2(v1)
    vector(const vector<T>& v) 
        :_start(new T[v.capacity()])
    	, _finish(_start + v.size())
    	, _end_of_storage(_start + v.capacity())
    {
    	memcpy(_start, v._start, sizeof(T) * v.size()); // 拷贝元素
    }
    
    
    // 拷贝构造(深拷贝),传统写法2 -- 推荐,好处在括号中
    // v2(v1)
    vector(const vector<T>& v) 
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
    	reserve(v.capacity());  // 调整新容器容量大小(这样的好处是:一次性把空间开好,效率高)
    	for (const auto& e : v) // 尾插元素到新容器中(复用push_back函数)
    		push_back(e);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    👉 赋值运算符重载函数:传统写法

    // 赋值运算符重载(深拷贝),传统写法
    // v2 = v1
    vector<T>& operator=(const vector<T>& v)
    {
        if (this != &v) // 防止自己给自己赋值
        {
            // 释放原空间
            delete[] _start; 
            _start = _finish = _end_of_storage = nullptr;
    
            // 插入元素
            reserve(v.capacity()); // 调整容量大小
            for (const auto& e : v)
                push_back(e);
    
            _finish = _start + v.size();
            _end_of_storage = _start + v.capacity();
        }
    
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    👉 交换两个容器的内容:方便实现拷贝构造和赋值重载的现代写法

    // 交换两个容器的内容
    // v1.swap(v2)
    void swap(vector<T>& v)
    {
        // 函数名冲突,指定去调用全局域里面的std::swap
        std::swap(_start, v._start);
        std::swap(_finish, v._finish);
        std::swap(_end_of_storage, v._end_of_storage);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    👉 拷贝构造函数:现代写法

    // 拷贝构造(深拷贝),现代写法
    // v2(v1)
    vector(const vector<T>& v) 
        :_start(nullptr) // 当前对象是一个正在构造的对象,成员变量还未初始化,是一个随机值,所以先置空
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
    	vector<T> tmp(v.begin(), v.end()); // 拿v的内容,调用构造函数构造一个临时对象tmp
    	this->swap(tmp);                   // 将临时对象tmp和当前对象的成员变量分别进行交换
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    👉 赋值运算符重载函数:

    // 赋值运算符重载(深拷贝),现代写法 -- 推荐
    // v2 = v1
    vector<T>& operator=(vector<T> v) // 通过传值传参,拷贝构造出临时对象
    {
        this->swap(v); // 交换临时对象和当前对象的内容
    
        return *this;  // 返回当前对象
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    👉 析构函数:

    ~vector()
    {
        delete[] _start;
        _start = _finish = _end_of_storage = nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ② 容量操作

    👉 size、capacity、empty 函数:

    size_t size() const // 有效元素个数
    {
        return _finish - _start;
    }
    
    size_t capacity() const // 容量大小
    {
        return _end_of_storage - _start;
    }
    
    bool empty() const // 判空
    {
        return size();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    reserve 函数:

    // 调整容器的容量大小(capacity)
    void reserve(size_t n)
    {
        if (n > capacity()) // 如果n大于当前 capacity 大小
        {
            size_t oldSize = size(); // 提前保存下旧空间size大小,便于后面更新_finish
    
            T* tmp = new T[n]; // 开辟并初始化新空间,n个T类型的对象
    
            if (_start)
            {
                // void * memcpy ( void * destination, const void * source, size_t num );
                
                memcpy(tmp, _start, sizeof(T) * oldSize); // 旧空间元素拷贝到新空间(浅拷贝)
    
                delete[] _start; // 释放旧空间
            }
    
            _start = tmp; // 指向新空间
    
            _finish = _start + oldSize;   // 更新有效元素长度
            _end_of_storage = _start + n; // 更新容量
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    思考:reserve 函数内部使用 memcpy 函数对数据进行字节序拷贝,实际上是有问题的。👇


    使用 memcpy 浅拷贝的问题(⭐)

    请看下面这段代码,会发生什么问题吗?

    void test7()
    {
        // memcpy值拷贝的问题
    
        vector<string> v;
        v.reserve(4); // 调整vector的容量为4
        v.push_back("111");
        v.push_back("222");
        v.push_back("333");
        v.push_back("444");
        v.push_back("555"); // 这里会发生扩容,调用reserve函数
    
        for (auto& e : v)
            cout << e << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:程序崩溃

    问题分析

    1. memcpy 是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中(字节序拷贝)。
    2. 如果拷贝的是内置类型的元素,memcpy 即高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时(比如在堆上申请的有资源),就会出错,因为 memcpy 的拷贝实际是浅拷贝

    画图分析

    1. 开空间,插入 4 个 string 类对象:

    image-20220515112708420

    1. 插入第 5 个 string 类对象,引发扩容,调用 reserve 函数,使用 memcpy 按字节序拷贝元素到新空间中,是浅拷贝,导致两个对象共享同一份资源:

    image-20220515113548929

    1. 程序结束时,已被析构的空间再次被析构,导致程序崩溃!

    image-20220515115728078


    解决 memcpy 浅拷贝的问题(⭐)

    • 容器中存的是内置类型元素,直接赋值即可
    • 容器中存的是自定义类型元素,通过调用该自定义类型的赋值运算符重载函数 operator= 完成「深拷贝

    👉 修改后的 reserve 函数如下:

    // 调整容器的容量大小(capacity)
    void reserve(size_t n)
    {
        if (n > capacity()) // 如果n大于当前 capacity 大小
        {
            size_t oldSize = size(); // 提前保存下旧空间size大小,便于后面更新_finish
    
            T* tmp = new T[n]; // 开辟并初始化新空间,n个T类型的对象
    
            if (_start)
            {
                // 如果T是int,直接赋值即可
    			// 如果T是string,就调用string类的赋值重载(进行深拷贝)
                // 如果T是vector,就调用vector类的赋值重载(进行深拷贝)
                for (size_t i = 0; i < oldSize; i++) // 旧空间元素赋值到新空间
                    tmp[i] = _start[i]; // 赋值
    
                delete[] _start; // 释放旧空间
            }
    
            _start = tmp; // 指向新空间
    
            _finish = _start + oldSize;   // 更新有效元素长度
            _end_of_storage = _start + n; // 更新容量
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    👉 resize 函数:

    // 调整容器的有效元素大小(size)
    void resize(size_t n, const T& val = T())
    {
        // 如果n小于当前size,,则有效元素个数缩小到n
        if (n < size())
        {
            _finish = _start + n;
        }
        // 如果n大于当前size
        else if (n > size())
        {
            // 如果n大于当前capacity,先进行增容
            if (n > capacity()) reserve(n);
    
            // 多出的位置用val或者缺省值T()填充
            while (_finish < _start + n)
            {
                *_finish = val;
                _finish++;      // 有效元素长度+1
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ③ 访问操作

    // []运算符重载,普通版本和 const 版本
    T& operator[](const size_t pos)
    {
        assert(pos >= 0 && pos < size()); // 检查越界
    
        return _start[pos];
    }
    
    T& operator[](const size_t pos) const
    {
        assert(pos >= 0 && pos < size()); // 检查越界
    
        return _start[pos];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ④ 修改操作

    👉 insert 函数:注意迭代器失效问题!!!

    iterator insert(iterator pos, const T& val = T())
    {
        assert(pos >= _start && pos <= _finish);
    
        size_t len = pos - _start; // 记录下pos相对_start的长度
        if (_finish == _end_of_storage) // 先检查是否需要扩容
        {
            size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
            reserve(newcapacity);
            // 注意:扩容后,pos还指向旧空间,pos位置迭代器已失效,需要重置pos
            pos = _start + len; // 解决迭代器失效问题⭐
        }
    
        // 往后挪动元素
        for (iterator end = _finish; end > pos; end--)
        {
            *end = *(end - 1);
        }
    
        *pos = val; // 在pos迭代器位置处插入元素
        _finish++;  // 有效元素长度+1
    
        return pos; // 返回指向第一个新插入元素的迭代器⭐
        // pos是传值传参,形参改变不会影响实参,所以更需要返回pos
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    👉 erase 函数:注意迭代器失效问题!!!

    iterator erase(iterator pos)
    {
        assert(pos >= _start && pos < _finish);
    
        // 往前挪动元素
        for (iterator it = pos + 1; it < _finish; it++)
        {
            *(it - 1) = *it;
        }
    
        _finish--; // 有效元素长度-1
    
        return pos; // 返回指向被删除元素下一个位置的迭代器⭐
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    👉 push_back 函数:

    void push_back(const T& x)
    {
    	/*
        if (_finish == _end_of_storage) // 先检查是否需要增容
        {
            size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
    
            reserve(newcapacity);
        }
    
        *_finish = x; // 尾插元素
        _finish++;
    	*/
    
        /* 复用 insert 函数的代码 */
    
        insert(_finish, x);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    测试:

    void test2()
    {
        vector<string> v;
    
        // 3种传值方式:
    
        string s("hello");
        v.push_back(s); // 构造有名对象作为实参传递
    
        v.push_back(string("hello")); // 构造一个匿名对象作为实参传递
    
        v.push_back("hello"); // 单参数的构造函数支持隐式类型转换
        /*
    	* 本质是用一个"hello"给string类型对象赋值
    	* 实际上编译器,会先用"hello"作为参数构造出一个临时对象,再用临时对象拷贝构造出新对象
    	* 最终编译器进行了优化,直接用"hello"作为参数构造出新对象
    	*/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    👉 pop_back 函数:

    void pop_back()
    {
    	/*
        assert(!empty());
        _finish--;
    	*/
    
        /* 复用 erase 函数的代码 */
    
        erase(--end());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    五. 动态二维数组

    以杨辉三角为例:

    image-20220512162325264

    class Solution {
    public:
        vector<vector<int>> generate(int n) {
            // 开辟和初始化杨辉三角存储空间
            vector<vector<int>> vv(n);
            for (size_t i = 0; i < vv.size(); i++)
            {
                vv[i].resize(i + 1, 0);
                
                // 每一行第一个元素和最后一个元素初始化为1
                vv[i][0] = 1;
                vv[i][vv[i].size() - 1] = 1;
            }
    
            // 填充杨辉三角
            for (size_t i = 2; i < vv.size(); i++) // 从第3行开始
                for (size_t j = 1; j < vv[i].size() - 1; j++) // 从第2列开始
                    vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
    
            return vv;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    二维数组 vector> vv 中总共有 n 个元素,每个元素都是 vector 类型的。

    当 n = 5 时:

    image-20220518202726771
    填充二维数组的每一个元素:
    image-20220518202813656

  • 相关阅读:
    【C++】特殊类设计
    如何看待程序员领域内的“内卷”现象?
    (计算机组成原理)第二章数据的表示和运算-第一节3:无符号数的表示和运算
    【YOLOv5】手把手教你使用LabVIEW ONNX Runtime部署 TensorRT加速,实现YOLOv5实时物体识别(含源码)
    CORE: Automatic Molecule Optimization using Copy and Refine Strategy(论文解读)
    javaH5醉美南湾湖网站设计计算机毕业设计MyBatis+系统+LW文档+源码+调试部署
    [爬虫]2.2.1 使用Selenium库模拟浏览器操作
    时序数据库介绍及应用场景,C#实例
    【C++】红黑树插入过程详解
    思科Nexus 9000系列交换机光模块解决方案
  • 原文地址:https://blog.csdn.net/Y673789476/article/details/126350487