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

Eternity._ 2024-07-03 10:35:02 阅读 69

📝个人主页🌹:Eternity._

⏩收录专栏⏪:C++ “ 登神长阶 ”

🤡往期回顾🤡:初步了解vector

🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

❀STL之vector

📒1. 迭代器失效🌈插入时失效🌞删除时失效

📕2. 解决迭代器失效🍂在插入时失效🍁在删除时失效

📜3. vector的拷贝问题🎩浅拷贝🎈深拷贝

📖4. 总结补充💧补充:insert和erase的模拟实现(优化前)🔥总结


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

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


🌞删除时失效

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指向进行更新,但是这样做依然解决不了迭代器失效,我们参考库里面,是将<code>insert从void变成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进行替换来避免浅拷贝,造成程序崩溃

<code>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的相关知识,不断提高自己的编程能力和代码质量

在这里插入图片描述

谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。