MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ STL 迭代器 begin 的常量迭代特性

2021-09-187.4k 阅读

C++ STL 迭代器 begin 的常量迭代特性概述

在 C++ 标准模板库(STL)中,迭代器是一种强大的工具,它提供了一种通用的方式来遍历容器中的元素。begin 函数是容器类的一个成员函数,用于返回指向容器中第一个元素的迭代器。而 begin 的常量迭代特性,即 cbegin 函数,返回的是一个常量迭代器,这意味着通过该迭代器不能修改容器中的元素。

迭代器基础回顾

在深入探讨 begin 的常量迭代特性之前,先来回顾一下迭代器的基本概念。迭代器是一种行为类似指针的对象,它提供了访问容器元素的方式。在 C++ STL 中,不同类型的容器(如序列容器 std::vectorstd::list,关联容器 std::mapstd::set 等)都支持迭代器。

例如,对于 std::vector 容器:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    // 使用迭代器遍历 vector
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

在上述代码中,vec.begin() 返回一个指向 vec 中第一个元素的迭代器 it,通过 ++it 可以逐个访问容器中的元素,直到 it 等于 vec.end()vec.end() 返回的迭代器指向容器末尾元素的下一个位置。

常量迭代器的重要性

常量迭代器在很多场景下都具有重要意义。它主要用于在遍历容器时,确保不会意外修改容器中的元素。这在一些需要只读访问容器内容的函数中非常有用,例如:

void printVector(const std::vector<int>& vec) {
    for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
}

printVector 函数中,参数是一个常量引用 const std::vector<int>& vec,这意味着函数不会修改传入的 vector。使用 vec.cbegin()vec.cend() 来获取常量迭代器,进一步确保在遍历过程中不会对容器元素进行修改。

begincbegin 的区别

  1. begin:对于非 const 容器,begin 返回的是一个普通迭代器,通过这个迭代器可以读取和修改容器中的元素。例如:
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
*it = 10; // 合法,通过普通迭代器修改元素
  1. cbegin:无论是 const 还是非 const 容器,cbegin 都返回一个常量迭代器。对于 const 容器,begincbegin 的行为相同,都返回常量迭代器。例如:
const std::vector<int> constVec = {4, 5, 6};
auto constIt1 = constVec.begin(); // 常量迭代器
auto constIt2 = constVec.cbegin(); // 常量迭代器,与 constIt1 相同
// *constIt1 = 20; // 非法,常量迭代器不能修改元素

对于非 const 容器,cbegin 返回的常量迭代器也不能用于修改元素:

std::vector<int> nonConstVec = {7, 8, 9};
auto constIt3 = nonConstVec.cbegin();
// *constIt3 = 30; // 非法,通过 cbegin 获取的常量迭代器不能修改元素

关联容器中的 begincbegin

  1. std::mapstd::map 是一种键值对存储的关联容器,有序存储。begincbegin 的特性与序列容器类似。
#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}};
    // 使用普通迭代器遍历
    for (auto it = myMap.begin(); it != myMap.end(); ++it) {
        std::cout << it->first << ": " << it->second << std::endl;
    }
    // 使用常量迭代器遍历
    for (auto cit = myMap.cbegin(); cit != myMap.cend(); ++cit) {
        std::cout << cit->first << ": " << cit->second << std::endl;
    }
    return 0;
}

在上述代码中,myMap.begin() 返回的普通迭代器可以用于修改键值对,而 myMap.cbegin() 返回的常量迭代器不能修改。需要注意的是,std::map 中的键是常量,即使通过普通迭代器也不能修改键,只能修改值。 2. std::setstd::set 是一种无序的关联容器,存储唯一元素。同样,begincbegin 分别返回普通迭代器和常量迭代器。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {1, 2, 3};
    // 使用普通迭代器遍历
    for (auto it = mySet.begin(); it != mySet.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    // 使用常量迭代器遍历
    for (auto cit = mySet.cbegin(); cit != mySet.cend(); ++cit) {
        std::cout << *cit << " ";
    }
    std::cout << std::endl;
    return 0;
}

std::set 中,由于元素是唯一且有序(对于 std::set 是按升序)的,无论是普通迭代器还是常量迭代器,都不能直接修改元素的值。如果要修改 std::set 中的元素,需要先删除原元素,再插入新元素。

容器适配器中的 begincbegin

  1. std::stackstd::stack 是一种后进先出(LIFO)的容器适配器,它没有 begincbegin 成员函数,因为栈这种数据结构不支持遍历。栈的操作主要是 push(压入元素)、pop(弹出元素)和 top(获取栈顶元素)。
  2. std::queuestd::queue 是一种先进先出(FIFO)的容器适配器,同样没有 begincbegin 成员函数,因为队列的操作主要围绕 push(插入元素到队尾)、pop(从队头移除元素)和 front(获取队头元素)、back(获取队尾元素),不支持遍历。

自定义容器与 begincbegin

当我们自定义一个容器类时,如果希望它能像 STL 容器一样支持迭代器遍历,就需要提供 begincbegin 成员函数。例如,定义一个简单的自定义容器类 MyContainer

#include <iostream>
#include <memory>

template <typename T>
class MyContainer {
private:
    std::unique_ptr<T[]> data;
    size_t size_;

public:
    MyContainer(size_t size) : size_(size), data(std::make_unique<T[]>(size)) {}

    // 普通迭代器
    T* begin() {
        return data.get();
    }

    T* end() {
        return data.get() + size_;
    }

    // 常量迭代器
    const T* cbegin() const {
        return data.get();
    }

    const T* cend() const {
        return data.get() + size_;
    }
};

int main() {
    MyContainer<int> myCont(5);
    for (size_t i = 0; i < 5; ++i) {
        myCont.begin()[i] = i;
    }
    for (auto it = myCont.cbegin(); it != myCont.cend(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;
    return 0;
}

在上述代码中,MyContainer 类实现了 begincbegin 函数,分别返回普通迭代器和常量迭代器。这样,MyContainer 类的对象就可以像 STL 容器一样使用迭代器进行遍历。

基于范围的 for 循环与 begincbegin

C++11 引入的基于范围的 for 循环语法糖,在遍历容器时非常方便。实际上,基于范围的 for 循环会隐式调用容器的 beginend 函数(对于 const 对象会调用 cbegincend)。例如:

std::vector<int> vec = {1, 2, 3};
for (int num : vec) {
    std::cout << num << " ";
}
std::cout << std::endl;

const std::vector<int> constVec = {4, 5, 6};
for (int num : constVec) {
    std::cout << num << " ";
}
std::cout << std::endl;

在第一个基于范围的 for 循环中,对于非 constvec,隐式调用 vec.begin()vec.end()。在第二个循环中,对于 constconstVec,隐式调用 constVec.cbegin()constVec.cend()

与算法结合使用

STL 提供了丰富的算法,这些算法通常通过迭代器来操作容器元素。当使用常量迭代器时,可以确保算法不会修改容器元素。例如,使用 std::for_each 算法来打印 std::vector 中的元素:

#include <iostream>
#include <vector>
#include <algorithm>

void print(int num) {
    std::cout << num << " ";
}

int main() {
    std::vector<int> vec = {1, 2, 3};
    std::for_each(vec.cbegin(), vec.cend(), print);
    std::cout << std::endl;
    return 0;
}

在上述代码中,std::for_each 算法接受两个迭代器作为参数,这里使用 vec.cbegin()vec.cend() 常量迭代器,保证在遍历过程中不会修改 vec 中的元素。

迭代器失效问题与常量迭代器

迭代器失效是指迭代器不再指向有效的元素位置。在使用普通迭代器时,容器的某些操作(如插入、删除元素)可能会导致迭代器失效。而常量迭代器在容器内容不发生改变的情况下,不会失效。例如:

std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 普通迭代器 it 失效
// *it = 5; // 未定义行为,因为 it 失效

const std::vector<int> constVec = {1, 2, 3};
auto constIt = constVec.cbegin();
// constVec.push_back(4); // 非法,const 容器不能修改
// 常量迭代器 constIt 在 constVec 内容不变时始终有效

由于常量迭代器用于只读访问,并且常量容器不能被修改,所以常量迭代器在常量容器的生命周期内通常不会失效,除非常量容器本身被销毁。

性能考虑

从性能角度来看,begincbegin 本身的返回操作通常是非常高效的,它们只是返回指向容器起始位置的指针或类似指针的对象。然而,在使用迭代器进行遍历操作时,常量迭代器可能在某些编译器优化下会有更好的性能表现。因为编译器知道通过常量迭代器不会修改容器元素,可能会进行一些额外的优化,例如对内存访问的优化。但这种性能差异在大多数情况下并不明显,除非是在非常高性能敏感的代码中。

例如,在一个简单的遍历求和操作中:

#include <iostream>
#include <vector>
#include <numeric>

int sumUsingBegin(const std::vector<int>& vec) {
    return std::accumulate(vec.begin(), vec.end(), 0);
}

int sumUsingCbegin(const std::vector<int>& vec) {
    return std::accumulate(vec.cbegin(), vec.cend(), 0);
}

在实际应用中,sumUsingBeginsumUsingCbegin 的性能差异很难察觉,因为现代编译器在优化时会尽量减少这种差异。但从理论上来说,sumUsingCbegin 可能会有一些微小的优化机会,因为它明确表示不会修改容器元素。

总结常量迭代特性的应用场景

  1. 只读数据处理:当函数只需要读取容器中的数据,而不需要修改时,使用常量迭代器可以防止意外修改数据,提高代码的安全性。例如,在数据统计、数据展示等功能中,使用常量迭代器可以确保数据的完整性。
  2. 传递容器给函数:在将容器作为参数传递给函数时,如果函数不需要修改容器,使用常量迭代器可以明确函数的意图,同时也避免了在函数内部意外修改容器的风险。
  3. 与 STL 算法结合:许多 STL 算法只需要对容器进行只读访问,使用常量迭代器可以与这些算法更好地配合,并且确保算法不会对容器进行不必要的修改。

通过深入理解 C++ STL 迭代器 begin 的常量迭代特性,开发者可以编写出更加健壮、安全且高效的代码,在处理容器数据时能够更好地控制对数据的访问和修改。无论是在简单的程序还是复杂的大型项目中,合理运用常量迭代器都能带来诸多好处。