【C++面试】迭代器失效是什么?vector、list、map中如何避免迭代器失效

一、什么叫迭代器失效

先看一个简单的:

复制代码
std::vector<int> nums = {10, 20, 30};

auto it = nums.begin();

std::cout << *it << std::endl;

此时:

复制代码
it
↓
┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘

it 保存的是一个能够定位到:

复制代码
nums中第一个元素

的位置。

但是如果容器发生变化:

复制代码
nums.push_back(40);

并且这次 push_back() 触发了扩容,那么 vector 会:

复制代码
申请新内存
↓
把10、20、30搬到新内存
↓
插入40
↓
释放旧内存

例如原来:

复制代码
0x1000

┌────┬────┬────┐
│ 10 │ 20 │ 30 │
└────┴────┴────┘

扩容后:

复制代码
0x5000

┌────┬────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │    │    │
└────┴────┴────┴────┴────┴────┘

但是原来的:

复制代码
it

可能还指向:

复制代码
0x1000

而原来的内存已经释放。

这时再:

复制代码
std::cout << *it << std::endl;

就是:

复制代码
未定义行为

这就是:

复制代码
迭代器失效
Iterator Invalidation

它本质上就是:

容器修改以后,原来迭代器所依赖的元素位置、节点或者底层内存已经发生变化,继续使用旧迭代器不再安全。

除了迭代器:

复制代码
pointer
reference

也可能一起失效。

例如:

复制代码
int *p = &nums[0];
int &ref = nums[1];

nums.push_back(40);

如果发生扩容:

复制代码
iterator
pointer
reference

都可能失效。

二、vector为什么最容易出现迭代器失效

vector 底层使用:

复制代码
连续内存

因此有两类特别常见的失效问题。

1. 扩容导致全部失效

例如:

复制代码
std::vector<int> nums = {1, 2, 3};

auto it = nums.begin();

nums.push_back(4);

std::cout << *it << std::endl;

如果:

复制代码
size == capacity

那么 push_back() 需要重新分配内存。

于是原来的:

复制代码
所有迭代器
所有指针
所有引用

都会失效。

所以如果提前知道元素数量,可以:

复制代码
std::vector<int> nums;

nums.reserve(1000);

提前准备容量。

然后:

复制代码
for (int i = 0; i < 1000; ++i) {
    nums.push_back(i);
}

只要没有超过已经预留的容量:

复制代码
就不会因为扩容反复搬迁底层数组

所以:

复制代码
reserve()

是减少 vector 扩容型迭代器失效的一种重要方法。

但要注意:

reserve() 只能减少因为重新分配内存造成的失效,并不能解决所有 vector 迭代器失效问题。


2. erase导致当前位置以及后面的迭代器失效

例如:

复制代码
std::vector<int> nums = {10, 20, 30, 40};

auto it = nums.begin() + 1;

nums.erase(it);

原来:

复制代码
10 20 30 40
   ↑
   it

删除 20 后:

复制代码
10 30 40

因为:

复制代码
30和40需要向前移动

所以被删除位置以及后面的迭代器都会失效。

因此下面这种写法是危险的:

复制代码
for (auto it = nums.begin(); it != nums.end(); ++it) {
    if (*it == 20) {
        nums.erase(it);
    }
}

问题在于:

复制代码
nums.erase(it);

之后:

复制代码
it已经失效

但是循环后面还执行:

复制代码
++it

相当于继续操作一个已经失效的迭代器。

正确写法:

复制代码
for (auto it = nums.begin(); it != nums.end(); ) {
    if (*it == 20) {
        it = nums.erase(it);
    } else {
        ++it;
    }
}

为什么?

因为:

复制代码
erase(it)

会返回:

复制代码
被删除元素后面的下一个有效迭代器

例如:

复制代码
10 20 30 40
   ↑
   it

erase(it)
   ↓

10 30 40
   ↑
返回新的it

所以这是删除元素时非常重要的写法:

复制代码
it = container.erase(it);

三、不同STL容器的失效规则并不一样

不是所有 STL 容器都和 vector 一样。

1. vector

vector:

复制代码
连续内存

所以比较容易因为元素搬迁导致失效。

常见情况:

复制代码
重新分配内存
↓
全部迭代器、引用、指针失效

而:

复制代码
erase
↓
删除位置以及之后的迭代器失效

2. list

std::list 底层通常是:

复制代码
双向链表

例如:

复制代码
Node1 ↔ Node2 ↔ Node3 ↔ Node4

每个节点相对独立。

所以插入一个节点:

复制代码
Node1 ↔ Node2 ↔ New ↔ Node3

通常不会移动其他节点。

因此:

复制代码
insert
↓
原来的其他迭代器通常仍然有效

删除:

复制代码
erase(Node2)

也通常只会使:

复制代码
指向Node2的迭代器失效

其他节点:

复制代码
Node1
Node3
Node4

没有搬家。

所以其他迭代器通常仍然有效。

这也是链式容器的重要特点。


3. map / set

map、set 通常是:

复制代码
树结构

插入新元素时:

复制代码
通常不会使已有元素的迭代器失效

删除某个元素时:

复制代码
只有指向被删除元素的迭代器失效

所以:

复制代码
for (auto it = m.begin(); it != m.end(); ) {
    if (it->second == 0) {
        it = m.erase(it);
    } else {
        ++it;
    }
}

同样是比较安全的删除写法。


4. unordered_map / unordered_set

这一类要特别注意:

复制代码
哈希表

普通插入:

复制代码
不一定导致已有迭代器失效

但是如果插入触发:

复制代码
rehash

哈希桶重新组织。

那么:

复制代码
已有迭代器可能全部失效

例如:

复制代码
std::unordered_map<int, int> table;

auto it = table.find(1);

table.insert({100, 100});

如果插入过程中触发 rehash:

复制代码
bucket数量变化
↓
元素重新分布
↓
原迭代器失效

所以如果已经知道大概会存多少元素,可以:

复制代码
table.reserve(10000);

减少频繁 rehash。

这里和:

复制代码
vector::reserve()

思路很像。

一个是:

复制代码
减少扩容

一个是:

复制代码
减少rehash

四、实际开发中怎么避免迭代器失效

第一种方法:

容器修改以后,不要盲目继续使用旧迭代器。

例如:

复制代码
auto it = nums.begin();

nums.push_back(100);

// 如果可能发生扩容
// 不继续使用原it

it = nums.begin();

也就是:

复制代码
容器结构改变以后
↓
必要时重新获取迭代器

第二种方法:

删除元素时使用 erase 的返回值。

错误:

复制代码
for (auto it = nums.begin(); it != nums.end(); ++it) {
    if (*it % 2 == 0) {
        nums.erase(it);
    }
}

正确:

复制代码
for (auto it = nums.begin(); it != nums.end(); ) {
    if (*it % 2 == 0) {
        it = nums.erase(it);
    } else {
        ++it;
    }
}

这个模式建议直接记住:

复制代码
如果删除
↓
it = erase(it)

如果不删除
↓
++it

第三种方法:

不要在 range-for 中随便修改容器结构。

例如:

复制代码
for (auto &x : nums) {
    if (x == 10) {
        nums.push_back(100);
    }
}

range-for 底层实际上会使用类似:

复制代码
begin
end
iterator

如果 push_back() 导致 vector 扩容:

复制代码
循环内部使用的迭代器也会失效

所以这种修改非常危险。

如果确实需要:

复制代码
遍历过程中新增/删除

最好重新设计:

复制代码
第一阶段遍历
↓
记录要修改的内容

第二阶段
↓
统一修改容器

例如:

复制代码
std::vector<int> toAdd;

for (int x : nums) {
    if (x == 10) {
        toAdd.push_back(100);
    }
}

nums.insert(nums.end(), toAdd.begin(), toAdd.end());

这样不会在遍历原容器过程中直接破坏迭代器。


第四种方法:

vector已知大小时提前reserve。

例如:

复制代码
std::vector<Task> tasks;

tasks.reserve(10000);

for (int i = 0; i < 10000; ++i) {
    tasks.push_back(Task{});
}

减少:

复制代码
扩容
↓
内存地址变化
↓
全部迭代器失效

第五种方法:

有时可以保存下标,而不是长期保存vector迭代器。

例如:

复制代码
size_t index = 2;

然后:

复制代码
nums.push_back(100);

std::cout << nums[index] << std::endl;

即使 vector 扩容:

复制代码
底层地址变了
但第2个元素的位置语义仍然可以通过下标重新计算

相比:

复制代码
auto it = nums.begin() + 2;

长期保存下标有时更加安全。

但是要注意:

复制代码
如果在index前面插入或删除元素
下标对应的元素本身也会发生变化

所以:

复制代码
下标不是万能解决方案

只是适合某些 vector 场景。

五、面试中迭代器失效应该怎么回答

如果面试官问:

什么叫迭代器失效?

可以回答:

迭代器失效是指容器经过插入、删除、扩容或 rehash 等操作以后,原来的迭代器不再能够安全定位原来的元素。如果继续解引用或移动这个迭代器,就可能产生未定义行为。

如果继续问:

vector什么时候会发生迭代器失效?

可以回答:

vector 如果发生重新分配内存,比如 push_back 导致容量不足而扩容,那么原来的所有迭代器、指针和引用都会失效。erase 不一定重新分配内存,但由于后面的元素会向前移动,所以被删除位置以及之后的迭代器会失效。

如果问:

怎么避免?

可以回答:

第一,不要在容器结构发生变化以后继续使用旧迭代器;第二,遍历删除时使用 erase 的返回值更新迭代器;第三,vector 如果能够预估容量,可以提前 reserve 减少扩容;第四,不要在 range-for 中随意插入或删除当前容器;如果修改后仍需访问元素,可以重新获取迭代器,或者在适合的场景保存下标。

如果继续问:

为什么list插入通常不会导致其他迭代器失效?

可以回答:

因为 list 是节点式存储,每个节点独立分配,插入新节点只需要修改相邻节点的指针,不需要移动其他节点,因此已有节点的地址通常不会发生变化。删除时一般也只有指向被删除节点的迭代器失效。

如果继续问:

unordered_map为什么会迭代器失效?

可以回答:

当插入元素导致 rehash 时,哈希表会重新建立 bucket,并重新组织元素所在的桶,所以已有迭代器会失效。可以通过提前 reserve 或合理设置 bucket 数量减少频繁 rehash。

最后可以把常见容器简单记成:

复制代码
vector
↓
扩容:全部失效
erase:删除位置及之后失效

list
↓
插入:其他迭代器通常不失效
删除:只删除节点的迭代器失效

map / set
↓
插入:已有迭代器通常有效
删除:被删除元素失效

unordered_map / unordered_set
↓
rehash
↓
迭代器失效

实际开发中最值得记住的代码就是:

复制代码
for (auto it = container.begin();
     it != container.end(); ) {

    if (needDelete(*it)) {
        it = container.erase(it);
    } else {
        ++it;
    }
}

以及一个原则:

复制代码
容器发生结构性修改
        ↓
先考虑旧迭代器是否还有效
        ↓
不确定时不要继续使用

迭代器失效本质上不是"迭代器坏掉了",而是:

复制代码
它原来记录的位置
已经无法再代表原来的元素

只要把这一点理解清楚,vector 扩容、erase、unordered_map rehash 等问题就比较容易判断了。

0voice · GitHub

相关推荐
怕浪猫1 小时前
面试官让我手写一个 Tool Use,我用了 3 种方案
设计模式·面试·程序员
苏辰澈1 小时前
C++自己的族谱规则:继承详解下
c语言·开发语言·c++·visualstudio
驭渊的小故事2 小时前
NC229023 题解:二分答案求解“最大化最小值“问题
java·算法
朝朝辞暮i2 小时前
C++ 第 36 课:Action——机械臂/VLA 非常重要
开发语言·c++·算法·ros2
无名猿2 小时前
锁进阶:atomic_flag 自旋锁、shared_mutex 读写锁与无锁的代价
c++·性能优化·并发编程·现代c++
ra90fy2 小时前
B3873 [GESP202309 六级] 小杨买饮料
c++·算法
尘客-追梦2 小时前
qmake / jom / 影子构建:把编译管起来
开发语言·c++·qt
EatFan2 小时前
2026 Java 后端面试风向变了:八股文只是门槛,场景化追问才是淘汰线
java·开发语言·jvm·面试·后端面试·八股文·full gc
Omics Pro3 小时前
北理工李荣华:AI虚拟细胞证据多模态推理基准
数据库·人工智能·算法·机器学习·自然语言处理