为了避免迭代器失效,我们需要了解哪些操作可能会导致迭代器失效,以及如何管理迭代器。下面是一些关于迭代器失效问题的建议和注意事项:
向容器添加元素和从容器删除元素的操作都可能导致迭代器失效。具体来说,对于不同的容器类型,失效的情况也有所不同,比如:
vector
或 string
容器,存储空间被重新分配时,所有指向容器元素的指针、引用或迭代器都会失效;如果存储空间未重新分配,则指向插入位置之后的元素的迭代器、指针和引用将会失效。deque
容器,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效;如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。list
和 forward_list
容器,指向容器的迭代器、指针和引用仍然有效。为了避免使用失效的迭代器,我们需要尽量减少要求迭代器必须保持有效的程序片段。具体来说,每次改变容器的操作之后都应该重新定位迭代器。在编写改变容器的循环程序时,需要特别注意。比如:
insert
或 erase
时,需要及时更新迭代器。end
返回的迭代器,而是应该在每次添加/删除元素后重新计算 end。添加/删除vector
、string
或deque
元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
//傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi = { 0,1,2,3,4,4,4,5,6,7,8 };
auto iter = vi.begin(); // 调用 begin 而不是 cbegin,因为我们要改变 vi
while (iter != vi.end()) {
if (*iter % 2) {
iter = vi.insert(iter, *iter); // 复制当前元素,并记录返回有效的迭代器
iter += 2; // 向前移动迭代器,跳过当前元素以及插入到它之前的元素
iter++;
}
else {
iter = vi.erase(iter); //删除偶数元素,并记录返回有效的迭代器
// 不应该向前移动迭代器,iter指向我们删除的元素之后的元素
}
}
此程序删除vector
中的偶数值元素,并复制每个奇数值元素。我们在调用insert
和erase
后都更新迭代器,因为两者都会使迭代器失效。
在调用erase后,不必递增迭代器,因为erase
返回的迭代器已经指向序列中下、个元素。调用insert
后,需要递增迭代器两次。记住,insert
在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。因此,在调用insert
后,iter
指向新插入元素,位于我们正在处理的元素之前。我们将迭代器递增两次,恰好越过了新添加的元素和正在处理的元素,指向下一个未处理的元素。
end
返回的选代器当我们添加/删除 vector
或 string
的元素后,或在 deque
中首元素之外任何位添加/删除元素后,原来 end
返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用 end
,而不能在循环之前保存 end
返回的选代器,一直当作容器末用。通常 C++标准库的实现中 end()
操作都很快,部分就是因为这个原因。
例如,考虑这样一个循环,它处理容器中 的每个元素,在其后添加一个新元素。我们希望循环能跳过新添加的元素,只处理原有元素。在每步循环之后,我们将定位选代器使其指向下一个原有元素。如果我们试图“优化”这个循环,在循环之前保存 end()
返的迭代器,一直用作容器末尾,就会导致一场灾难:
vector<int> v = { 0,1,2,3,4,4,4,5,6,7,8 };
//灾难:此循环的行为是未定义的
auto begin = v.begin(),
end = v.end(); //保存尾迭代器的值是个坏注意
while (begin != end) {
//做一些处理
//插入新的值,对begin重新赋值,否则的话他就会失效
++begin; // 向前移动begin重新赋值,否则的话它就会失效
begin = v.insert(begin, 42); // 插入新值
++begin; //向前移动begin跳过我们刚刚加入的元素
}
此代码行为是未定义的。在很多标准库实现上,此代码会导致无限循环。问题在于我们将end操作返回的迭代器保存在一个名为end的局部变量中。在循环体中,我们向容器添加了一个元素,这个操作使保存end迭代器中的迭代器失效了。这个迭代器不再指向v中任何元素,或是v中元素之后的值。
如果在一个循环中插入/删除
deque、string
或vector
中的元素,不要缓存end
返回的迭代器。
必须每插入操作后重新调用end(),而不能在循环开始保存它返回的迭代器:
//更安全的方法:在每个循环步添加/删除元素后都重新计算end
while (begin != v.end()) {
//做一些处理
++begin; // 向前移动begin,因为我们想要在此元素后插入元素
begin = v.insert(begin, 42);// 插入新值
++begin; //向前移动begin,跳过我们刚刚加入的元素
}
当我们编写添加/删除容器元素的循环程序时,需要特别小心迭代器失效问题。在循环中应该尽量减少对失效的迭代器的使用,并及时更新迭代器。比如:
vector
或 string
中添加/删除元素时,不要保存 end
返回的迭代器。deque
或 vector
中的元素时,需要在每个循环步骤之后重新计算迭代器。std::vector<int> vec;
// 错误示例:直接赋值操作会导致迭代器失效
for (int i = 0; i < 10; ++i) {
vec[i] = i;
}
// 正确示例:使用插入操作保证迭代器的有效性
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
std::list<int> lst{1, 2, 3, 4, 5};
// 错误示例:未更新迭代器导致失效
for (auto it = lst.begin(); it != lst.end(); ++it) {
if (*it % 2 == 0) {
lst.erase(it); // 删除偶数元素
}
}
// 正确示例:正确更新迭代器
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it % 2 == 0) {
it = lst.erase(it); // 删除偶数元素,并更新迭代器
} else {
++it;
}
}
std::vector<int> vec{1, 2, 3, 4, 5};
// 错误示例:在循环中直接删除元素导致迭代器失效
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
vec.erase(it); // 直接删除偶数元素,导致迭代器失效
}
}
// 正确示例:先保存需要删除的元素,再进行删除操作
std::vector<int> vec{1, 2, 3, 4, 5};
std::vector<int> toRemove;
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
toRemove.push_back(*it); // 保存需要删除的元素
}
}
for (const auto& elem : toRemove) {
vec.erase(std::remove(vec.begin(), vec.end(), elem), vec.end()); // 删除元素
}
总之,迭代器失效问题是 C++ 程序设计中一个非常重要的问题,需要特别小心,才能避免程序错误。在编写代码时,要时刻注意迭代器的有效性,并且尽量减少对失效迭代器的使用,这是一个良好的编程习惯。