[C++面试] 迭代器面试点(难点)

种地  论坛元老 | 2025-3-21 20:54:27 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1048|帖子 1048|积分 3144

一、入门

1、什么是迭代器?它的作用是什么?

提供对容器元素次序访问的抽象接口,行为类似指针。
  1. std::vector<int> vec{1,2,3};
  2. for (auto it = vec.begin(); it != vec.end(); ++it) {
  3.     std::cout << *it << " "; // 输出1 2 3
  4. }
复制代码
解耦了容器与算法,使得STL算法(如sort、find)可以同一处理不同容器(如vector、list)。
   迭代器就像一个「遥控器」,可以帮你逐个访问容器里的元素。好比用遥控器换台一样,迭代器可以一步步走到下一个元素。 
  2、​迭代器有哪些类别?



  • 输入/输出迭代器​(只读/写,单遍扫描,如istream_iterator、ostream_iterator)
  • 前向迭代器​(可读写,多遍扫描,如forward_list的迭代器,好比单链表
  • 双向迭代器​(支持--操作,如list的迭代器,好比双向链表
  • 随机访问迭代器​(支持+n、-n,如vector的迭代器,好比数组的指针
3、如何判断两个迭代器是否指向同一个元素?

直接比较迭代器是否相等:if (it1 == it2)。留意:​只有同一个容器的迭代器才能比较,否则行为未界说
二、进阶

1、​迭代器失效的典型场景有哪些?如何制止?



  • vector插入/删除元素:插入可能导致所有迭代器失效(如 vector 插入元素超过容量时);删除元素后,被删元素后的迭代器失效。 [C++面试] vector 面试点总结-CSDN博客 
  1. for (auto it = vec.begin(); it != vec.end(); ++it) {
  2.     if (*it == 5) {
  3.         vec.erase(it); // it此时已失效!
  4.     }
  5. }
  6. // 正确示范:erase返回下一个有效迭代器
  7. for (auto it = vec.begin(); it != vec.end(); ) {
  8.     if (*it == 5) {
  9.         it = vec.erase(it); // 直接更新it
  10.     } else {
  11.         ++it;
  12.     }
  13. }
复制代码


  • ​map/unordered_map删除元素:被删元素的迭代器失效,其他迭代器仍有效。
    ​解决:插入/删除后重新获取迭代器,或使用返回值(如erase返回下一个有效迭代器)
2、迭代器与性能:vector的迭代器访问比索引访问更快吗?

在Release模式下,两者性能几乎雷同(编译器优化后等价于指针运算)。但在list等非一连容器中,迭代器访问更高效(索引访问需要O(n)遍历)。
在非一连容器(如list、forward_list、树布局容器)中,迭代器直接通过指针跳转,​无需计算偏移量,因此效率远高于索引访问。list[2]比++迭代器两次慢得多。
list的operator[]或std::advance(it, n)需要从头节点逐个遍历
  1. std::list<int> lst = {10, 20, 30, 40};
  2. // 模拟索引访问:获取第3个元素(索引2)
  3. auto it = lst.begin();
  4. std::advance(it, 2); // 需要从第1个节点 -> 第2个 -> 第3个
复制代码
3、为什么vector的迭代器和索引性能雷同?

vector的内存是一连的,两者生成的汇编代码完全一致,性能无差异:


  • vec会被编译器优化为*(vec.data() + i)(指针算术)
  • vector::iterator本质是原生指针,++it等价于ptr += 
4、如何实现一个自界说的迭代器?

需要界说迭代器类,并实现一些必要的操作符重载,如 ++、*、==、!= 等。
  1. #include <iostream>
  2. // 自定义容器类
  3. template <typename T>
  4. class MyContainer {
  5. private:
  6.     T* data;
  7.     int size;
  8. public:
  9.     MyContainer(T* arr, int s) : data(arr), size(s) {}
  10.     // 自定义迭代器类
  11.     class Iterator {
  12.     private:
  13.         T* ptr;
  14.     public:
  15.         Iterator(T* p) : ptr(p) {}
  16.         Iterator& operator++() {
  17.             ++ptr;
  18.             return *this;
  19.         }
  20.         T& operator*() {
  21.             return *ptr;
  22.         }
  23.         bool operator==(const Iterator& other) const {
  24.             return ptr == other.ptr;
  25.         }
  26.         bool operator!=(const Iterator& other) const {
  27.             return ptr != other.ptr;
  28.         }
  29.     };
  30.     Iterator begin() {
  31.         return Iterator(data);
  32.     }
  33.     Iterator end() {
  34.         return Iterator(data + size);
  35.     }
  36. };
  37. int main() {
  38.     int arr[] = {1, 2, 3, 4, 5};
  39.     MyContainer<int> container(arr, 5);
  40.     for (auto it = container.begin(); it != container.end(); ++it) {
  41.         std::cout << *it << " ";
  42.     }
  43.     std::cout << std::endl;
  44.     return 0;
  45. }
复制代码
三、高阶

1、​迭代器traits技能是什么?(也就是:iterator_traits)有什么作用?

   总结:Traits就像迭代器的「阐明书」,告诉算法这个迭代器能做什么(好比能否跳步)。
  迭代器的 traits 技能是一种利用模板元编程实现的技能,用于在编译时获取迭代器的相干信息,如迭代器的范例、值范例、引用范例等。使算法可以或许根据迭代器的特性举行优化,提高代码的效率和通用性。
  1. template<typename Iter>
  2. void print(Iter it) {
  3.     using ValueType = typename std::iterator_traits<Iter>::value_type;
  4.     ValueType val = *it;
  5.     std::cout << val;
  6. }
  7. std::vector<int> numbers = {1, 2, 3};
  8. std::list<double> data = {3.14, 2.71, 1.618};
  9. // 调用时自动推导类型
  10. print(vec.begin()); // ValueType是int
  11. print(data.begin()); // ValueType是double
复制代码
iterator_traits 是 C++ 标准库中的范例萃取工具,用于同一获取迭代器的属性(如元素范例、迭代器类别等)。它提供了一种标准化的方式,让算法可以或许透明地处理不同范例的迭代器(包括原生指针和自界说迭代器),无需关心其详细实现细节。
例如,std::distance根据迭代器范例(随机访问或双向)选择最优实现(O(1)或O(n))。
   范例萃取(Type Traits)是什么?
  范例萃取是模板元编程技能,通过编译时范例推断,获取范例的属性或行为。例如:
  

  • 判断一个范例是否为指针。
  • 获取迭代器指向元素的范例(value_type)。
  • 判断迭代器是否支持随机访问(iterator_category)。
  核心头脑:通过模板特化,为不同范例提供同一的接口。
  
  value_type 的作用在泛型算法中声明临时变量或容器时,需要知道元素范例:
  1. template<typename Iterator>
  2. void print_element(Iterator it) {
  3.     typename std::iterator_traits<Iterator>::value_type val = *it;
  4.     std::cout << val;
  5. }
  6. std::vector<int>::iterator::value_type  // int
  7. std::list<double>::iterator::value_type // double
复制代码
std::distance 用于计算两个迭代器之间的间隔,当时间复杂度依赖迭代器范例:
  

  • 随机访问迭代器:直接计算 end - begin,时间复杂度 ​O(1)
  • 其他迭代器:逐个递增直到到达终点,时间复杂度 ​O(n)
  
2、C++20中的std::ranges如何依赖迭代器?

 ranges库通过概念(Concepts)​束缚迭代器范例,例如:
  1. template<std::random_access_iterator Iter>
  2. void my_sort(Iter begin, Iter end);
复制代码
同时引入sentinel(哨兵)答应迭代器与竣事标记范例不同(如nullptr作为链表竣事标记)
   概念(Concepts)​ 是 C++20 引入的模板参数束缚机制,用于明白要求模板参数必须满足的接口或行为。你可以把它想象成一种“编译时合同”——假如范例不满足概念的要求,代码将无法编译。
  为什么要用概念?
传统的 STL 算法(如 std::sort)依赖迭代器范例,但缺乏明白的束缚。例如,假如你误将 std::list 的迭代器传给 std::sort,代码会编译,但运行时崩溃(由于 std::list 迭代器不支持随机访问)。通过概念,​错误会在编译时被捕获
  1. #include <ranges>
  2. #include <vector>
  3. #include <list>
  4. // 只有支持随机访问的迭代器才能调用此函数
  5. // std::random_access_iterator 是一个预定义概念,要求迭代器支持 +, -, [] 等操作。
  6. void my_sort(std::random_access_iterator auto begin,
  7.              std::random_access_iterator auto end) {
  8.     // 实现排序...
  9. }
  10. int main() {
  11.     std::vector<int> vec = {3, 1, 4}; // vector 的迭代器是随机访问类型
  12.     std::list<int> lst = {3, 1, 4};    // list 的迭代器是双向类型
  13.     my_sort(vec.begin(), vec.end()); // ✅ 编译通过
  14.     my_sort(lst.begin(), lst.end()); // ❌ 编译错误:双向迭代器不满足随机访问概念
  15. }
复制代码
Sentinel(哨兵):竣事标记的灵活性 
   传统 STL 的问题
竣事迭代器必须与起始迭代器范例雷同。例如,std::find(begin, end, value) 中 begin 和 end 必须是同范例迭代器。
  Sentinel 的改进
std::ranges 答应 ​迭代器与竣事标记(Sentinel)范例不同。例如,可以用一个特殊值(如 nullptr)作为链表遍历的竣事标记。
  示例:以空指针为哨兵的链表
  
  1. struct Node {
  2.     int data;
  3.     Node* next;
  4. };
  5. // 自定义迭代器(指向链表节点)
  6. struct Iterator {
  7.     Node* ptr;
  8.     int& operator*() const { return ptr->data; }
  9.     Iterator& operator++() { ptr = ptr->next; return *this; }
  10.     bool operator!=(std::nullptr_t) const { return ptr != nullptr; } // 与哨兵比较
  11. };
  12. // 遍历链表
  13. Node* head = ...; // 链表头节点
  14. for (Iterator it{head}; it != nullptr; ++it) { // ✅ 结束标记是 nullptr
  15.     std::cout << *it;
  16. }
复制代码


  • 竣事标记 nullptr 是一个哨兵,范例与迭代器不同。
  • 迭代器只需实现与哨兵的比较操作(如 operator!=),即可表现范围竣事。
  
   std::ranges 如何联合概念和哨兵?std::ranges 的算法直接接受一个 ​范围(Range)​,而范围由迭代器和哨兵界说
  1. #include <ranges>
  2. #include <vector>
  3. int main() {
  4.     std::vector<int> vec = {3, 1, 4, 1, 5};
  5.     // 传统 STL:需要 begin 和 end
  6.     std::sort(vec.begin(), vec.end());
  7.     // Ranges 风格:直接传递整个范围
  8.     std::ranges::sort(vec); // ✅ 更简洁
  9.     // 自定义范围(迭代器 + 哨兵)
  10.     // std::ranges::subrange 可以组合任意类型的迭代器和哨兵。
  11.     auto custom_range = std::ranges::subrange(
  12.         Iterator{head},  // 起始迭代器
  13.         nullptr          // 结束哨兵
  14.     );
  15.     std::ranges::for_each(custom_range, [](int x){ /*...*/ });
  16. }
复制代码
3、​如何用范例擦除实现泛型迭代器(如any_iterator)? 

  1. class AnyIterator {
  2.     struct Concept { virtual void next() = 0; /*...*/ };
  3.     template<typename Iter> struct Model : Concept { /*...*/ };
  4.     std::unique_ptr<Concept> impl;
  5. public:
  6.     template<typename Iter> AnyIterator(Iter it) : impl(new Model<Iter>(it)) {}
  7.     // 重载operator*等接口
  8. };
复制代码
  范例擦除是一种隐藏对象详细范例的技能,通过同一接口操作不同范例的数据。它的核心头脑是:
  

  • ​运行时多态:将详细范例的操作抽象为接口(如虚函数)。
  • ​范例无关性:外部代码仅依赖接口,不关心内部详细范例。
  常见应用:
  

  • std::function:可保存任何可调用对象(函数、Lambda、函数对象)。
  • std::any:可保存恣意范例的值。
  • std::shared_ptr 的删除器:隐藏资源释放的详细逻辑。
  
4、为什么STL迭代器不通过继承实现多态(如抽象基类)? 

为了制止虚函数调用开销,同时保持静态多态特性。通过模板和Traits,STL在编译期确定迭代器范例,生成高效代码。
STL(Standard Template Library)的迭代器设计依照 ​​“零开销抽象”​ 原则,即在不牺牲性能的条件下提供泛型本领。若使用继承和虚函数实现多态(如抽象基类),会带来以下问题:
4.1 性能开销:虚函数与动态绑定的代价



  • 虚函数需要通过虚表(vtable)举行动态绑定,每次调用需要额外的间接寻址(vptr → vtable → function),无法被编译器内联优化。
  • 每个对象需要存储虚表指针(vptr),对于轻量级迭代器(如原生指针或简单包装类),这会明显增加内存占用。
  • 虚函数调用可能导致缓存未命中,低落性能。
4.2 范例信息丢失与泛化本领

若迭代器继承自抽象基类,其详细范例在运行时才能确定,导致以下问题:(1)无法静态分派(static dispatch)特定操作(如 std::advance(it, n) 对随机访问迭代器的优化)。(2)无法通过迭代器标签(Iterator Tags)​ 在编译期选择最优算法实现(如 std::sort 需要随机访问迭代器)。(3)STL算法依赖迭代器的范例特性(Traits)​​(如 iterator_category、value_type),这些信息必须在编译期可知。若使用运行时多态,范例信息会被擦除,无法静态检查。
4.3 代码膨胀的衡量

固然模板可能导致代码膨胀(为不同范例生成多份代码),但:


  • 当代编译器会对雷同底层范例的模板实例化举行代码归并(如 vector<int>::iterator 和 array<int>::iterator 可能共享实现)。
  • STL迭代器通常轻量(如指针或简单包装类),模板生成的代码效率远高于虚函数动态绑定的开销。
4.4 STL迭代器的设计哲学

5、const_iterator与iterator的隐式转换

const迭代器不能隐式转为非const迭代器,但反之答应。
6、在多线程环境下,迭代器的使用需要留意什么?

假如多个线程同时访问和修改容器,可能会导致迭代器失效或数据不一致的问题。需要使用同步机制(如互斥锁)来保证线程安全。
在一个线程中使用的迭代器可能会由于另一个线程对容器的修改而失效。因此,需要确保迭代器在使用期间容器不会被修改。
  1. #include <iostream>
  2. #include <vector>
  3. #include <thread>
  4. #include <mutex>
  5. std::vector<int> vec = {1, 2, 3, 4, 5};
  6. std::mutex mtx;
  7. void printVector() {
  8.     std::lock_guard<std::mutex> lock(mtx);
  9.     for (auto it = vec.begin(); it != vec.end(); ++it) {
  10.         std::cout << *it << " ";
  11.     }
  12.     std::cout << std::endl;
  13. }
  14. void modifyVector() {
  15.     std::lock_guard<std::mutex> lock(mtx);
  16.     vec.push_back(6);
  17. }
  18. int main() {
  19.     std::thread t1(printVector);
  20.     std::thread t2(modifyVector);
  21.     t1.join();
  22.     t2.join();
  23.     return 0;
  24. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

种地

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表