【C++面试高频】STL 迭代器失效:vector、map、unordered_map 常见问题总结

一、什么是迭代器失效?

迭代器可以理解为"指向容器中某个元素的位置"。

例如:

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

// it 指向 nums 中的第一个元素 10
auto it = nums.begin();

cout << *it << endl;

这里的:

复制代码
*it

表示访问迭代器当前指向的元素。

所谓迭代器失效,就是容器发生某些操作后,原来的迭代器不再能安全地访问原来的元素位置。

继续使用失效迭代器,可能导致:

复制代码
程序崩溃
访问错误数据
运行结果异常
未定义行为

除了迭代器,某些操作还可能导致原来的指针和引用失效。

例如:

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

// p 指向第一个元素
int* p = &nums[0];

// ref 是第一个元素的引用
int& ref = nums[0];

如果 vector 后续发生扩容,pref 和原来的迭代器都可能失效。


二、为什么会出现迭代器失效?

不同容器的底层结构不同,所以失效规则也不同。

复制代码
vector:底层是连续内存的动态数组。
map:底层通常是红黑树。
unordered_map:底层通常是哈希表。

其中,vector 最容易发生迭代器失效,因为它需要保证元素在一段连续内存中存储。

vector 容量不足时,它会申请一块新的、更大的连续内存,再把旧元素移动过去。此时原来的地址就可能全部变化。

map 使用树结构存储节点,插入新节点一般不会移动旧节点。

unordered_map 使用哈希表存储元素,当桶数量变化时会发生 rehash,原来的迭代器可能失效。


三、vector 的迭代器失效

vector 是面试中最常被问到的迭代器失效容器。

它的底层是动态数组,元素在内存中连续存储。


1. vector 扩容为什么会导致迭代器失效?

先看一个例子:

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums;

    // 先放入一个元素
    nums.push_back(10);

    // 保存第一个元素的迭代器、指针和引用
    auto it = nums.begin();
    int* p = &nums[0];
    int& ref = nums[0];

    // 继续插入元素,可能触发扩容
    nums.push_back(20);
    nums.push_back(30);
    nums.push_back(40);

    // 如果中间发生扩容,
    // it、p、ref 都可能已经失效

    cout << nums[0] << endl;

    return 0;
}

vector 内部有两个重要概念:

复制代码
nums.size();      // 当前元素数量
nums.capacity();  // 当前已经申请的容量

当:

复制代码
size > capacity

时,vector 通常会:

复制代码
1. 申请一块更大的连续内存。
2. 将旧元素移动或复制到新内存。
3. 释放旧内存。

因此,原来的迭代器、指针和引用仍然指向旧内存,自然就失效了。

可以简单理解为:

复制代码
扩容前:

it、p、ref  ───> [10]

扩容后:

nums         ───> [10][20][30][40]

it、p、ref  ───> 旧内存,已经不能继续使用

2. vector 的 push_back 会不会导致迭代器失效?

答案是:可能会。

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

auto it = nums.begin();

nums.push_back(4);

如果 push_back(4) 时发生扩容,那么:

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

如果没有发生扩容,则:

复制代码
原来指向已有元素的迭代器、指针和引用通常仍然有效。
但是 end() 迭代器会失效。

例如:

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums;

    // 预留 10 个元素空间,减少后续扩容概率
    nums.reserve(10);

    nums.push_back(1);
    nums.push_back(2);

    // it 指向第一个元素
    auto it = nums.begin();

    // 当前容量足够,通常不会发生扩容
    nums.push_back(3);

    // it 仍然可以安全访问第一个元素
    cout << *it << endl;

    return 0;
}

这里的 reserve(10) 只是提前申请容量,不能改变 size()

复制代码
vector<int> nums;

nums.reserve(10);

cout << nums.size() << endl;      // 0
cout << nums.capacity() << endl;  // 至少为 10

3. vector 的 insert 为什么会导致迭代器失效?

vector 中间插入元素时,后面的元素需要向后移动。

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4};

    // it 指向元素 3
    auto it = nums.begin() + 2;

    // 在下标 1 的位置插入 100
    nums.insert(nums.begin() + 1, 100);

    // 原来的 it 可能失效,不能继续使用
    // cout << *it << endl;

    return 0;
}

如果插入操作触发扩容:

复制代码
所有迭代器、指针和引用都会失效。

即使没有扩容,插入位置及其后面的元素都发生了移动,因此:

复制代码
插入位置之前的迭代器通常仍然有效。
插入位置及其之后的迭代器、指针和引用会失效。

4. vector 的 erase 为什么会导致迭代器失效?

删除 vector 中间元素后,后面的元素需要向前移动来填补空位。

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {10, 20, 30, 40};

    // it 指向 20
    auto it = nums.begin() + 1;

    // 删除元素 20
    nums.erase(it);

    // 原来的 it 已经失效,不能继续使用
    // cout << *it << endl;

    return 0;
}

对于 vector::erase()

复制代码
被删除位置及其之后的迭代器、指针和引用都会失效。

因此,遍历 vector 时删除元素,不能直接写成下面这样:

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

for (auto it = nums.begin(); it != nums.end(); ++it) {
    if (*it % 2 == 0) {
        nums.erase(it);  // 删除后 it 已经失效
    }
}

上面代码的问题是:

复制代码
erase(it) 后,it 已经失效。
循环末尾还会执行 ++it。
此时相当于操作失效迭代器。

5. vector 中安全删除元素的正确写法

正确方式是接收 erase() 的返回值。

erase() 会返回被删除元素之后的下一个有效迭代器。

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5, 6};

    for (auto it = nums.begin(); it != nums.end(); ) {
        // 删除所有偶数
        if (*it % 2 == 0) {
            // erase 返回下一个有效位置
            it = nums.erase(it);
        } else {
            // 当前元素不删除,迭代器正常后移
            ++it;
        }
    }

    for (int num : nums) {
        cout << num << " ";
    }

    return 0;
}

输出结果:

复制代码
1 3 5

这段代码需要重点理解:

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

删除当前元素后,不再手动 ++it,而是直接使用 erase() 返回的下一个有效迭代器。


6. vector 常见操作的失效规则

操作 迭代器、指针、引用失效情况
push_back() / emplace_back() 触发扩容 全部失效
push_back() / emplace_back() 未扩容 原有元素位置通常有效,但 end() 失效
insert() 触发扩容 全部失效
insert() 未扩容 插入位置及其之后失效
erase() 删除位置及其之后失效
clear() 全部失效
reserve() 导致重新分配内存 全部失效
resize() 导致扩容 可能全部失效

可以简单记忆:

复制代码
vector 只要发生扩容,全部失效。
vector 在中间插入或删除,操作位置及其后面的迭代器容易失效。

四、map 的迭代器失效

map 是有序键值对容器,底层通常使用红黑树实现。

vector 不同,map 的元素通常是独立节点存储的。插入新元素时,不需要整体移动原有元素。


1. map 插入元素会导致原迭代器失效吗?

通常不会。

复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";

    // it 指向 key 为 1 的元素
    auto it = students.find(1);

    // 插入新元素
    students[3] = "Alice";

    // 原来的 it 仍然有效
    cout << it->first << " : " << it->second << endl;

    return 0;
}

输出结果:

复制代码
1 : Tom

对于 map

复制代码
插入元素通常不会使已有元素的迭代器、指针和引用失效。

这也是 mapvector 的重要区别。


2. map 删除元素会导致哪些迭代器失效?

map 中,删除某个元素时:

复制代码
只有指向被删除元素的迭代器、指针和引用失效。
其他元素的迭代器通常仍然有效。

示例:

复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";
    students[3] = "Alice";

    auto it1 = students.find(1);
    auto it2 = students.find(2);

    // 删除 key 为 2 的元素
    students.erase(it2);

    // it2 已经失效,不能继续使用
    // cout << it2->second << endl;

    // it1 指向的元素没有被删除,仍然有效
    cout << it1->first << " : " << it1->second << endl;

    return 0;
}

3. 遍历 map 时安全删除元素

遍历 map 时删除元素,也推荐使用 erase() 的返回值。

复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";
    students[3] = "Alice";
    students[4] = "Bob";

    for (auto it = students.begin(); it != students.end(); ) {
        // 删除所有偶数 key 对应的元素
        if (it->first % 2 == 0) {
            // erase 返回下一个有效迭代器
            it = students.erase(it);
        } else {
            ++it;
        }
    }

    for (const auto& item : students) {
        cout << item.first << " : " << item.second << endl;
    }

    return 0;
}

输出结果:

复制代码
1 : Tom
3 : Alice

4. map 常见操作的失效规则

操作 迭代器、指针、引用失效情况
insert() / emplace() 已有元素通常不失效
operator[] 插入新元素 已有元素通常不失效
erase(it) 只有被删除元素失效
clear() 全部失效

简单记忆:

复制代码
map 插入通常不影响旧迭代器。
map 删除谁,谁的迭代器失效。

五、unordered_map 的迭代器失效

unordered_map 是无序键值对容器,底层通常是哈希表。

它最大的特点是查找效率高,但由于可能发生 rehash,它的迭代器失效规则比 map 更需要注意。


1. 什么是 rehash?

unordered_map 内部会把元素分配到不同的桶中。

例如:

复制代码
桶 0:元素 A
桶 1:元素 B、元素 C
桶 2:元素 D

当元素越来越多时,如果桶数量不够,哈希冲突可能增加,查找效率下降。

这时 unordered_map 可能会重新申请更多桶,并重新计算每个元素应该放到哪个桶中。

这个过程叫:

复制代码
rehash,中文通常叫重哈希。

2. unordered_map 插入元素会导致迭代器失效吗?

答案是:可能会。

复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";

    auto it = students.find(1);

    // 插入新元素时,可能触发 rehash
    students[3] = "Alice";

    // 如果发生 rehash,it 可能已经失效
    // cout << it->second << endl;

    return 0;
}

如果插入元素导致 rehash

复制代码
所有迭代器都会失效。

但是需要注意:

复制代码
rehash 通常不会让指向元素的引用和指针失效。
真正失效的重点是迭代器。

为了避免频繁 rehash,可以提前预留空间:

复制代码
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, string> students;

    // 预计要插入 100 个元素,提前预留空间
    students.reserve(100);

    students[1] = "Tom";
    students[2] = "Jack";

    return 0;
}

reserve() 可以减少插入过程中的重哈希次数,但不代表任何情况下都绝对不会发生 rehash


3. unordered_map 删除元素的影响

删除元素时:

复制代码
只有指向被删除元素的迭代器、指针和引用失效。
其他元素通常仍然有效。

示例:

复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";
    students[3] = "Alice";

    auto it1 = students.find(1);
    auto it2 = students.find(2);

    // 删除 key 为 2 的元素
    students.erase(it2);

    // it2 已经失效,不能继续使用
    // cout << it2->second << endl;

    // it1 仍然指向 key 为 1 的元素
    cout << it1->first << " : " << it1->second << endl;

    return 0;
}

4. 遍历 unordered_map 时安全删除元素

vectormap 一样,删除时接收 erase() 的返回值。

复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    unordered_map<int, string> students;

    students[1] = "Tom";
    students[2] = "Jack";
    students[3] = "Alice";
    students[4] = "Bob";

    for (auto it = students.begin(); it != students.end(); ) {
        // 删除 key 为偶数的元素
        if (it->first % 2 == 0) {
            it = students.erase(it);
        } else {
            ++it;
        }
    }

    for (const auto& item : students) {
        cout << item.first << " : " << item.second << endl;
    }

    return 0;
}

注意:unordered_map 是无序容器,因此最终输出顺序不固定。


5. unordered_map 常见操作的失效规则

操作 迭代器、指针、引用失效情况
插入元素但未发生 rehash 原有迭代器通常仍有效
插入元素且发生 rehash 所有迭代器失效
reserve() / rehash() 所有迭代器失效
erase(it) 只有被删除元素失效
clear() 全部失效

简单记忆:

复制代码
unordered_map 插入时要防 rehash。
一旦 rehash,原来的迭代器可能全部失效。

六、三种容器的迭代器失效对比

操作 vector map unordered_map
插入元素 可能扩容,可能全部失效 原有迭代器通常有效 可能 rehash,迭代器可能全部失效
中间插入 插入位置及之后通常失效 不适用下标位置概念 不适用下标位置概念
删除一个元素 删除位置及之后失效 仅被删元素失效 仅被删元素失效
扩容 / 重分配 全部失效 不存在类似扩容移动 rehash 后迭代器失效
clear 全部失效 全部失效 全部失效
是否有序 按插入顺序 按 key 排序 无序

可以这样快速记忆:

复制代码
vector:最容易失效,扩容全失效,删除后面也失效。
map:最稳定,插入不影响,删除谁谁失效。
unordered_map:插入可能 rehash,rehash 后迭代器失效。

七、遍历容器时删除元素的通用写法

无论是 vectormap 还是 unordered_map,遍历时删除元素都推荐使用下面的写法:

复制代码
for (auto it = container.begin(); it != container.end(); ) {
    if (满足删除条件) {
        it = container.erase(it);
    } else {
        ++it;
    }
}

核心原因是:

复制代码
erase(it) 后,原来的 it 不一定还能使用。
erase 的返回值正好是下一个有效位置。

下面是一个通用示例:

复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 2, 3, 4, 5};

    for (auto it = nums.begin(); it != nums.end(); ) {
        if (*it > 3) {
            // 删除大于 3 的元素
            it = nums.erase(it);
        } else {
            ++it;
        }
    }

    for (int num : nums) {
        cout << num << " ";
    }

    return 0;
}

输出结果:

复制代码
1 2 3

八、常见错误写法

1. 删除后继续使用旧迭代器

错误写法:

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

        // it 已经失效,后续 ++it 存在风险
    }
}

正确写法:

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

2. 用范围 for 循环直接删除元素

错误写法:

复制代码
for (int num : nums) {
    if (num == 3) {
        nums.erase(nums.begin());
    }
}

范围 for 循环本质上也会使用迭代器。

在循环过程中修改容器结构,可能导致内部迭代器失效,因此不推荐这样写。

如果需要删除元素,应该改用显式迭代器循环。


3. 以为 reserve 可以解决所有 vector 失效问题

reserve() 的作用是提前预留容量,主要减少扩容带来的失效问题。

但是它不能解决中间插入、删除元素带来的迭代器失效问题。

例如:

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

nums.reserve(100);

auto it = nums.begin() + 2;

// 即使容量足够,中间插入仍会移动后面的元素
nums.insert(nums.begin() + 1, 100);

// it 仍然可能失效

因此:

复制代码
reserve 只能减少扩容问题,
不能解决 vector 元素移动问题。

九、面试高频问题整理

1. 什么是迭代器失效?

迭代器失效是指容器经过插入、删除、扩容或重哈希等操作后,原来的迭代器不再能安全访问原来的元素位置。继续使用失效迭代器可能导致程序崩溃或未定义行为。


2. vector 为什么容易发生迭代器失效?

因为 vector 底层是连续内存的动态数组。当容量不足时,vector 会重新申请更大的内存,并把原元素移动过去,原来的迭代器、指针和引用仍指向旧内存,所以会失效。

另外,在中间插入和删除元素时,后面的元素需要移动,因此操作位置及其之后的迭代器也可能失效。


3. vector 扩容后哪些内容会失效?

如果 vector 发生扩容:

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

因为元素存储地址发生了变化。


4. map 插入元素会导致迭代器失效吗?

通常不会。

map 底层通常是红黑树,插入新节点不需要移动已有节点,因此已有元素的迭代器、指针和引用通常仍然有效。

删除元素时,只有指向被删除元素的迭代器、指针和引用失效。


5. unordered_map 为什么插入后迭代器可能失效?

unordered_map 底层是哈希表。插入元素后,如果负载过高,可能触发 rehash,也就是重新分配桶并重新组织元素。

一旦发生 rehash,原来的迭代器可能全部失效。

因此如果预计要插入大量数据,可以提前调用 reserve(),减少重哈希概率。


6. 遍历容器时如何安全删除元素?

推荐写法是:

复制代码
for (auto it = container.begin(); it != container.end(); ) {
    if (满足条件) {
        it = container.erase(it);
    } else {
        ++it;
    }
}

因为 erase() 会返回下一个有效迭代器,可以避免继续操作已经失效的旧迭代器。


十、总结

迭代器失效是 STL 面试和实际开发中都非常重要的问题。

vector 底层使用连续内存,扩容会导致所有迭代器、指针和引用失效;中间插入和删除会导致操作位置及其之后的迭代器失效。

map 的底层通常是红黑树,插入元素通常不会影响已有迭代器;删除元素时,只有被删除元素对应的迭代器失效。

unordered_map 的底层通常是哈希表,插入元素时可能触发 rehash。一旦发生 rehash,原来的迭代器可能全部失效,因此大量插入前可以使用 reserve() 提前预留空间。

最后可以简单记忆:

复制代码
vector:
扩容时全部失效;
插入和删除时,操作位置及之后容易失效。

map:
插入通常不失效;
删除谁,谁失效。

unordered_map:
插入可能触发 rehash;
rehash 后迭代器可能全部失效。

遍历删除元素:
使用 it = container.erase(it)。

掌握这些规则后,可以避免 STL 容器操作中很多隐蔽的崩溃和错误。

0voice · GitHub