【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题

简介: 【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题

前言:在C++的STL(Standard Template Library)库中,vector容器无疑是最常用且功能强大的数据结构之一。它提供了动态数组的功能,允许我们在运行时动态地增加或减少元素。然而,随着我们对vector的深入使用,一些潜在的问题也逐渐浮现,其中最为常见和棘手的就是迭代器失效以及拷贝问题 (关于初始insert和erase的模拟实现在本篇末尾)


注意:我们使用的函数是上一篇模拟实现的函数

📒1. 迭代器失效

迭代器失效是指在使用迭代器遍历或操作vector容器时,由于某些操作导致迭代器失效,无法再正确引用容器中的元素。 这种情况往往发生在vector容器进行扩容、插入或删除元素等操作时。迭代器失效可能导致程序出现未定义行为,甚至崩溃。

因此:深入理解vector迭代器失效的原因和场景,对于编写健壮、可靠的C++代码至关重要。


🌈插入时失效

代码示例:(插入)

void test_vector()
{
  vector<int> v1; // 创建一个vector插入4个元素
  v1.push_back(1);
  v1.push_back(2);
  v1.push_back(3);
  v1.push_back(4);
  vector<int>::iterator it = find(v1.begin(), v1.end(), 1);
  v1.insert(it, 2); // 然后我们再来插入两个元素
  v1.insert(it, 3); 
  for (auto e : v1)
  {
    cout << e << " ";
  }
  cout << endl;
}

哎呀,怎么程序出错了?

扩容前:迭代器pos在_start和_finish之间

扩容后:start和finish的地址改变,pos不再指向vector区域的位置

迭代器失效: 迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间


🌞删除时失效

erase也会造成迭代器失效

代码示例:(删除)

void test_vector()
{
  vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  v.push_back(5);
  v.push_back(6);
  auto it = v.begin();
  while (it != v.end())
  {
    if (*it % 2 == 0) v.erase(it);
    ++it;
  }
}

此段代码依然会出现错误,我们可以画图来理解:

erase删除元素后,会进行数据的挪动,我们自己也对迭代器进行了++,导致最后it指向了vector有效范围之外

注意:在vs中,使用erase函数,因为vs对迭代器进行了封装,编译器自动认为此位置迭代器失效


📕2. 解决迭代器失效

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


🍂在插入时失效

这种情景是因为在插入一次元素时,进行了扩容,导致pos位置不对,因此我们只需要不用当前pos迭代器,而是将pos指向进行更新,但是这样做依然解决不了迭代器失效,我们参考库里面,是将insertvoid变成iterator 类型,将迭代器返回给it重新赋值即可

iterator insert(iterator pos, const T& x)
{ 
  assert(pos <= _finish);
  assert(pos >= _start);
  if (_finish == _end_of_storage)
  {
    size_t len = pos - _start; // 在扩容时, 我们保留下pos和_start的相对位置
    reserve(capacity() == 0 ? 4 : capacity() * 2);
    pos = _start + len; // 在扩容结束后,将pos恢复回来
    // 虽然我们进行了此处操作当时依然不能避免迭代器失效
  }
  iterator end = _finish - 1;
  while (end >= pos)
  {
    *(end + 1) = *end;
    end--;
  }
  *pos = x;
  _finish++;
  return pos; // 返回迭代器在重新赋值
}

🍁在删除时失效

解决删除时的迭代器失效,我们只需要更改代码,让它删除后不用再++迭代器,或者没删除的时候再++,但是这样治标不治本,因此我们选择效仿库里面,返回迭代器,将迭代器返回给it重新赋值即可


iterator erase(iterator pos)
{
  assert(pos >= _start);
  assert(pos < _finish);

  iterator it = pos + 1;
  while (it < _finish)
  {
    *(it - 1) = *it;
    it++;
  }
    _finish--;
    return pos;
}

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

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


📜3. vector的拷贝问题

vector的拷贝问题也是我们在实际编程中经常需要面对的挑战。拷贝操作在C++中非常常见,无论是函数参数的传递、对象的赋值还是容器之间的交互,都可能涉及到拷贝操作。然而,对于vector这样的动态容器,拷贝操作可能会带来性能上的开销,尤其是浅拷贝和深拷贝的问题,容易给我们带来困扰


🎩浅拷贝

由于我们在模拟实现时,用的都是memcpy来拷贝元素,操作不慎就会引发浅拷贝问题

  • memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  • 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
// memcpy(tmp, _start, sizeof(T) * sz); 拷贝元素

void test_vector()
{
  vector<string> v1;
  v1.push_back("aaaaaaaaaaaaaa");
  v1.push_back("bbbbbbbbbbbbbb");
  v1.push_back("cccccccccccccc");
  v1.push_back("dddddddddddddd");
  v1.push_back("dddddddddddddd");
  v1.push_back("eeeeeeeeeeeeee"); // 此处需要扩容 
  for (auto e : v1)
  {
    cout << e << " ";
  }
}

memcpy会带来浅拷贝的隐患,因此我们用另外一种方法来进行拷贝

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



🎈深拷贝

我们可以用for循环将memcpy进行替换来避免浅拷贝,造成程序崩溃

void push_back(const T& x)
{
  if (_finish == _end_of_storage)
  {
    reserve(capacity() == 0 ? 4 : capacity() * 2);
    size_t sz = size();
    size_t cp = capacity();
    T* tmp = new T[cp];

    //memcpy(tmp, _start, sizeof(T) * sz);
    // 用for循环进行深拷贝
    for (size_t i = 0; i < sz; i++)
    {
      tmp[i] = _start[i];
    }
    delete[] _start;

    _start = tmp;
    _finish = _start + sz;
    _end_of_storage = _start + cp;
  }
  *_finish = x;
  _finish++;
}

📖4. 总结补充

💧补充:insert和erase的模拟实现(优化前)

void insert(iterator pos, const T& x)
{ 
  assert(pos <= _finish);
  assert(pos >= _start);
  
  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 erase(iterator pos)
{
  assert(pos >= _start);
  assert(pos < _finish);
  
  iterator it = pos + 1;
  while (it < _finish)
  {
    *(it-1) = *it;
    it++;
  }
  _finish--;
}

🔥总结

在深入探讨STL中vector的迭代器失效和拷贝问题后,我们不难发现,这些问题虽然常见,但理解其背后的原理并采取相应的措施,可以有效避免它们带来的潜在风险

  • 对于迭代器失效,我们了解到它通常发生在vector进行扩容、插入或删除元素等操作时。为了避免迭代器失效,我们需要时刻注意迭代器的有效性和生命周期,确保在操作过程中不会意外地修改或销毁迭代器所指向的对象。此外,了解vector扩容的时机和机制,也可以帮助我们预测和避免潜在的迭代器失效问题
  • 而对于拷贝问题,我们认识到vector的拷贝操作可能会带来性能上的开销,以及造成程序崩溃的结果。为了减少这些开销,我们可以考虑使用移动语义、避免不必要的拷贝以及优化拷贝策略等方法。同时,了解不同拷贝方式的优缺点和适用场景,可以帮助我们更加明智地选择适当的拷贝方式


我们希望能够为大家提供关于vector迭代器失效和拷贝问题的深入理解,并引导他们采取正确的措施来避免这些问题。然而,学习是一个永无止境的过程。随着C++语言的不断发展和STL库的更新迭代,我们可能会发现更多关于vector的新特性和最佳实践。 因此,我们希望大家继续深入学习C++和STL的相关知识,不断提高自己的编程能力和代码质量

目录
相关文章
|
4月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
99 2
|
6月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
168 2
|
2月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
64 0
|
3月前
|
存储 机器学习/深度学习 算法
基于 C++ 的局域网访问控制列表(ACL)实现及局域网限制上网软件算法研究
本文探讨局域网限制上网软件中访问控制列表(ACL)的应用,分析其通过规则匹配管理网络资源访问的核心机制。基于C++实现ACL算法原型,展示其灵活性与安全性。文中强调ACL在企业与教育场景下的重要作用,并提出性能优化及结合机器学习等未来研究方向。
90 4
|
4月前
|
监控 算法 数据处理
基于 C++ 的 KD 树算法在监控局域网屏幕中的理论剖析与工程实践研究
本文探讨了KD树在局域网屏幕监控中的应用,通过C++实现其构建与查询功能,显著提升多维数据处理效率。KD树作为一种二叉空间划分结构,适用于屏幕图像特征匹配、异常画面检测及数据压缩传输优化等场景。相比传统方法,基于KD树的方案检索效率提升2-3个数量级,但高维数据退化和动态更新等问题仍需进一步研究。未来可通过融合其他数据结构、引入深度学习及开发增量式更新算法等方式优化性能。
132 17
|
6月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
319 73
|
3月前
|
机器学习/深度学习 存储 算法
基于 C++ 布隆过滤器算法的局域网上网行为控制:URL 访问过滤的高效实现研究
本文探讨了一种基于布隆过滤器的局域网上网行为控制方法,旨在解决传统黑白名单机制在处理海量URL数据时存储与查询效率低的问题。通过C++实现URL访问过滤功能,实验表明该方法可将内存占用降至传统方案的八分之一,查询速度提升约40%,假阳性率可控。研究为优化企业网络管理提供了新思路,并提出结合机器学习、改进哈希函数及分布式协同等未来优化方向。
79 0
|
7月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
7月前
|
算法 编译器 C++
模拟实现c++中的vector模版
模拟实现c++中的vector模版
|
6月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
258 3