栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C++ STL】vector 容器(介绍、使用、@迭代器失效问题、模拟实现、动态二维数组)

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C++ STL】vector 容器(介绍、使用、@迭代器失效问题、模拟实现、动态二维数组)

文章目录
      • 一、STL - vector 的介绍
      • 二、vector 的使用(常用接口介绍)
        • 2.1 常见构造
        • 2.2 迭代器及遍历操作
        • 2.3 容量操作
        • 2.4 访问操作
        • 2.5 修改操作
        • 2.6 vector 迭代器失效问题(⭐重要)
        • 2.7 补充:其它接口
      • 三、源码剖析:vector 的底层结构
      • 四、vector 的模拟实现
        • 4.1 vector 的结构
        • 4.2 vector 的迭代器
        • 4.3 一些成员函数的实现
          • ① 默认成员函数
          • ② 容量操作
            • 使用 memcpy 浅拷贝的问题(⭐)
            • 解决 memcpy 浅拷贝的问题(⭐)
          • ③ 访问操作
          • ④ 修改操作
      • 五、动态二维数组

一、STL - vector 的介绍

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

  1. vector 是表示可变大小数组的序列容器。

  2. 就像数组一样,vector 也采用的连续存储空间来存储元素。也就是意味着可以采用下标对 vector 的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

  3. 本质讲,vector 使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector 并不会每次都重新分配大小。

  4. vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。

  5. 因此,vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。

  6. 与其它动态序列容器相比(deques, lists and forward_lists), vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起 lists 和 forward_lists 统一的迭代器和引用更好。

二、vector 的使用(常用接口介绍)
template < class T, class Alloc = allocator > class vector; // generic template
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 first;                                // 构造一个没有元素的空容器
	std::vector second (4,100);                       // 4个值为100的整数
	std::vector third (second.begin(), second.end()); // 迭代器构造
	std::vector fourth (third);                       // 拷贝构造
    
	// 迭代器构造函数也可以使用数组来进行构造,传的区间是左闭右开
    // 因为指向数组空间的指针是天然的迭代器
	int arr[] = {16,2,77,29};
    std::vector fifth (arr, arr + 4 );
	// std::vector fifth (arr, arr + sizeof(arr) / sizeof(int) );
    
    // first : []
    // second: [100,100,100,100]
    // third : [100,100,100,100]
    // fourth: [100,100,100,100]
    // fifth : [16,2,77,29]
}

Example2:不实用

void test2()
{
	// 用其它容器的迭代器初始化,只要数据d类型可以匹配上
	string s("hello");
	vector v(s.begin(), s.end());
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

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

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

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

Example:

void test1()
{
	vector 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::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	// 3. 范围for
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

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

Example1:

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

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

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

比如:

void test3()
{
    vector v;
    v.resize(10); // v中的10个有效元素全是缺省值''
}

Example2:

reserve 函数介绍:只是开空间

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

void reserve (size_type n);

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

void test3()
{
	vector v;
	v.reserve(10); // 有效元素个数 size = 0,只是开了空间 capacity = 10
	
    
    
    // 正确做法
    for (size_t i = 0; i < 10; i++)
    {
        v.push_back(i);
    }
}

图示说明:

Example2:

如果不断往 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;
// 函数内部会检查越界

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

Example1:

insert 函数介绍:

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

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

erase 函数介绍:

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

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

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

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

Example2:

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

void swap (vector& x);

比如:

vector v1(2, 10); // v1 = [10, 10]
vector v2(5, 50); // v2 = [50, 50, 50 ,50 ,50]
v1.swap(v2);
// v1 = [50, 50, 50 ,50 ,50]
// v2 = [10, 10]

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

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector 的迭代器就是原生态指针 T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

对于 vector,会引起其底层空间改变的操作,都有可能会导致其迭代器失效,比如:resize、reserve、insert、assign、push_back 等,还有 erase(指定位置元素的删除操作)

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

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

    情况一:

    void test6()
    {
    	// 迭代器失效问题 -- 类似野指针问题
    	vector v;
    	v.push_back(1);
    	v.push_back(2);
    	v.push_back(3);
    
    	vector::iterator pos = find(v.begin(), v.end(), 2);
    	if (pos != v.end())
    	{
    		v.insert(pos, 20); // 在pos之前插入20
            
            
    	}
    
    	// 在insert之后,pos迭代器位置就失效了,不能再去访问了 -- insert时增容导致的
    	//cout << *pos << endl;
    	/
    	}
    
    	// 在insert时空间足够,没有增容,但我们依旧认为pos迭代器位置失效了,不能再去访问了
        // 这里的失效是指pos位置的意义变了,pos不再指向原来的值
    	//cout << *pos << endl;
    	/
    	}
    
    	// 在erase之后,pos迭代器位置就失效了,不能再去访问了
    	// 这里的失效是指pos位置的意义变了,pos不再指向原来的值
    	//cout << *pos << endl;
    	/
    	template
    	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
    		vector(InputIterator first, InputIterator last); // 使用迭代器初始化构造
    		
            vector(const vector& v); // 拷贝构造(深拷贝)
    		
    		void swap(vector& v) // 交换两个容器的内容
    
    		vector& operator=(vector 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(); // 尾删
    	};
    }
    

    4.2 vector 的迭代器

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

    namespace winter
    {
    	
    	template
    	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:
            // ...
            // ...
        };
    }
    

    4.3 一些成员函数的实现 ① 默认成员函数

     构造函数:

    // 无参构造
    vector()
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {}
    
    // 使用迭代器进行初始化构造 [first,last)
    // 注意:若使用vector的 iterator 做形参,会导致初始化的迭代器区间[first,last)只能是vector的迭代器
    
    template
    vector(InputIterator first, InputIterator last) // [first,last)
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
        while (first != last)
        {
            push_back(*first);
            first++;
        }
    }
    

    测试:

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

    
    // 拷贝构造(深拷贝),传统写法1
    // v2(v1)
    vector(const vector& 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& v) 
        :_start(nullptr)
    	, _finish(nullptr)
    	, _end_of_storage(nullptr)
    {
    	reserve(v.capacity());  // 调整新容器容量大小(这样的好处是:一次性把空间开好,效率高)
    	for (const auto& e : v) // 尾插元素到新容器中(复用push_back函数)
    		push_back(e);
    }
    

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

    // 赋值运算符重载(深拷贝),传统写法
    // v2 = v1
    vector& operator=(const vector& 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;
    }		
    

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

    // 交换两个容器的内容
    // v1.swap(v2)
    void swap(vector& v)
    {
        // 函数名冲突,指定去调用全局域里面的std::swap
        std::swap(_start, v._start);
        std::swap(_finish, v._finish);
        std::swap(_end_of_storage, v._end_of_storage);
    }
    

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

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

     赋值运算符重载函数:现代写法(赋值重载一般都是推荐现代写法,很方便,也很香)

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

     析构函数:

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

    ② 容量操作

     size、capacity、empty 函数:

    size_t size() const // 有效元素个数
    {
        return _finish - _start;
    }
    
    size_t capacity() const // 容量大小
    {
        return _end_of_storage - _start;
    }
    
    bool empty() const // 判空
    {
        return size();
    }
    

    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; // 更新容量
        }
    }
    

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


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

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

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

    画图分析:

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

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

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


    解决 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; // 更新容量
        }
    }
    

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

    ③ 访问操作
    // []运算符重载,普通版本和 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];
    }
    
    ④ 修改操作

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

     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; // 返回指向被删除元素下一个位置的迭代器⭐
    }
    

     push_back 函数:

    void push_back(const T& x)
    {
    	
    
        
    
        insert(_finish, x);
    }
    

    测试:

    void test2()
    {
        vector v;
    
        // 3种传值方式:
    
        string s("hello");
        v.push_back(s); // 构造有名对象作为实参传递
    
        v.push_back(string("hello")); // 构造一个匿名对象作为实参传递
    
        v.push_back("hello"); // 单参数的构造函数支持隐式类型转换
        
    }
    

     pop_back 函数:

    void pop_back()
    {
    	
    
        
    
        erase(--end());
    }
    

    五、动态二维数组

    以杨辉三角为例:

    class Solution {
    public:
        vector> generate(int n) {
            // 开辟和初始化杨辉三角存储空间
            vector> 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;
        }
    };
    

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

    当 n = 5 时:

    填充二维数组的每一个元素:


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/935837.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号