【C++初阶】:(10)vector的使用及模拟实现

1. vector的介绍

  • vector的基本定义:

vector(向量)是一种序列容器,本质上是可以改变大小的数组。

就像普通数组一样,vector 使用连续的存储空间来存放元素。这意味着它的元素可以通过常规指针的偏移量来访问,且效率与数组一样高。但与数组不同的是,vector 的大小可以动态变化,其存储由容器自动处理。

  • vector的内存管理与扩容策略:

在内部,vector 使用一个动态分配的数组来存储元素。当插入新元素时,为了增长大小,这个数组可能需要重新分配------即分配一个新的更大的数组并将所有旧元素移动过去。这是一个相对耗时的操作,因此 vector 不会在每次添加元素时都重新分配内存。

相反,vector 容器通常会分配一些额外的存储空间 以适应未来的增长。因此,容器的实际容量通常大于严格容纳其元素所需的存储空间(即当前大小)。虽然不同的标准库实现可能会采用不同的增长策略来平衡内存使用和重分配次数,但在任何情况下,重分配都应该以对数级增长的间隔 发生。这样可以确保在 vector 尾部插入单个元素的操作具有分摊常数时间复杂度

  • 与其他容器的对比

对比普通数组:vector 通过消耗更多的内存(预留空间),换取了能够高效管理存储和动态增长的能力。

对比其他动态序列容器(如 deque, list, forward_list)

优势 :vector 在访问元素方面非常高效(像数组一样支持随机访问),在尾部添加或删除元素也相对高效。

劣势:对于涉及在非尾部位置插入或删除元素的操作,vector 的性能不如其他容器;而且相比于 list 和 forward_list,vector 的迭代器和引用在修改过程中更容易失效(一致性较差)。

2. vector核心函数的使用

2.1 vector的定义

vector提供了4中常用的构造函数用来满足不同的构造需求,如下表所示:

|---------------------------------------------------------------|----------------|
| 构造函数声明 | 功能说明 |
| vector() | 无参构造 |
| vector(size_type n,const value_type& val = value_type()) | 构造并初始化n个val |
| vector(const vector& x) | 拷贝构造 |
| vector(InputVector first,InputVector last) | 使用迭代器区间进行初始化构造 |

使用实例如下:

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;

//vector的初始化
void test_vector1()
{
	//无参构造
	vector<int>v1;
    cout<<"v1的size大小为:"<<v1.size()<<"v1的capacity大小:"<<v1.capacity()<<endl;
	//构造并初始化10个1(n个val)
	vector<int>v2(10, 1);
    for(int i = 0;i<v2.size();++i)
    {
        cout<<v2[i]<<" ";
    }
    cout<<endl; 
	//使用迭代器进行初始化构造
	vector<int>v3(v2.begin(), v2.end());
    for(auto e:v3)
    {
        cout<<e<<" ";
    }
    cout<<endl;
    //使用拷贝构造
    vector<int>v4(v3);
    for(auto a:v4)
    {
        cout<<a<<" "
    }
    cout<<endl;
}

2.2 vector iterator的使用

iterator--迭代器:提供了统一的接口来访问容器中的元素,不需要担心不同容器底层数据结构的差异。vector iterator的本质是类型为int*的原生指针。

下表为vector常用的迭代器接口:

|---------------------|-------------------------------------------------------------------|
| iterator的使用 | 接口说明 |
| begin()+end() | begin()返回指向容器中第一个元素的迭代器;end()返回指向容器中最后一个元素下一个位置的迭代器。区间左闭右开。 |
| rbegin()+rend() | rbegin()返回指向容器中最后一个元素的反向迭代器;rend()返回指向容器中第一个元素前一个位置的反向迭代器。区间左开右闭。 |
| cbegin()+cend() | 与begin()+end()的功能一致,但是返回的是const修饰的迭代器,只能执行"读"操作,不能执行"写"操作。 |

迭代器的使用代码演示如下:

cpp 复制代码
int main()
{
    vector<int> v={3,4,5,6,7}
    
    //正向迭代器遍历
    auto it = v.begin();
    while(it!=v.end())
    {
        cout<<*it<<" ";
        ++it;
    }
    cout<<endl;

    //反向迭代器遍历
    auto rit v.rbegin();
    while(rit!=v.rend())
    {
        cout<<*rit<<" ";
        ++rit;
    }
    cout<<endl;
    
    //const迭代器遍历
    auto cit = v.cbegin();
    while(cit!=v.cend())
    {
        //错误操作,修改元素:*cit=2;
        cout<<*cit<<" ";
    }
    cout<<endl;
    return 0;
}

2.3 vector空间增长问题

|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
| 容量空间 | 接口说明 |
| size() | 返回当前容器中的有效元素个数 |
| capacity() | 返回当前容器的容量大小 |
| empty() | 判断容器是否为空 |
| resize(size_t type n,value_type val = value_type()) | 调整vector的size为n:若n>当前size,在尾部补上n-size个val,若未指定val,内置类型补0,自定义类型调用默认构造函数;若n<当前size:删除尾部的size-n个元素;若n>当前capacity,则进行扩容操作之后再补齐元素。 |
| reserve(size_type n) | 调整vector的capacity为n(只负责开辟空间,不初始化元素)。当n>当前capacity,扩容capacity至n;当n<=当前capacity,不进行操作(不会缩容)。 |

  • 在不同编译环境下,vector的capacity增长策略存在差异:Visual Studio(采用PJ版STL)按1.5倍扩容,而GCC(采用SGI版STL)按2倍扩容。需注意,vector的扩容系数并非固定值,具体增长倍数由STL实现版本决定,需根据实际环境验证。
  • reserve函数仅负责预分配内存空间,若提前明确所需容量,可通过reserve避免多次扩容带来的性能损耗,从而优化vector的动态增长效率。
  • resize函数在分配新空间的同时会完成元素初始化,并直接修改vector的size值,需根据实际需求选择使用。

下面的代码用来探究VS下的vector的扩容机制:

cpp 复制代码
//测试vector的默认扩容机制
void TestVectorExpand()
{
	size_t sz;
	vector<int>v;
	sz = v.capacity();
	cout << "当前sz的大小为: " << sz << endl;
	for (int i = 0; i < 100; ++i)
	{
		v.push_back(i);
		if (sz != v.capacity())
		{
			sz = v.capacity();
			cout << "sz大小变为: " << sz << endl;
		}
	}
}
void test_vector2()
{
	//TestVectorExpand();

	//探究verctor::reserve的capacity扩容机制
	vector<int>v(10, 1);
	cout<< "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;

	v.reserve(20);
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;

	v.reserve(15);
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;

	v.reserve(5);
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;
}

void test_vector3()
{
	//探究vector::resize()的机制

	vector<int>v(10, 1);
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;

	v.resize(5);
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;
	v.resize(15);
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;
	v.resize(20);
	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;
	cout << "size大小为: " << v.size() << endl;
	cout << "capacity大小为: " << v.capacity() << endl;
}

2.4 vector的增删查改

|----------------------------------------------------------------|---------------------------------------------------------------------------|
| 增删查改接口 | 接口说明 |
| push_back(const value_type& val) | 尾插 |
| pop_back() | 尾删 |
| find(InputIterator first,InputIterator last,const T& val) | 查找[first,last]这个迭代器区间中的val',返回指向val的迭代器;注意这个算法模块是算法实现的,不是vector的成员函数接口。 |
| insert(iterator pos,const value_type& val) | 在pos指向的空间中插入元素 |
| erase(iterator pos) | 删除pos指向的空间中的元素 |
| swap(vector& x) | 交换两个vector的数据空间 |
| operator[ ](size_t n) | 像数组一样使用下标进行元素访问,无越界检查 |
| at(size_type n) | 同样是进行下标访问,但是有越界检查(越界抛出out_of_range异常) |

使用代码演示:

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm> // for find
using namespace std;
 
int main() {
    vector<int> v = {1, 2, 3, 4};
 
    // 1. 尾插(push_back)
    v.push_back(5);
    cout << "push_back(5)后: ";
    for (auto e : v) { cout << e << " "; } // 1 2 3 4 5
    cout << endl;
 
    // 2. 尾删(pop_back)
    v.pop_back();
    cout << "pop_back()后: ";
    for (auto e : v) { cout << e << " "; } // 1 2 3 4
    cout << endl;
 
    // 3. 查找(find)
    auto pos = find(v.begin(), v.end(), 3);
    if (pos != v.end()) {
        cout << "找到元素3,位置:" << pos - v.begin() << endl; // 2(下标从0开始)
    } else {
        cout << "未找到元素3" << endl;
    }
 
    // 4. 插入(insert)
    pos = v.begin() + 2; // 指向第3个元素(3)
    v.insert(pos, 30); // 在3之前插入30
    cout << "insert(30)后: ";
    for (auto e : v) { cout << e << " "; } // 1 2 30 3 4
    cout << endl;
 
    // 5. 删除(erase)
    pos = v.begin() + 2; // 指向30
    v.erase(pos); // 删除30
    cout << "erase(30)后: ";
    for (auto e : v) { cout << e << " "; } // 1 2 3 4
    cout << endl;
 
    // 6. 交换(swap)
    vector<int> v2 = {10, 20, 30};
    v.swap(v2);
    cout << "swap后v: ";
    for (auto e : v) { cout << e << " "; } // 10 20 30
    cout << ",v2: ";
    for (auto e : v2) { cout << e << " "; } // 1 2 3 4
    cout << endl;
 
    // 7. 下标访问(operator[] vs at)
    cout << "v[1] = " << v[1] << endl; // 20(无越界检查)
    // cout << "v[10] = " << v[10] << endl; // 未定义行为(乱码或崩溃)
    try {
        cout << "v.at(10) = " << v.at(10) << endl; // 抛出out_of_range异常
    } catch (const out_of_range& e) {
        cout << "at()越界:" << e.what() << endl;
    }
 
    return 0;
}

3. vector核心函数的模拟实现

3.1 vector的底层结构

vector的底层结构和string是有所区别的,string由一个核心指针进行维护,而vertor使用三个核心指针进行维护,但这两者本质上是没有区别的,只不过vector把指针细分成了三个:

  • _start:指向底层数组的第一个元素;
  • _finish:指向底层数组中最后一个有效元素的下一个位置,即_start+size;
  • _end_of_storage:指向底层数组的最后一个位置,即_start+capacity.
cpp 复制代码
size_t size()
	{
		return _finish - _start;
	}
size_t capacity()
	{
		return _end_of_storage - _start;
	}

3.2 核心接口的模拟实现

由于 vector 是基于类模板实现的,其成员函数的声明与定义必须保留在同一个文件中(通常是头文件)。这一点与上一节模拟实现的 string 类有所不同:string 类并非类模板,因此可以将声明和定义分别存放在头文件和源文件中。在使用 vector 时,请务必注意这一区别。

在vector头文件vector.h中:

cpp 复制代码
#pragma once
//vector类的模拟实现
//由于vector是基于模板进行实现的,所以成员函数的声明和定义不能分离!
#include<iostream>
#include<assert.h>
#include<list>
#include<string>
using namespace std;

namespace yanjun
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		/*vector()
		{}*/

		// C++11 强制生成默认构造
		vector() = default;

		vector(const vector<T>& v)
		{
			reserve(v.size());
			for (auto& e : v)
			{
				push_back(e);
			}
		}

		//类模板的成员函数,还可以继续是函数模板
		//下面这个示例不直接使用iterator是因为这里的iterator只能是vector的迭代器,写成函数模板可以代表任意类型的迭代器
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)//迭代器区间的构造,需要推导 
		{
			while (first != last)//不使用first<last是因为链表可能会有这种情况,因为他的内存空间不是连续的,为了普适性,使用first!=last
			{
				//在迭代器区间中插入数据
				push_back(*first);
				++first;
			}
		
		}
		//需要推导
		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 (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		void clear()
		{
			_finish = _start;
		}
		// v1 = v3
		/*vector<T>& operator=(const vector<T>& v)
		{
			if (this != &v)
			{
				clear();

				reserve(v.size());
				for (auto& e : v)
				{
					push_back(e);
				}
			}

			return *this;
		}*/

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

		// v1 = v3
		//vector& operator=(vector v)
		//这里的形参必须使用传值,这样v就是v3的拷贝了!!!
		vector<T>& operator=(vector<T> v)
			//对于类模板,类里面可以用类名直接替代类型,类外面不能这样干
			//vector& operator=(vector v)
		{
			swap(v);

			return *this;
		}

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				//下面这段代码对于内置类型是没问题的,但是对于自定义类型进行的是浅拷贝,会出现问题
				//memcpy(tmp, _start, old_size * sizeof(T));
				// 所以要对上面这段代码进行改进,复用赋值函数进行深拷贝
				for (size_t i = 0; i < old_size; ++i)
				{
					tmp[i] = _start[i];
				}
				//释放旧空间
				delete[] _start;
				//指向新空间
				_start = tmp;
				//更新剩余指针的指向位置
				//_finish = _start + size();//写在这个位置会导致finish=空指针,因为此时的_start已经是更新后的了
				_finish = tmp + old_size;
				_end_of_storage = _start + n;
			}
		}
		size_t size()
		{
			return _finish - _start;
		}
		size_t capacity()
		{
			return _end_of_storage - _start;
		}

		bool empty()
		{
			return _start == _finish;
		}

		void push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : 2 * capacity());
			}
			*_finish = x;
			++_finish;
		}
		//可读可写版本
		T& operator[](size_t i)
		{
			assert(i < size());
			return _start[i];
			//上面一行代码等同于
			//return = *(_start+i);
		}
		//只读版本
		const T& operator[](size_t i) const
		{
			assert(i < size());
			return _start[i];
			//上面一行代码等同于
			//return = *(_start+i);
		}
		//删除最后一个元素
		void pop_back()
		{
			assert(!empty());
			--_finish;
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//首先检查扩容
			if (_finish == _end_of_storage)
			{
				//记录pos相对于_start的相对位置
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				//更新扩容后在新空间中pos的位置,防止迭代器失效
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			iterator it = pos + 1;
			while (it != end())
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}
		//因为这里传输的参数类型是T类型的,所有不能使用一个固定值进行初始化,这样会影响编译器的自动推导,所以这里使用构造函数进行初始化
		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				//进行删除操作
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
	private:
		iterator _start = nullptr;;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

测试模块中,创建test.cpp

cpp 复制代码
name yanjun
{
void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(4);
		v.push_back(5);
		print_vector(v);
		auto it = v.begin();

		//这种写法会导致迭代器失效
		//while (it != v.end())
		//{
		//	if (*it % 2 == 0)
		//	{
		//		v.erase(it);
		//	}
		//	++it;
		//}

		//这样写就不会发生迭代器失效
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;
			}
		}
		print_container(v);

	}
	void test_vector3()
	{
		int i = int();
		int j = int(1);
		int k(2);

		vector<int> v;
		v.resize(10, 1);
		v.reserve(20);

		print_container(v);
		cout << v.size() << endl;
		cout << v.capacity() << endl;

		v.resize(15, 2);
		print_container(v);

		v.resize(25, 3);
		print_container(v);

		v.resize(5);
		print_container(v);
	}
	void test_vector4()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		print_container(v1);

		vector<int> v2 = v1;
		print_container(v2);

		vector<int> v3;
		v3.push_back(10);
		v3.push_back(20);
		v3.push_back(30);

		v1 = v3;
		print_container(v1);
		print_container(v3);
	}
	void test_vector5()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(4);
		v1.push_back(4);

		vector<int> v2(v1.begin(), v1.begin() + 3);
		print_container(v1);
		print_container(v2);

		list<int> lt;
		lt.push_back(10);
		lt.push_back(10);
		lt.push_back(10);
		lt.push_back(10);
		vector<int> v3(lt.begin(), lt.end());
		print_container(lt);
		print_container(v2);

		vector<string> v4(10, "1111111");
		print_container(v4);

		vector<int> v5(10);
		print_container(v5);

		vector<int> v6(10u, 1);
		print_container(v6);

		vector<int> v7(10, 1);//这种情况对于多个构造函数,编译器不知道选择哪个才好,会导致选择错误而报错,所以最好的解决方法就是直接提供现成的,不用推导的
		print_container(v7);
	}
	void test_vector6()
		//探究自定义类型深拷贝浅拷贝问题
	{
		vector<string> v;
		v.push_back("11111111111111111111");
		v.push_back("11111111111111111111");
		v.push_back("11111111111111111111");
		v.push_back("11111111111111111111");
		print_container(v);
		//如果扩容后使用原来的memcpy函数进行拷贝,进行的是浅拷贝,因为string是自定义类型,所以会出现错误,使用赋值函数能完成深拷贝从而解决问题
		v.push_back("11111111111111111111");
		print_container(v);
	}
}


int main()
{
    return 0;
}

4. 迭代器失效问题

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

会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize,reserve,inset,assign,push_back等。

cpp 复制代码
#include <iostream>
using namespace std;
#include <vector>
int main()
{
    vector<int> v{1,2,3,4,5,6};
    
    auto it = v.begin();
    
    // 将有效元素个数增加到100个,多出的位置使用8填充,操作期间底层会扩容
    // v.resize(100, 8);
    
    // reserve的作用就是改变扩容大小但不改变有效元素个数,操作期间可能会引起底层容
量改变
    // v.reserve(100);
    
    // 插入元素期间,可能会引起扩容,而导致原空间被释放
    // v.insert(v.begin(), 0);
    // v.push_back(8);
    
    // 给vector重新赋值,可能会引起底层容量改变
    v.assign(100, 8);
   
    while(it != v.end())
   {
        cout<< *it << " " ;
        ++it;
   }
    cout<<endl;
    return 0;
}

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

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


在我们的模拟实现过程中,我们就遇到了几种迭代器失效的情况,如下:

4.1 insert体现出的两种失效

4.1.1 扩容导致的失效

cpp 复制代码
类中成员函数:

		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//首先检查扩容
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}
测试模块:


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

		print_container(v);

		v.insert(v.begin() + 2, 30);
		print_vector(v);

		int x;
		cin >> x;
		auto p = find(v.begin(), v.end(), x);
		if (p != v.end())
		{
			// insert以后p就是失效,不要直接访问,要访问就要更新这个失效的迭代器的值
			v.insert(p, 20);
			(*p) *= 10;
		}
		print_container(v);
	}

4.1.2 逻辑位置改变导致的失效(非扩容)

cpp 复制代码
类中成员函数:

		void insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//首先检查扩容
			if (_finish == _end_of_storage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
		}
测试模块:


	void test_vector1()
	{
		std::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
        v.push_back(5);
		print_container(v);
		int x;
		cin >> x;
		auto p = find(v.begin(), v.end(), x);
		if (p != v.end())
		{
			v.insert(p, 20);
			(*p) *= 10;
		}
		print_container(v);
	}

迭代器失效详解

在之前的错误版本测试中,我们观察到了两种典型的迭代器失效情况:

1. 扩容导致的失效(野指针)

在无参构造 vector 并尾插 4 个元素后,容器恰好填满。此时执行 insert 操作(在 find 到的迭代器 pos 处插入元素 20):

  • 过程 :触发扩容机制,系统会开辟一块两倍于原空间的新内存,将旧数据拷贝过去,随后释放旧空间

  • 后果 :迭代器 pos 仍然指向已经被释放的旧内存地址。此时 pos 变成了野指针,任何对它的访问或操作都会导致程序崩溃。

2. 逻辑位置改变导致的失效(非扩容)

若先尾插 5 个元素(此时已完成扩容),再执行 insert 操作:

  • 过程 :由于容量充足,不涉及内存重新分配。但在 pos 位置插入新元素后,原位置的元素 x 会被向后移动(挪到下一个位置)。

  • 后果 :虽然迭代器 pos 指向的内存地址依然有效,但该地址存储的内容已变为新插入的元素,不再是原来的 x。这种逻辑上的错位同样属于迭代器失效。

要解决以上情况,就是及时的更新迭代器:

cpp 复制代码
类中成员函数:

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//首先检查扩容
			if (_finish == _end_of_storage)
			{
				//记录pos相对于_start的相对位置
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				//更新扩容后在新空间中pos的位置,防止迭代器失效
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
            return pos;
		}

测试模块:

	void test_vector1()
	{
		std::vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);

		print_container(v);

		int x;
		cin >> x;
		auto p = find(v.begin(), v.end(), x);
		if (p != v.end())
		{
			// insert以后p就是失效,不要直接访问,要访问就要更新这个失效的迭代器的值
			//v.insert(p, 20);
			//(*p) *= 10;
		    p = v.insert(p, 40);
			(*(p+1)) *= 10;
		}
		print_container(v);
	}

4.2 erase体现出的失效

错误代码:

cpp 复制代码
类中成员函数:

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

测试模块:

void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(4);
		v.push_back(5);
		print_vector(v);
		auto it = v.begin();

		//这种写法会导致迭代器失效
		while (it != v.end())
		{
            //删除容器中所有偶数
			if (*it % 2 == 0)
			{
				v.erase(it);
			}
			++it;
		}
	}

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

正确做法还是更新迭代器位置:

cpp 复制代码
类中成员函数:

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

测试模块:

void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(4);
		v.push_back(5);
		print_vector(v);
		auto it = v.begin();
		//这样写就不会发生迭代器失效
		while (it != v.end())
		{
			if (*it % 2 == 0)
			{
				it = v.erase(it);
			}
			else
			{
				++it;
			}
		}
		print_container(v);
	}

4.3 Linux下g++对失效的处理

Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。

cpp 复制代码
// 1. 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
int main()
{
    vector<int> v{1,2,3,4,5};
    for(size_t i = 0; i < v.size(); ++i)
         cout << v[i] << " ";
    cout << endl;
    auto it = v.begin();
    cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
   // 通过reserve将底层空间设置为100,目的是为了让vector的迭代器失效    
    v.reserve(100);
    cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
    
    // 经过上述reserve之后,it迭代器肯定会失效,在vs下程序就直接崩溃了,但是linux
下不会
    // 虽然可能运行,但是输出的结果是不对的
    while(it != v.end())
   {
        cout << *it << " ";
        ++it;
   }
    cout << endl;
    return 0;
}
程序输出:
1 2 3 4 5 
扩容之前,vector的容量为: 5
扩容之后,vector的容量为: 100
0 2 3 4 5 409 1 2 3 4 5
// 2. erase删除任意位置代码后,linux下迭代器并没有失效
// 因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
#include <vector>
#include <algorithm>
int main()
{
    vector<int> v{1,2,3,4,5};
    vector<int>::iterator it = find(v.begin(), v.end(), 3);
    v.erase(it);
    cout << *it << endl;
    while(it != v.end())
   {
        cout << *it << " ";
        ++it;
   }
    cout << endl;
    return 0;
}
程序可以正常运行,并打印:
4
4 5
cpp 复制代码
// 3: erase删除的迭代器如果是最后一个元素,删除之后it已经超过end
// 此时迭代器是无效的,++it导致程序崩溃
int main()
{
    vector<int> v{1,2,3,4,5};
   // vector<int> v{1,2,3,4,5,6};
    auto it = v.begin();
    while(it != v.end())
   {
        if(*it % 2 == 0)
            v.erase(it);
        ++it;
   }
    for(auto e : v)
       cout << e << " ";
    cout << endl;
    return 0;
}
========================================================
// 使用第一组数据时,程序可以运行
[sly@VM-0-3-centos 20220114]$ g++ testVector.cpp -std=c++11
[sly@VM-0-3-centos 20220114]$ ./a.out
1 3 5 
=========================================================
// 使用第二组数据时,程序最终会崩溃
[sly@VM-0-3-centos 20220114]$ vim testVector.cpp 
[sly@VM-0-3-centos 20220114]$ g++ testVector.cpp -std=c++11
[sly@VM-0-3-centos 20220114]$ ./a.out
Segmentation fault

从上述三个例子中可以看到:SGI STL中,迭代器失效后,代码并不一定会崩溃,但是运行 结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的。


4.4 string是否存在迭代器失效

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

cpp 复制代码
#include <string>
void TestString()
{
     string s("hello");
     auto it = s.begin();
     // 放开之后代码会崩溃,因为resize到20会string会进行扩容
     // 扩容之后,it指向之前旧空间已经被释放了,该迭代器就失效了
     // 后序打印时,再访问it指向的空间程序就会崩溃
     //s.resize(20, '!');
     while (it != s.end())
         {
             cout << *it;
             ++it;
         }
     cout << endl;
     it = s.begin();
     while (it != s.end())
        {
             it = s.erase(it);
             // 按照下面方式写,运行时程序会崩溃,因为erase(it)之后
             // it位置的迭代器就失效了
             // s.erase(it);  
             ++it;
         }
}    

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


5. 使用memcpy拷贝问题

首先看一段代码:

cpp 复制代码
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				memcpy(tmp, _start, old_size * sizeof(T));
				//释放旧空间
				delete[] _start;
				//指向新空间
				_start = tmp;
				//更新剩余指针的指向位置
				//_finish = _start + size();//写在这个位置会导致finish=空指针,因为此时的_start已经是更新后的了
				_finish = tmp + old_size;
				_end_of_storage = _start + n;
			}
		}

        int main()
            {
                 bite::vector<bite::string> v;
                 v.push_back("1111");
                 v.push_back("2222");
                 v.push_back("3333");
                 return 0;
            }

问题分析:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中;

  2. 如果拷贝的是内置类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型 元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅 拷贝。

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

所有正确写法如下,reserve函数不采用memcpy进行对象间拷贝:

cpp 复制代码
void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t old_size = size();
				T* tmp = new T[n];
				//下面这段代码对于内置类型是没问题的,但是对于自定义类型进行的是浅拷贝,会出现问题
				//memcpy(tmp, _start, old_size * sizeof(T));
				// 所以要对上面这段代码进行改进,复用赋值函数进行深拷贝
				for (size_t i = 0; i < old_size; ++i)
				{
					tmp[i] = _start[i];
				}
				//释放旧空间
				delete[] _start;
				//指向新空间
				_start = tmp;
				//更新剩余指针的指向位置
				//_finish = _start + size();//写在这个位置会导致finish=空指针,因为此时的_start已经是更新后的了
				_finish = tmp + old_size;
				_end_of_storage = _start + n;
			}

对于自定义类型,应通过 "逐个元素赋值" 或 "拷贝构造" 的方式进行深拷贝,确保每个元素的资源被正确复制,而不是简单的二进制拷贝。在模拟实现的reserve接口中,我们使用了:

cpp 复制代码
for (size_t i = 0; i < old_size; ++i)
	{
		tmp[i] = _start[i];
	}

对于string类型,tmp[i] = _start[i]会调用string的赋值运算符,分配新的堆内存并拷贝字符串内容,避免浅拷贝问题。

6. 动态二维数组:vector的嵌套

二维 Vector 底层结构解析

vector<vector<int>> vv 本质上是一个嵌套结构,其底层逻辑如下:

  • 外层容器vv 的每个元素都是一个独立的 vector<int> 对象。
  • 内存布局 :外层 vector_start 指针指向一段连续的内存空间,这段空间存储的是内层 vector 对象本身 。由于 vector 通常由三个指针(_start_finish_end_of_storage)组成,因此外层数组的每个元素实际上就是这组指针数据(在 64 位系统下通常占 24 字节)。
  • 独立性(不规则性) :每个内层 vector 都维护着自己独立的数据空间和容量。这意味着每一行都可以单独扩容,互不干扰,从而实现了变长二维数组(即每一行的长度可以不同)。

接下来利用标准库vector来构建动态二维数组实现杨辉三角:

cpp 复制代码
////杨辉三角的构建
	vector<vector<int>>generate(int numRows)
	{
		//首先构建行数
		vector<vector<int>>vv(numRows);
		for (size_t i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < vv.size(); ++i)
		{
			for (int j = 1; j < vv[i].size()-1; ++j)
			{
				vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
			}
		}
		return vv;
	}

上面代码中构造了一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:

vv中元素填充完成之后,如下图所示:

当然,我们使用模拟实现的vector构建动态二维数组时与上图实际是一致的。

相关推荐
所愿ღ2 小时前
SSM框架-Spring2
java·开发语言·笔记·spring
SariHcr1232 小时前
Openarm机器人双臂模型仿真从零部署
c++·人工智能·python·机器人·bash·openarm
故事还在继续吗2 小时前
C++11关键特性
开发语言·c++·算法
格林威2 小时前
面阵相机 vs 线阵相机:堡盟与Basler选型差异全解析 +C++ 实战演示
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
zzzsde2 小时前
【Linux】线程概念与控制(2)线程控制与核心概念
linux·运维·服务器·开发语言·算法
白夜11172 小时前
C++(不适合使用 CRTP情况)
开发语言·c++·笔记
宁静致远20212 小时前
ARM 架构 Ubuntu 20.04 / 22.04 触摸屏设备
linux·c++·ubuntu
栗少2 小时前
Python 入门教程(面向有 Java 经验的开发者)
java·开发语言·python
草莓熊Lotso2 小时前
Linux C++ 高并发编程:从原理到手撕,线程池全链路深度解析
linux·运维·服务器·开发语言·数据库·c++·mysql