C++之vector容器

欢迎来到Deven的代码之路!

目录

1.vector的介绍及使用

1.1vector的介绍

1.2vector的使用

[1.2.1 vector的定义](#1.2.1 vector的定义)

[1.2.2 vector iterator 的使用](#1.2.2 vector iterator 的使用)

[1.2.3 vector 空间增长问题](#1.2.3 vector 空间增长问题)

[1.2.4 vector 增删改查](#1.2.4 vector 增删改查)

[1.2.5 vector迭代器失效问题](#1.2.5 vector迭代器失效问题)

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

2.指定位置元素的删除操作--erase

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

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

2.vector深度剖析及模拟实现

2.1使用memcpy拷贝问题

[2.2 动态二维数组理解](#2.2 动态二维数组理解)


1.vector的介绍及使用

1.1vector的介绍

vector的文档介绍

使用STL的三个境界:能用,明理,能扩展

1.2vector的使用

下面列出哪些接口是重点掌握的。

1.2.1 vector的定义

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

1.2.2 vector iterator 的使用

|-------------|--------------------------------------------------------------------------|
| iterator的使用 | 接口说明 |
| begin + end | 获取第一个数据位置的iterator/const_iterator,获取最后一个数据的下一个位置的iterator/const_iterator |
| rbegin+rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |

1.2.3 vector 空间增长问题

|----------|-------------------|
| 容量空间 | 接口说明 |
| size | 获取数据个数 |
| capacity | 获取容量大小 |
| empty | 判断是否为空 |
| resize | 改变vector的size |
| reserve | 改变vector的capacity |

  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
  • resize在开空间的同时还会进行初始化,影响size。
cpp 复制代码
// 测试vector的默认扩容机制
void TestVectorExpand()
{
     size_t sz;
     vector<int> v;
     sz = v.capacity();
     cout << "making v grow:\n";
     for (int i = 0; i < 100; ++i)
     {
         v.push_back(i);
         if (sz != v.capacity())
         {
             sz = v.capacity();
             cout << "capacity changed: " << sz << '\n';
         }
     }
}
vs:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141

g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128
cpp 复制代码
void TestVectorExpandOP()
{
     vector<int> v;
     size_t sz=v.capacity();
     v.reserve(100);
     for(int i=0;i<100;++i)
     {
         v.push_back(i);
         if(sz != v.capacity())
         {
            sz=v.capacity();
            cout<<"capacity changed:"<<sz<<'\n';
         }
     }
}

1.2.4 vector 增删改查

|--------------|-------------------------------|
| vector增删改查 | 接口说明 |
| push_back | 尾插 |
| pop_back | 尾删 |
| find | 查找(注意这个是算法模块实现,不是vector的成员接口) |
| insert | 在position之前插入val |
| erase | 删除position位置的数据 |
| swap | 交换两个vector的数据空间 |
| operator[] | 数据访问 |

1.2.5 vector迭代器失效问题

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

对于vector可能会导致其迭代器失效的操作有:

1.会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。
cpp 复制代码
#include <iostream>
#include <vector>
using namesapce std;

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);

    /*出错原因:以上操作,都有可能会导致vector扩容,也就是说vector底层原来旧空间被释放掉,而在打印时,it还使用的是释放之前的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃。
      解决方式:在以上操作完成之后,若想要继续通过迭代器操作vector中的元素,只需给it重新赋值即可。*/
    while(it != v.end())
    {
        cout<<*it<<" ";
        ++it;
    }
    cout<<endl;
    return 0;
}
2.指定位置元素的删除操作--erase
cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int a[]={1,2,3,4};
    vector<int> v(a,a+sizeof(a)/sizeof(int));

    //查找3所在位置的iterator
    vector<int>::iterator pos=find(v.begin(),v.end(),3);

    //删除pos位置的数据,导致pos迭代器失效
    v.erase(pos);
    cout<<*pos<<endl; // 此处会导致非法访问
    return 0;
}

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

以下代码功能是删除vector中所有的偶数,哪个代码是正确的?

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

int main()
{
    vector<int> v{1,2,3,4};
    auto it=v.begin();
    while(it != v.end())
    {
        if(*it % 2==0)
           v.erase(it);

         ++it;
    }
    return 0;
}


int main()
{
    vector<int> v{1,2,3,4};
    auto it=v.begin();
    while(it != v.end())
    {
         if(*it % 2==0)
            it=v.erase(it);
         else
            ++it;
    }

    return 0;
}
3.注意: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


// 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.与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效
cpp 复制代码
#include <string>
void Test()
{
    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;
    }
}

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

2.vector深度剖析及模拟实现

2.1使用memcpy拷贝问题

假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

cpp 复制代码
int main()
{
   deven::vector<deven::string> v;
   v.push_back("1111");
   v.push_back("2222");
   v.push_back("3333");
   return 0;
}

问题分析:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是POD类型(普通旧数据类型),memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

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

2.2 动态二维数组理解

cpp 复制代码
//以杨辉三角的前n行为例,假设n为5
void test(size_t n)
{
    //使用vector定义二维数组vv,vv中的每个元素都是vector<int>
    deven::vector<deven::vector<int>> vv(n);

    //将二维数组每一行中的vector<int>中的元素全部设置为1
    for(size_t i=0;i<n;++i)
        vv[i].resize(i+1,1);
    
    //给出第一列和对角线的所有元素赋值
    for(int i=2;i<n;++i)
    {
        for(int j=1;j<i;++j)
        {
            vv[i][j]=vv[i-1][j] + vv[i-1][j-1];
        }
    }
}

deven::vector<deven::vector<int>> vv(n);构造一个vv动态二维数组,vv中共有n个元素,每个元素都是vector类型的,如果n为5时如下图所示:

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

相关推荐
猷咪10 分钟前
C++基础
开发语言·c++
IT·小灰灰12 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧13 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q14 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳014 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾14 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB17 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683618 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计32 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
蒹葭玉树43 分钟前
【C++上岸】C++常见面试题目--操作系统篇(第二十八期)
linux·c++·面试