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的插入是否会使迭代器改变失效:
以上两种情况的测试代码为:
//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的测试代码:
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的迭代器后面插入
}
}
}
搬家是指:容量不足系统帮我们新建内存并且将原来内存数据拷贝后释放掉。
所以我们可以根据上面的总结:
非常简单,不做详细介绍。
//打印vector容器函数
void PrintVector(vector<int>&v){
for (vector<int>::iterator it = v.begin(); it != v.end(); it++){
cout << *it << " ";
}
cout << endl;
}
在讲删除前,先讲一个小细节,当我往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++就会报错。有人会说,为什么迭代器不是没变吗,为什么又失效了,这种失效与搬家不一样,这种是编译器认为你失效,只要你进行了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删除:
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:
3.4 总结vec的erase和pop_back的删除
即将上面的两个总结抄下来,方便对比。
erase:
pop_back:
对比以上两个总结可以发现,凡是对vec的插入删除操作,vec的插入都是不安全的,单线程push_back时最好不要指定大小,insert必须必须更新迭代器,多线程必须更新迭代器并且加锁。
这一点总结非常重要,是我们对上面所有例子的大总结。
由于vec不像map那样提供find和count查找,所以我们只能通过遍历去查找了。所以说效率比map差很多并且容易程序崩溃。
for (vector<int>::iterator it = v.begin(); it != v.end(); ) {
if (*it == 2) {
//逻辑
}
else {
it++;
}
}
这些函数比较少用,包括构造初始化等号赋值等等,但是也要知道,并且巧用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 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次不定。
下面模拟一个业务场景,全局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 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 lock(mutex);
it = v.erase(it);
printf("Hello,I am 2.\n");
}
else if (i == 10) {
//std::lock_guard lock(mutex);
v.push_back(A(11));
printf("Hello,I am 5.\n");
i = 11;
}
else {
it++;
}
}
}
}
return 0;
}
2)错误代码:
错误代码是要么在主线程只锁住循环的一次要么子线程只锁住循环的一次或者两个线程均锁住循环的一次,这是非常危险的操作,因为你测试运行代码时可能没错,但是用到实际就容易报错,因为发生错误的概率是不定的,出现的时候就找死人了。
下面以子线程只锁住循环的一次为例,你也可以主线程,记住多执行多几次,因为一两次可能不会报错,代码就是将上面正确写法的子线程的锁注释,换成循环内的锁,结果如下,循环多几次必定报错:
好了,Vector的用法和错误点已经例举完毕,非常详细了,够你在平时开发遇到的问题解决了。
7.1 插入总结
7.2 删除总结
erase:
pop_back:
对比以上两个总结可以发现,凡是对vec的插入删除操作,vec的插入都存在不安全性,单线程push_back时最好不要指定大小,insert必须必须更新迭代器,多线程必须更新迭代器并且加锁。
这一点总结非常重要,是我们对上面所有例子的大总结。实际上vector容器和deque容器的成员函数使用迭代器时,注意事项可以说是完全一样的。