当前位置: 首页 > news >正文

【C++篇】揭秘STL vector:高效动态数组的深度解析(从使用到模拟实现)

💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习!
👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对C++感兴趣的朋友


文章目录

    • 前言
      • 一、vector的介绍
      • 二、vector的使用
        • 1. constructor
        • 2. iterator
        • 3. capacity
        • 4. Modify 增删查改
      • 三、深度剖析vector底层(含模拟实现)(重点)
        • 1. 成员变量设置
        • 2. reserve扩容器
          • 使用memcpy导致的隐藏浅拷贝问题
        • 3. insert插入元素
          • 迭代器失效问题1
        • 4. erase删除元素
          • 迭代器失效问题2
        • 5. resize控制大小
        • 6.构造函数
          • 因参数匹配导致的调用构造函数错乱问题
        • 7. 拷贝构造
        • 8. 赋值重载
        • 源码
      • 四、vector实现动态二维数组


前言

本文核心内容为vector的介绍及使用和对vector的模拟实现。本文包含很多易错点,都是我在学习vector过程中踩过的坑。因为自己淋过雨,希望可以为你们撑一把伞!共勉。


一、vector的介绍

📜vector的文档介绍(cplusplus)

在这里插入图片描述
vector使用了模板,以满足任意类型数据的适用。
另外还使用到了内存池,以提高效率。

  1. vector是表示可变大小数组的序列容器
  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
  5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
  6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。

二、vector的使用

这里主要以介绍vector常用接口的方式进行讲解,由于vector的接口与过去所学的string的接口雷同,许多接口直接平移使用即可。
使用vector需包头文件:#include<vector>
使用格式:vector<T> + [对象名]

1. constructor
构造函数声明接口说明
vector()(重点)无参构造
vector(size_type n, const value_type& val = value_type())构造并初始化n个val
vector (const vector& x)(重点)拷贝构造
vector (InputIterator first, InputIterator last)使用迭代器进行初始化构造
vector& operator=(vector v)赋值运算符重载

调用演示:

//无参构造
vector<int> v;//构造并初始化n个val
vector<int> v1(10, 6);
vector<string> v2(10, "1111111");//使用迭代器进行初始化构造
string str("qwertyuiop");
vector<char> v3(str.begin(), str.end())//拷贝构造
vector<int> v4(v1);//赋值运算符重载
v = v1;

疑点:
这里你可能看不懂构造并初始化n个val的构造函数的第二个参数:const value_type& val = value_type()
缺省值是一个类型后面跟个(),这是啥?为什么不用0?
这里是为了解决val是自定义类型时的情况。
当val是string类型是,val还能为0吗?当然不能!
其实,这里这样用在语法上看是不合理的,但在C++引入模板的时候,包容了这个写法,以兼容模板的功能。


2. iterator

vector的迭代器是一个原生指针

iterator的使用接口说明
begin + end(重点)获取第一个数据位置的iterator/const_iterator; 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator;获取第一个数据前一个位置的reverse_iterator

在这里插入图片描述


3. capacity
容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize(重点)改变vector的size
reserve (重点)改变vector的capacity
  1. capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。因此不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
  2. reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
  3. resize在开空间的同时还会进行初始化,影响size。

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

这里在使用insert和erase时,可能会发生迭代器失效的问题,导致出错。在后文模拟实现这两个接口时会进行讲解。


三、深度剖析vector底层(含模拟实现)(重点)

当我们在模拟实现vector的过程中会出现很多问题,这些问题都十分重要!都是重点!
罗列一下:

  • 迭代器失效
  • 使用memcpy导致的隐藏浅拷贝
  • 因参数匹配导致的调用构造函数错乱

所有接口我就不一一实现了,我会讲解实现:构造、拷贝构造、赋值重载、reserve、resize、insert、erase。

1. 成员变量设置

对标stl30学习,成员变量包括:

typedef T* iterator;
typedef const T* const_iterator;iterator _start = nullptr; // 指向数据块的开始
iterator _finish = nullptr; // 指向有效数据的尾
iterator _endOfStorage = nullptr; // 指向存储容量的尾
2. reserve扩容器

功能:当n>capacity()时,进行扩容;反之,不作处理。

STL中的做法是:分配一个容量更大的新的数组,然后将全部元素迁移到这个数组中,再清理原数组的空间,完成扩容。

实现(bug):

void reserve(size_t n)
{if (n > capacity()){size_t sz = size();//创建一个容量更大的新的数组T* tmp = new T[n];if (_start){//元素迁移memcpy(tmp, _start, sizeof(T) * size());//清理原数组delete[] _start;}_start = tmp;_finish = _start + sz;_endOfStorage = _start + n;}
}
使用memcpy导致的隐藏浅拷贝问题

在进行元素迁移时,T为内置类型的时候,使用memcpy是没有问题的。
但T为自定义类型时,就不行了。

vector<string>为例:

memcpy会将原数组的元素一一拷贝到新的扩容数组中,并没有调用拷贝构造,因此发生了浅拷贝
在这里插入图片描述
所以在接下来调用delete时,原数组和新数组的元素都会被清理,后序再进行扩容操作时,会导致一个对象被析构两次,最终程序崩溃

在这里插入图片描述
总结问题
vector需要的是深拷贝,但是当存储的数据为自定义类型时,使用memcpy会导致元素对象浅拷贝。

解决方案:

调用元素对象的赋值重载来进行对象的深拷贝。

for (int i = 0; i < size(); ++i)
{tmp[i] = _start[i];
}

reserve正确代码:

void reserve(size_t n)
{if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start){//自定义类型会发生浅拷贝//memcpy(tmp, _start, sizeof(T) * size());//用赋值重载来避免自定义类型发生浅拷贝问题for (int i = 0; i < size(); ++i){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_endOfStorage = _start + n;}
}

3. insert插入元素

功能:在pos位置插入一个元素,并返回pos位置(pos是一个迭代器)
实现思路:

  1. 检查容量,不足就扩
  2. 挪动数据:
    • 将pos位及后面的元素向后挪动一个位置
    • 挪动方法:从后向前依次向后一位覆盖数据。
  3. 插入数据

实现(bug):

iterator insert(iterator pos, const T& x)
{//检查pos是否合法assert(pos <= _finish && pos >= _start);//检查容量,不足就扩if (_finish == _endOfStorage){int newcapacity = capacity() == 0 ? 1 : capacity() * 2;reserve(newcapacity);}//挪数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}// 插入数据*pos = x;++_finish;return pos;
}
迭代器失效问题1

我们仔细走读一下代码(包括reserve),可以发现:pos成野指针了!

我们每次在扩容时,会使得数据更换空间,此时pos迭代器指向的空间地址是无效地址(pos成了野指针(pos迭代器失效))。一旦我们再使用这个迭代器,就会出错。

在这里:扩容使得迭代器失效,后移使用了失效迭代器

解决问题:

更新迭代器:每次扩容时,将pos也同时指向新数组的对应位置。

正确代码:

iterator insert(iterator pos, const T& x)
{assert(pos <= _finish && pos >= _start);if (_finish == _endOfStorage){int len = pos - _start;int newcapacity = capacity() == 0 ? 1 : capacity() * 2;reserve(newcapacity);//解决迭代器失效问题pos = _start + len;//如果发生扩容,会使得数据更换空间,此时迭代器指向的空间地址是无效地址(pos变为野指针)//因此,要更新迭代器pos。}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;return pos;
}

4. erase删除元素

功能:删除pos位置的元素,并返回pos(pos是迭代器)

实现思路:

  1. 判断数组是否为空,删除空数组元素会出错
  2. 挪动数据:从pos+1位置开始,数据向前覆盖一位。

代码实现:

 iterator erase(iterator pos){assert(pos < _finish && pos >= _start);assert(size() > 0);iterator end = pos;while (end < _finish - 1){*end = *(end + 1);++end;}--_finish;return pos;}
迭代器失效问题2

都没有发生扩容呀?难道pos也能失效???

在我们删除pos位置的元素后,pos位会由其他元素代替。
如果pos位及其之后的所有元素都被删除了,pos就会指向一个无效的位置,导致迭代器失效。

例如:2,3,5,7,9,pos指向第三个位置,我们要对第3个位置删除3次:
在这里插入图片描述
这里我们可以发现:pos迭代器是有失效的风险的,也不是一定会失效。

但是:在vs上,编译器认为vector的insert和erase后的迭代器就是失效的,不能再去访问这个迭代器,否则报错

然而,g++就有所不同,它就是我们刚刚的结论:不失效不报错。但是我们也不能这样去做,否则代码你的代码没有可移植性。


5. resize控制大小
void resize(size_t n, const T& value = T());

功能:改变数组大小
实现思路:

  • n <= size():将数组大小缩小至n
  • n > size()
    • n < capacity :将数组大小扩大至n,增加的元素设置为value
    • n > capacity :扩容,并将数组大小扩大至n,增加的元素设置为value

代码实现:

 void resize(size_t n, const T& value = T()){if (n > size()){reserve(n);while (_finish != _start + n){*_finish = value;++_finish;}}else{_finish = _start + n;}}

6.构造函数

我们来实现常用的构造:
在这里插入图片描述
实现(bug):

//构造并初始化n个val
vector(size_t n, const T& val = T())
{//直接复用resize的功能可以完美解决resize(n, val);
}// 迭代器构造
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}//无参构造
vector()
{}

注意:要在定义成员变量处设置缺省值,否则就需要用初始化列表来进行初始化。

private:iterator _start = nullptr; // 指向数据块的开始iterator _finish = nullptr; // 指向有效数据的尾iterator _endOfStorage = nullptr; // 指向存储容量的尾
因参数匹配导致的调用构造函数错乱问题

我们试试这样实例化对象:vector<int> v(10, 10)
运行代码,发现报错了,为什么呢?
调试一下,可以发现并没有去调用vector(size_t n, const T& val = T()),而是去调用了迭代器构造函数。
原来,我们的两个参数都是int型,恰好符合迭代器的两个相同参数,而vector(size_t n, const T& val = T())第一个参数类型是size_t,符合度没有迭代器构造函数高才导致这个结果的。

解决问题:

再重载一个vector(int n, const T& val = T())构造函数,以防止调用迭代器构造。

//构造1
vector(size_t n, const T& val = T())
{resize(n, val);
}//构造2
//避免vector<int> v(10, 1) 调用3
//亦或者用vector<int> v(10u, 1),使其完美匹配1
vector(int n, const T& val = T())
{resize(n, val);
}// 构造3
// 迭代器构造
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}vector()
{}

7. 拷贝构造

有两个实现思路:

  1. 常规思路:开辟一个新的空间,将所有元素深拷贝至整个空间即可
  2. 创建一个空对象,将所有元素依次尾插至目标数组即可

代码实现:


//拷贝构造v1
vector(const vector<T>& v)
{_start = new T[v.capacity()];int sz = v.size();//自定义类型会发生浅拷贝//memcpy(_start, v, sizeof(T) * sz);//用赋值重载来避免自定义类型发生浅拷贝问题for (int i = 0; i < sz; ++i){_start[i] = v[i];}_finish = _start + sz;_endOfStorage = _start + v.capacity();
}//拷贝构造v2
vector(const vector<T>& v):_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{reserve(v.capacity());for (auto e : v){push_back(e);}
}

8. 赋值重载

实现思路:利用传值会进行拷贝构造的特性,再用swap完成实现。

代码实现:

//赋值重载
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}

源码

这是我模拟实现vector的源码,仅供参考。


四、vector实现动态二维数组

vector嵌套一个vector就可以实现一个二维数组了。

vector<vector<int>> vv(n); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:
在这里插入图片描述
vv中元素填充完成之后,如下图所示:

在这里插入图片描述
在使用时和普通二维数组差不多,也是vv[][]


相关文章:

  • 从技术层⾯来说深度SEO优化的⽅式有哪些?
  • Java 中Supplier延迟生成值的原因
  • 2025-5-19Vue3快速上手
  • java.lang.UnsupportedOperationException: null
  • 【java第18集】java引用数据类型详解
  • 进程退出 和 僵尸进程、孤儿进程
  • Linux错误处理集合 GLIBCXX_3.4.25‘ not found和 安装glibc-2.28和Error: rpmdb open failed
  • JQuery 禁止页面滚动(防止页面抖动)
  • VS中将控制台项目编程改为WINDOWS桌面程序
  • ai决策平台:AnKo如何推动引领智能化未来?
  • 【PhysUnits】4.5 负数类型(Neg<P>)算术运算(negative.rs)
  • 幻觉、偏见与知识边界——认识并驾驭AI的固有缺陷
  • 交叉引用、多个参考文献插入、跨文献插入word/wps中之【插入[1,3,4]、跨文献插入】
  • 千问大模型部署
  • 2.1.1(数据处理规范)
  • Google设置app-ads.txt
  • Linux串口绑定
  • Chromium 浏览器核心生命周期剖析:从 BrowserProcess 全局管理到 Browser 窗口实例
  • IOS 创建多环境Target,配置多环境
  • Windows 安装显卡驱动
  • 欧洲观察室|“美国优先”使欧盟对华政策面临地缘经济困境
  • 上海文化馆服务宣传周启动,为市民提供近2000项活动
  • 对话作家吉井忍:“滚石”般的生活,让我看到多种人生可能
  • 吴双评《发展法学》|穷国致富的钥匙:制度,还是产业活动?
  • 国家统计局:4月份各线城市商品住宅销售价格环比持平或略降
  • 雅典卫城上空现“巨鞋”形状无人机群,希腊下令彻查