begin()和end()是范围for循环唯一识别的入口,要求返回支持operator!=、operator*、operator++的类型,并需提供const重载以支持const遍历。

为什么 begin() 和 end() 是关键入口
因为 C++ 范围 for 循环(for (auto& x : obj))底层只认这两个函数,不依赖继承、接口或宏。只要你的类提供公开的 begin() 和 end() 成员函数(或友元/非成员重载),且返回类型支持 operator!=、operator*、operator++,就能被遍历。
常见错误是只实现了 operator[] 或写了个 get_iterator() —— 这些名字编译器根本不会查。
-
begin()必须返回可解引用、可自增、可比较的迭代器类型(可以是裸指针、std::vector::iterator,也可以是自定义结构) - 返回值类型不必叫
iterator,但语义必须匹配:比如const T*对应const T*,std::list<int>::iterator</int>对应同类型 - 如果你的类支持 const 遍历,必须同时提供
const_begin()/const_end()或 const 重载版本,否则for (const auto& x : obj)会编译失败
用裸指针实现最简可行版
适合内部数据连续存储(如封装了 std::array、std::vector 或原始 T* + size_t 的类),零开销、无额外依赖。
假设你有个类封装了固定大小数组:
立即学习“C++免费学习笔记(深入)”;
class MyData {
int data_[10];
public:
int* begin() { return data_; }
int* end() { return data_ + 10; }
const int* begin() const { return data_; }
const int* end() const { return data_ + 10; }
};注意:两个 const 版本不能省略,否则 const 对象无法参与范围 for;返回类型必须严格匹配(int* vs const int*),混用会导致编译错误。
用 std::vector 成员时别直接暴露其迭代器
如果类里存的是 std::vector<T> vec_,很多人会写 return vec_.begin(); —— 表面能用,但有隐患:
- 用户拿到的迭代器生命周期绑定到内部
vec_,一旦类发生移动(move)、重新分配或内部清空,迭代器立即失效 - 如果你未来想把
vec_换成std::deque或自定义存储,所有外部迭代器使用点都要改 - 更严重的是:它泄露了实现细节,破坏封装;用户可能误以为能对返回的迭代器调用
vec_.erase(it),实际根本不行
正确做法是转发调用,但保持类型稳定:
class Container {
std::vector<int> data_;
public:
auto begin() { return data_.begin(); }
auto end() { return data_.end(); }
auto begin() const { return data_.cbegin(); }
auto end() const { return data_.cend(); }
};这里用 auto 让编译器推导真实类型,既避免手写冗长类型名,又保证与底层容器一致;cbegin()/cend() 确保 const 正确性。
自定义迭代器要小心 operator!= 和 operator++ 的行为
当你需要非连续存储(如树、哈希表)或特殊遍历顺序(如反向、过滤)时,就得手写迭代器类。最容易踩的坑不是功能,而是语义违规:
- 标准要求
it != end判断必须在每次循环前执行,所以operator!=必须能安全比较“结束哨兵”和任意有效迭代器,不能抛异常、不能访问已释放内存 -
operator++必须是前置形式(++it),且返回*this的引用;后置it++不是必须的,范围 for 根本不用它 - 如果迭代器持有裸指针或引用,务必确保所指向对象的生命周期长于迭代器本身——这点在返回局部变量迭代器时极易出错
复杂点在于:不同容器对“相等性”的定义不同(比如 std::unordered_map 迭代器比较的是桶位置+节点指针,不是 key 值),但只要你只用于自家类的 begin()/end(),按自己数据结构定义清楚就行。


















