C++STL之vector容器

1 Vector容器的插入

1.1 简单使用vector容器的插入方法
vector的插入不难,一般我们在使用时都是使用push_back插入,当使用下标法时在不指定vec大小很容易越界,编译器不会开辟默认容量(我记得以前的好像会默认开辟20大小的容量)。

//vector的插入
void test01() {
	vector<int> vi1;
	
	//1 push_back往尾部插入法
	vi1.push_back(10);
	vi1.push_back(40);
	vi1.push_back(20);
	vi1.push_back(30);

	//2 调用insert插入,参1代表插入的开始位置,参2代表插入的个数,参3代表插入的值.
	vi1.insert(vi1.begin(), 2, 5);
	PrintVector(vi1); //5,5,10,40,20,30插在前面

	//3 下标法
	//不建议使用,因为非常容易越界,目前的编译器不会再开辟默认容量(默认构造下),而是你插入多少个容量就是多少
	vi1[1] = 3;//ok,5的值改成3
	//vi1[10] = 10;//error,数组越界
	//vi1[1000] = 1000;//error,数组越界

}

看图说话容量确实是没有默认值(vs2017),对应上面的容量大小为6,而非默认大小容量。
在这里插入图片描述

1.2 测试vector容器的插入是否会造成迭代器失效

1.2.1 测试push_back的插入是否会使迭代器改变失效:

  • 情况1:
    首先获取未知的迭代器,然后往vec.insert或者push_back后都没有改变迭代器。
    在这里插入图片描述

  • 情况2:
    插入之前迭代器的下标为指向100的地址,push_back后扔指向100,也没有改变迭代器。
    在这里插入图片描述
    在这里插入图片描述

以上两种情况的测试代码为:

//vector的插入   insert()
void test01() {

	vector<int> vi1;

	auto it1 = vi1.begin();
	auto it2 = vi1.end();

	//1 push_back往尾部插入法
	vi1.push_back(10);
	vi1.push_back(40);
	vi1.push_back(20);
	vi1.push_back(30);

	//2 调用insert插入,参1代表插入的开始位置,参2代表插入的个数,参3代表插入的值.
	vi1.insert(vi1.begin(), 2, 5);
	PrintVector(vi1); //5,5,10,40,20,30插在前面

	//3 下标法
	//不建议使用,因为非常容易越界,目前的编译器不会再开辟默认容量(默认构造下),而是你插入多少个容量就是多少
	vi1[1] = 3;//ok,5的值改成3
	//vi1[10] = 10;//error,数组越界
	//vi1[1000] = 1000;//error,数组越界

	for (auto i = 0; i < 100000; i++) {
		vi1.push_back(i);
	}
	
	//it1 = vi1.begin();
	for (auto it = vi1.begin(); it != vi1.end(); it++) {
		if (*it == 100) {
			vi1.push_back(1);
		}
	}

}

上面两种情况没有改变迭代器,所以可以正常使用,但这是没有指定大小的情况,并且这是单线程的情况下。

  • 情况3(这种情况很重要):
    先指定容量为6,当再push_back插入时迭代器是否发生改变:
    结果可以看到,当resize容量为6后(注意:此时已经有6个值在vec,所以我们只需要插入一个值即可测试),我们记录插入前的迭代器指向末尾3的地址,再push_back插入一个值后,迭代器指向改变了,说明系统帮我们自动内存搬家,旧的那六块内存已经变成未知也就是不可访问了。
    在这里插入图片描述
    在这里插入图片描述

情况3再访问时就会报错。
在这里插入图片描述

情况3的测试代码:

void test01() {

	vector<int> vi1;
	vi1.resize(6);//指定容量

	vi1[5] = 3;//为了标记而已,你也可以随便标记一个值(例如下标2)用于下面插入,同样也像上面一样

	for (auto it = vi1.begin(); it != vi1.end(); it++) {
		if (*it == 3) {
			vi1.push_back(1);//模拟刚好容量不足时插入
		}
	}

}

所以,通过上面push_back的使用,单线程指定大小或者多线程的情况下(操作同一全局vector变量时),push_back是不安全的。例如一个线程在遍历着迭代器,另一个线程push_back后内存刚好搬家,导致迭代器再访问就会失效报错。

1.2.2 测试使用insert函数往数组中间任意位置插入后,当前迭代器(即当前迭代器及其后面迭代器,因为无法索引)是否失效:
注:(内存容量足够的情况下,因为容量刚好不足时上面已经测试)。

1)首先插入前迭代器指向有效值(可以自己使用快速监视取&地址查看)。
在这里插入图片描述

2)插入后迭代器指向非法值,即该迭代器失效了。
在这里插入图片描述
再循环的话就报错了:
在这里插入图片描述

insert测试的代码:

//vector的插入   insert()
void test01() {

	vector<int> vi1;

	auto it1 = vi1.begin();
	auto it2 = vi1.end();

	//1 push_back往尾部插入法
	vi1.push_back(10);
	vi1.push_back(40);
	vi1.push_back(20);
	vi1.push_back(30);
	
	vi1[2] = 3;//ok,5的值改成3

	for (auto it = vi1.begin(); it != vi1.end(); it++) {
		if (*it == 40) {
			vi1.insert(it, 50);//往40的迭代器后面插入
		}
	}

}

搬家是指:容量不足系统帮我们新建内存并且将原来内存数据拷贝后释放掉。

所以我们可以根据上面的总结:

  • 1)使用push_back插入时,绝大多数时不会引起迭代器失效,但容量不足时(我们没有设定resize时,系统认为不足时需要搬家他也会重新创建)系统帮你搬家就会造成迭代器失效。
    对于单线程没有使用迭代器或者单线程使用了迭代器并未指定大小不需要担心报错。但是如果是多线程下,一个线程只push_back(假设刚好搬家),不操作迭代器,另一个线程不断使用迭代器循环遍历检测vec中的数据,那么push_back后,迭代器失效,造成使用迭代器遍历的线程崩溃。
    解决的办法就是加锁,只有获取到锁后才能push_back,另一个线程则等待,每一次遍历都是单独的是顺序的,这样我每次循环都能通过v.begin去更新迭代器。
  • 2)insert函数的插入更加离谱,只要插入就会造成当前迭代器及其往后迭代器失效(无法索引),每次插入都必须利用返回值更新迭代器。
    所以单线程下使用insert必须更新迭代器,多线程下也要更新迭代器并且必须加锁。
  • 3)对比map,map的insert会造成当前迭代器及其后面迭代器失效吗?这里暂时没研究过。编码时一般只研究删除时会造成当前迭代器失效。且对比上一篇的效率,我们能用map的优先使用map。

2 vector容器的遍历

非常简单,不做详细介绍。

//打印vector容器函数
void PrintVector(vector<int>&v){
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++){
		cout << *it << " ";
	}
	cout << endl;
}

3 vector容器的删除

在讲删除前,先讲一个小细节,当我往vec中push_back7个元素时,发现它的容量为9,即vec的容量不一定是顺序递增的。
插入1个,容量为1:
在这里插入图片描述
插入4个,容量为4:
在这里插入图片描述

而当我插入第五个时,发现容量为6,所以得出容量是编译器按照实际需要帮我们自动指定一个值递增。
在这里插入图片描述
插入第6个,容量不变:
在这里插入图片描述
插入第7个,容量又以2递增:
在这里插入图片描述
所以vector的容量递增是以一定值递增的,且该值会按一定方式变化。

好了,现在我们正式讲vector的删除。

3.1 首先先讲vector的正确删除(erase)方法。
删除前的迭代器指向以下地址:
在这里插入图片描述
删除后迭代器的指向没有改变,仍指向原地址,说明vector元素的删除就是前移操作。
在这里插入图片描述

在删除vector的第二个2前:
在这里插入图片描述
删除后,发现地址仍未改变,完全确定vec的删除是前移操作,不管你删除哪个元素都是前移操作。
在这里插入图片描述

3.2 接着是vector的错误删除(erase)
这里以v.erase(it)错误的方法删除为例。

删除前it的指向地址:
在这里插入图片描述

删除后的it指向地址仍一样,但是接着循环it++就会报错。有人会说,为什么迭代器不是没变吗,为什么又失效了,这种失效与搬家不一样,这种是编译器认为你失效,只要你进行了vec的删除操作,编译器就会认为你迭代器失效,必须更新迭代器,否则报错。
在这里插入图片描述
接着循环就会报错:
在这里插入图片描述

接着给出上面正确的测试代码

void test03() {

	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);

	//正确写法
	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			it = v.erase(it);
			//it = v.erase(it++); //虽然这样写也可以,但是最后你的it还是用返回值替代,所以没必要.

			/*
				注:map的可以使用m.erase(it++)的,因为map的删除类似引用(便于理解),并且map是红黑树,编译器认为它删除当前节点与下一节点无关,
				即下一元素节点位置保持不变,所以map这种写法编译器认为它仍然有效;
				而vertor的erase删除,是基于元素前移的,当删除一个元素后,当前迭代器不变,但是元素已经前移,所以你再it++的话就造成两次自增,
				编译器不允许这样,所以你不能再使用该迭代器
			*/

		}
		else {
			it++;
		}
	}

}

错误的测试代码:

void test03() {

	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);

	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			//it = v.erase(it);
			//it = v.erase(it++); //虽然这样写也可以,但是最后你的it还是用返回值替代,所以没必要.

			/*
				注:map的可以使用m.erase(it++)的,因为map的删除类似引用(便于理解),并且map是红黑树,编译器认为它删除当前节点与下一节点无关,
				即下一元素节点位置保持不变,所以map这种写法编译器认为它仍然有效;
				而vertor的erase删除,是基于元素前移的,当删除一个元素后,当前迭代器不变,但是元素已经前移,所以你再it++的话就造成两次自增,
				编译器不允许这样,所以你不能再使用该迭代器
			*/

			v.erase(it);
		}
		else {
			it++;
		}
	}


}

完整的vec删除代码,包含正确的写法(唯一),和一些错误的写法总结。

void test03() {

	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);

	//正确写法
	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			it = v.erase(it);
			//it = v.erase(it++); //虽然这样写也可以(和上面一模一样,it不会自增两次,具体笔者也不是太懂),但是最后你的it还是用返回值替代,所以没必要.

			/*
				注:map的可以使用m.erase(it++)的,因为map的删除类似引用(便于理解),并且map是红黑树,编译器认为它删除当前节点与下一节点无关,
				即下一元素节点位置保持不变,所以map这种写法编译器认为它仍然有效;
				而vertor的erase删除,是基于元素前移的,当删除一个元素后,当前迭代器不变,但是元素已经前移,所以你再it++的话就造成两次自增,
				编译器不允许这样,所以你不能再使用该迭代器
			*/

		}
		else {
			it++;
		}
	}


	PrintVector(v);



#if 0
	// 1 直接报错
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		if (*it == 2) {
			v.erase(it);	 //error 1
		}
	}

	// 2 不报错,但是进行了两次自增,即删除后更新了一次又自增,这样导致的是以it+2的方式自增,索引时无法找到对应数据
	for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
		if (*it == 2) {
			it = v.erase(it);	 //error 2
		}
	}

	// 3 同样报错
	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			v.erase(it++);   //error 3
			//v.erase(it);   //error 4,虽然逻辑没错,但是编译器不允许这种操作
		}
		else {
			it++;
		}
	}

	// 4 同样可能报错
	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			it = v.erase(it++);
			std::cout << *it << std::endl;//error 5.这里错误是因为继续使用了it。因为更新后,it指向下一元素,当上面的it刚好
										  //是vec的末尾元素,那么指向下一元素后,it指向了end,所以这是不安全的,也是一个错误。
										  //去掉这个打印就是安全的。
		}
		else {
			it++;
		}
	}
	
#endif

}

这里总结一下vector的erase删除:

  • 1)正确的写法只有一个,就是必须更新返回值,并且it++不能写在for循环的参3,否则自增了两次,索引寻找数据时就会有问题。
  • 2)删除报错都是基于没有更新迭代器的。单线程很容易理解,多线程的话,报错也是因为迭代器没有更新,例如一个线程删除并且更新了迭代器,另一个线程使用迭代器在遍历全局vec,但是由于只是在删除线程更新了,遍历的没有更新,所以就会出错。解决方法上面讲过就是加锁,确保每次循环遍历都是独立的。
  • 3)针对上面错误4强调,调用erase并且更新后的it,在if中不能再使用,必须让其跳出if才能再使用,即下一次for循环及之后才能使用。否则可能存在it指向end的情况,造成访问越界。具体可以参考我写的线程池的ReadMe的第3点。ThreadPool

3.3 以上的删除都是针对于erase函数的,下面来测试测试pop_back的删除对迭代器是否有影响,即是否失效:

3.3.1 pop_back删除后迭代器是否失效-测试1
1)先获取迭代器首地址。

void test04() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto it = v.begin(); it != v.end(); it++) {
		if (*it == 2) {
			v.pop_back();
		}
	}
}

在这里插入图片描述
2)执行pop_back函数后,可以看到,迭代器首地址是一样的,并且继续运行程序是正常。
在这里插入图片描述

3.3.2 错误的循环调用pop_back删除后导致迭代器失效-测试2
pop_back的测试代码:

void test04() {

	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(2);
	v.push_back(4);
	v.push_back(5);

	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			v.pop_back();//一般很少遍历调用pop_back
		}
		else {
			it++;
		}
	}
}

1)调用pop_back前的迭代器,指向有效值。
在这里插入图片描述
2)调用pop_back删除之后的迭代器,和erase一样,也没有改变。
在这里插入图片描述
在这里插入图片描述

在往下执行发生错误,注意并非是下一次循环发生错误,而是发生在删除完*it=2时,在访问该第一个2的地址发生非法访问。与erase区别一下,erase删除后第一次使用就会报错。
下面分析一下这种情况:
当我们插入1,2,2,2,2,4,5后,满足if(*it==2),此时it一直指向下标1的2,所以当我们不断pop,直至数组没有2即剩余1时,由于2的地址已经被删除,所以在访问就会报错,这就是下面代码pop报错的原因。
通过上面两个pop_back的测试,所以这里可以总结一下pop_back:

  • 1)pop的删除只要不写成测试2的错误写法,单线程和多线程都是安全的(多线程可以参考deque那篇文章的测试),但是由于多线程操作vector的话,必须加锁。

3.4 总结vec的erase和pop_back的删除
即将上面的两个总结抄下来,方便对比。

erase:

  • 1)正确的写法只有一个,就是必须更新返回值,并且it++不能写在for循环的参3,否则自增了两次,索引寻找数据时就会有问题。
  • 2)删除报错都是基于没有更新迭代器的。单线程很容易理解,多线程的话,报错也是因为迭代器没有更新,例如一个线程删除并且更新了迭代器,另一个线程使用迭代器在遍历全局vec,但是由于只是在删除线程更新了,遍历的没有更新,所以就会出错。解决方法上面讲过就是加锁,确保每次循环遍历都是独立的。

pop_back:

  • 1)pop的删除只要不写成测试2的错误写法,单线程和多线程都是安全的(多线程可以参考deque那篇文章的测试),但是由于多线程操作vector的话,必须加锁。

对比以上两个总结可以发现,凡是对vec的插入删除操作,vec的插入都是不安全的,单线程push_back时最好不要指定大小,insert必须必须更新迭代器,多线程必须更新迭代器并且加锁。
这一点总结非常重要,是我们对上面所有例子的大总结。

4 Vector容器的查找

由于vec不像map那样提供find和count查找,所以我们只能通过遍历去查找了。所以说效率比map差很多并且容易程序崩溃。

	for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
		if (*it == 2) {
			//逻辑
		}
		else {
			it++;
		}
	}

5 Vector容器的其它函数

这些函数比较少用,包括构造初始化等号赋值等等,但是也要知道,并且巧用swap来收缩内存和巧用reverse预留空间也是非常常用的在某些情况。
1)构造与赋值

//构造与赋值
void test02() {
	//1 默认构造
	vector<int> vi1;
	for (int i = 0; i < 5; i++) {
		vi1.push_back(i);
	}

	//2 迭代器区间构造
	vector<int> vi2(vi1.begin(), vi1.end());

	//3 个数构造,往vector插入5个1
	vector<int> vi3(5, 1);

	//4 拷贝构造
	vector<int> vi4(vi3);

	PrintVector(vi1);
	PrintVector(vi2);
	PrintVector(vi3);
	PrintVector(vi4);

	//5 等号运算符(赋值)
	vector<int> vi5;
	vi5 = vi2;

	//6 默认构造后,调用分配函数assign赋值,类似2的作用
	vector<int> vi6;
	vi6.assign(vi2.begin(), vi2.end());
	
}

2)size()与empty()–resize()函数变大变小的区别–reserve()函数

//size()与empty()--resize()函数变大变小的区别--reserve()函数
void test05() {

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

	//1 获取数组中的元素个数size()
	cout << vi1.size() << endl;

	//2 判断数组是否为空empty()
	if (vi1.empty() == true) {
		cout << "vector容器为空" << endl;
	}
	else {
		cout << "容器不为空" << endl;
	}

	PrintVector(vi1);

	//3 resize(),该函数改变数组的大小,与容量无关,并且若一开始调用,就会占用数组的空间
	//例如vector<int> v; v.resize(6);此时数组已有6个元素,再push则变成7个.
	cout << "=======" << endl;
	vi1.resize(3);//容量不变,数组大小改变,后面的被删除.(数组容量与大小是不一样的)
	PrintVector(vi1);

	vi1.resize(5, 1);//数组大小被重置为5,且多余的默认以1初始化
	PrintVector(vi1);

	//reserve()函数,该函数改变的是容量,与数组大小无关,原本是多少就是多少.
	cout << "=======" << endl;
	vector<int> vi2;
	vi2.reserve(20);
	vi2.push_back(1);
	PrintVector(vi2);

}

这里给出结果:
在这里插入图片描述

3)巧用swap来收缩内存

//巧用swap来收缩内存
void test06() {
	vector<int> v;
	for (int i = 0; i < 100000; i++){
		v.push_back(i);
	}
	cout << "v的容量: " << v.capacity() << endl;
	cout << "v的大小: " << v.size() << endl;

	v.resize(3);//只对数组大小起作用,容量还是很大.

	cout << "v的容量: " << v.capacity() << endl;
	cout << "v的大小: " << v.size() << endl;

	vector<int>().swap(v);  //与匿名对象交换内存,匿名对象生命期结束自动清空内存

	cout << "v的容量: " << v.capacity() << endl;
	cout << "v的大小: " << v.size() << endl;
}

在这里插入图片描述

4)巧用reverse预留空间

//巧用reverse预留空间
void test07() {

	vector<int> vi1;

	//若你提前知道你的vector容器装多少东西,则可以预留,避免多次申请空间
	vi1.reserve(10000);//预留的为容量

	int *str = NULL;
	int num = 0;

	//测试,不断push10000的内容进去,若首地址相同则证明是同一片内存,不等说明系统重新开辟空间,记录开辟的num次数
	//一般不会是1,系统一般要求容量比存储的内容大一点
	for (int i = 0; i < 10000; i++) {
		vi1.push_back(i);

		if (str != &vi1[0]) {
			str = &vi1[0];
			num++;
		}

	}
	cout << num << endl;
}

结果可以看到,虽然容量刚好与数组元素个数一样,但是系统一般在认为你容量快不足时帮你重新申请内存。这里系统帮你申请了一次,有些操作系统可能2-5次不定。
在这里插入图片描述

6 例子—>多线程下测试vector

下面模拟一个业务场景,全局vector中管理着某些类对象,子线程隔断时间不断循环检测这些对象是否有状态异常,而当客户有需求时需要进行请求(影响vec的删除和添加),我们需要如何处理,以确保程序不发生异常?

思路:很简单,上面已经讲了,多线程下vec的操作在本线程内更新迭代器,并且必须加锁,并且这把锁是锁住一个循环,而非简单锁住循环中的一次。
虽然有人说效率慢,一般你循环几万次还不到一秒呢,对于不追求效率的这样做是最安全的,否则锁住一次都是有机会报错的。

先看代码,子线程在循环遍历检测,主线程也在遍历寻找合适的对象进行操作。

1)以下为正确的代码,主子线程对每个循环都分别加锁操作。

//我要存放在vec的类
class A {
public:
	A(int i) {
		m_i = i;
	}
	int GetInt() {
		return m_i;
	}
private:
	struct S1 {
		int a;
		int b;
		double c;
	};

	int m_i;
};


//全局vector
std::vector<A> v;
std::mutex myMutex;//不能和mutex重名,我开始忘写了害我找了很久的错

//子线程回调,模拟循环遍历
DWORD WINAPI ThreadCBHleStreamQuestion(LPVOID lpThreadParameter) {

	while (1) {
		//子线程睡眠是防止占用CPU过高,一般30s,这里为了测试睡眠2s
		std::this_thread::sleep_for(std::chrono::seconds(2));
		{
			std::lock_guard<std::mutex> lock(myMutex);
			for (auto it = v.begin(); it != v.end();) {
				if ((*it).GetInt() == 3) {
					//std::lock_guard<std::mutex> lock(mutex);//错误写法,只锁住本次循环
					it = v.erase(it);//更新当前迭代器
					printf("Hello,I am 3 erase.\n");

					v.push_back(A(3));
					printf("Hello,I am 3 push_back.\n");
					
					std::cout << sizeof(A(3)) << std::endl;

					//删除更新迭代器,但push没有,所以应该退出,防止内存搬家导致迭代器失效
					break;
				}
				else {
					it++;
				}
			}
		}

	}
}

//主线程
int main() {

	//放元素进去,
	int i = 0;
	for (i; i < 10; i++) {
		v.push_back(A(i));
	}

	//创建子线程
	HANDLE thread = NULL;
	thread = CreateThread(NULL, 0, ThreadCBHleStreamQuestion, NULL, 0, NULL);

	//睡眠3s,让子线程先执行
	std::this_thread::sleep_for(std::chrono::seconds(3));

	//模拟子线程遍历时,有客户端请求删除或者添加操作
	while (1) {
		{//该括号是为了guard对象利用生命期自动释放锁,防止出错而没有释放锁导致出现死锁

			std::lock_guard<std::mutex> lock(myMutex);
			for (auto it = v.begin(); it != v.end();) {
				if ((*it).GetInt() == 2) {
					//std::lock_guard<std::mutex> lock(mutex);
					it = v.erase(it);
					printf("Hello,I am 2.\n");
				}
				else if (i == 10) {
					//std::lock_guard<std::mutex> lock(mutex);
					v.push_back(A(11));
					printf("Hello,I am 5.\n");
					i = 11;
				}
				else {
					it++;
				}
			}

		}
	}

	return 0;
}

2)错误代码:
错误代码是要么在主线程只锁住循环的一次要么子线程只锁住循环的一次或者两个线程均锁住循环的一次,这是非常危险的操作,因为你测试运行代码时可能没错,但是用到实际就容易报错,因为发生错误的概率是不定的,出现的时候就找死人了。

下面以子线程只锁住循环的一次为例,你也可以主线程,记住多执行多几次,因为一两次可能不会报错,代码就是将上面正确写法的子线程的锁注释,换成循环内的锁,结果如下,循环多几次必定报错:
在这里插入图片描述

好了,Vector的用法和错误点已经例举完毕,非常详细了,够你在平时开发遇到的问题解决了。

7 总结vector容器

7.1 插入总结

  • 1)使用push_back插入时,绝大多数时不会引起迭代器失效,但容量不足时(我们没有设定resize时,系统认为不足时需要搬家他也会重新创建)系统帮你搬家就会造成迭代器失效。
    对于单线程没有使用迭代器或者单线程使用了迭代器并未指定大小不需要担心报错。但是如果是多线程下,一个线程只push_back(假设刚好搬家),不操作迭代器,另一个线程不断使用迭代器循环遍历检测vec中的数据,那么push_back后,迭代器失效,造成使用迭代器遍历的线程崩溃。
    解决的办法就是加锁,只有获取到锁后才能push_back,另一个线程则等待,每一次遍历都是单独的是顺序的,这样我每次循环都能通过v.begin去更新迭代器。
  • 2)insert函数的插入更加离谱,只要插入就会造成当前迭代器及其往后迭代器失效(无法索引),每次插入都必须利用返回值更新迭代器。
    所以单线程下使用insert必须更新迭代器,多线程下也要更新迭代器并且必须加锁。
  • 3)对比map,map的insert会造成当前迭代器及其后面迭代器失效吗?这里暂时没研究过。编码时一般只研究删除时会造成当前迭代器失效。且对比上一篇的效率,我们能用map的优先使用map。

7.2 删除总结
erase:

  • 1)正确的写法只有一个,就是必须更新返回值,并且it++不能写在for循环的参3,否则自增了两次,索引寻找数据时就会有问题。
  • 2)删除报错都是基于没有更新迭代器的。单线程很容易理解,多线程的话,报错也是因为迭代器没有更新,例如一个线程删除并且更新了迭代器,另一个线程使用迭代器在遍历全局vec,但是由于只是在删除线程更新了,遍历的没有更新,所以就会出错。解决方法上面讲过就是加锁,确保每次循环遍历都是独立的。

pop_back:

  • 1)pop的删除只要不写成测试2的错误写法,单线程和多线程都是安全的(多线程可以参考deque那篇文章的测试),但是由于多线程操作vector的话,必须加锁。

对比以上两个总结可以发现,凡是对vec的插入删除操作,vec的插入都存在不安全性,单线程push_back时最好不要指定大小,insert必须必须更新迭代器,多线程必须更新迭代器并且加锁。
这一点总结非常重要,是我们对上面所有例子的大总结。实际上vector容器和deque容器的成员函数使用迭代器时,注意事项可以说是完全一样的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值