主题:std::list<Node>::iterator 和 Node* 的区别 - 实践
1. 定义层面的区别:
Node*
纯粹的裸指针类型,只表示“一块内存的地址”,不知道自己属于哪个容器,也不知道这块内存是不是链表节点。std::list<Node>::iterator
STL 定义的一个迭代器类类型,语法上表现得“像指向Node的指针”,但内部通常保存的是容器节点指针和一些额外信息,用来描述:“在这条
std::list<Node>上的某一个位置”
2. 语义上的区别:
Node*:
只有“地址”这一层含义,不知道自己是不是链表节点,也不知道自己是否仍然有效(比如对象是否已被释放)。list<Node>::iterator:
绑定在具体容器实例上(逻辑上如此,debug 模式甚至会在内部记录指向哪个容器),语义是:“这个迭代器指向某个
std::list<Node>的某个元素”
很多实现里,调试模式会在以下情况直接报错或断言:
使用一个已被
erase掉的迭代器在 A 容器上的迭代器,用在 B 容器上
裸指针无法提供这种语义检查。
3. 使用层面的区别:看起来像指针,但不是指针
共同点:
*it得到Node&it->member访问成员++it/--it在容器中移动(对list是前后节点)
不同点:
Node* p = it;不允许,迭代器不能隐式转换成Node*ListIter it = nullptr;不允许,迭代器不是指针迭代器类别不同,算法可用的操作也不同,例如:
vector<int>::iterator是随机访问迭代器,可以it + 3list<int>::iterator是双向迭代器,不能it + 3,只能++it/--it
4. 实现层面的区别:多了一层节点结构
很多 std::list<T> 的内部节点类似:
template
struct ListNodeImpl {
ListNodeImpl* prev;
ListNodeImpl* next;
T value;
};
Node*:可以指向完全任何地方,比如一个被new出来的单独对象。std::list<Node>::iterator:内部通常保存的是ListNodeImpl<Node>*,operator*返回value成员的引用。
等价伪代码:
class ListIterator {
ListNodeImpl* ptr; // 指向“内部节点”
public:
Node& operator*() const { return ptr->value; }
Node* operator->() const { return &ptr->value; }
ListIterator& operator++() { ptr = ptr->next; return *this; }
ListIterator& operator--() { ptr = ptr->prev; return *this; }
};
也就是说:
裸指针:直接指向
Node
迭代器:指向“容器节点”,再通过节点找到Node
5. 失效规则上的区别
Node*:
只要指向的对象没被释放、没被移动,就一直有效。是否失效完全靠人工约束和自觉。std::list<Node>::iterator:
遵守 C++ 标准规定的“迭代器失效规则”,例如:std::listlst = {1,2,3}; auto it = lst.begin(); // 指向 1 lst.push_back(4); // 所有迭代器仍然有效 lst.erase(it); // it 失效,之后使用是 UB std::vector<int>的迭代器又是另一套规则:std::vectorv = {1,2,3}; auto it = v.begin(); // 指向 1 v.push_back(4); // 可能重新分配内存 // it 可能失效
迭代器失效规则是标准的一部分;裸指针没有这样的“规则”,失效全靠自律,一旦用错就是未定义行为,且更难排查。
6. 例子一:std::vector<int>::iterator vs int*
对 std::vector<int>:
std::vector v = {1, 2, 3};
auto it = v.begin(); // std::vector::iterator
int* p = v.data(); // int*,指向底层数组
在大多数实现中,这两者在当前时刻的底层地址是一样的(都指向第一个元素),但语义不同:
iterator是抽象层:++it,std::sort(it, v.end())等,都遵守标准
int*是纯指针:可以
p += 2,*(p+1)等一旦
vector扩容,指针全部失效;迭代器也会失效,但标准会说明哪些操作导致失效
两者可以做的事情很多重合,但 STL 算法和容器接口以迭代器为基本抽象。
7. 例子二:std::map<Key, T>::iterator
std::map<std::string, int>:
std::map mp;
mp["a"] = 1;
mp["b"] = 2;
auto it = mp.find("a");
此时 it 的类型是:
std::map::iterator
// 底层等价于:iterator -> pair
*it 的类型是 std::pair<const std::string, int>&:
it->first // key(const std::string)
it->second // value(int)
此处可以注意到:
key 是
const,防止通过迭代器修改键,破坏红黑树有序性如果直接用指针,容易不小心写出
p->first = "xxx";这种会破坏有序结构的操作
迭代器在这里的作用不仅是“指针封装”,还顺带保护了容器的不变量。
8. 例子三:手写链表 Node* vs std::list + iterator
手写链表:
struct Node {
int value;
Node* prev;
Node* next;
};
可以把若干个 Node* 串起来形成一个双向链表:
优点:完全掌控内存布局和行为,可做各种“奇技淫巧”
缺点:
所有
new/delete自己管理边界情况(空表、头删、尾删、单节点)全是手工代码
容易出现悬空指针、环、断链等隐藏 bug
与 STL 算法接口不兼容
std::list<int> + iterator:
节点管理、内存释放由容器负责
通过
iterator进行插入、删除、移动:auto it = lst.insert(pos, value); lst.erase(it); lst.splice(dstPos, lst, srcIt); // O(1) 移动节点与其它 STL 算法兼容,例如:
lst.remove_if([](int x){ return x % 2 == 0; });
迭代器的意义在这里就很明显:
在不暴露底层节点实现的前提下,提供对容器的统一访问方式。
9. 总结要点(只保留和问题直接相关的部分)
std::list<Node>::iterator 和 Node* 的差异可以压缩成几个关键点:
本质类型不同
Node*:裸指针iterator:类类型,封装了节点指针和容器语义
语义不同
Node*:只是某个对象的地址iterator:某个容器上的“位置”,属于某个std::list<Node>实例
使用接口不同
迭代器支持 STL 算法、容器成员函数(如
erase,splice),行为符合标准规定指针只是一块地址,所有操作都靠程序员约定和自律
安全与约束不同
迭代器有明确定义的“失效规则”,并在 debug 模式下有机会被检测
指针失效后继续使用不会有任何提示,只会变成未定义行为
抽象层不同
Node*关注的是内存iterator关注的是“容器中的元素访问逻辑”
在 STL 里,迭代器是“容器视角的指针”(跟那个智能指针差不多感觉,都是对象,但是用起来的形式和指针大差不差),而 Node* 是“内存视角的指针”。两者可以互相类比,但职责和抽象层次并不相同.

浙公网安备 33010602011771号