- >第一个/第二个到空的地图迭代器开始

DsC*_*Cpp 4 c++ iterator


我无法理解这个片段中发生了什么.
映射引用声明"如果容器为空,则不应取消引用返回的迭代器值."
但那怎么样some_map->begin()->second?在一张空地图上.
我认为它会无效,但此代码打印为'0'.任何人都可以选择为什么?

int main()
{
 map<int,int> a;

 printf("%d",a.begin()->second);
 return 1;
}
Run Code Online (Sandbox Code Playgroud)

谢谢!

Som*_*ude 5

这个std::map::begin参考

如果容器为空,则返回的迭代器将等于 end()

然后看这个std::map::end参考:

该元素充当占位符; 尝试访问它会导致未定义的行为.

[强调我的]

您正在经历一种称为未定义行为的事情,而且实际上就是它的所有内容.只是不要做那样愚蠢的事.


Arn*_*gel 1

正如其他人指出的那样,您的代码具有未定义的行为,并且打印 0 与未定义的行为 \xe2\x80\x93 一致,就像任何其他行为一样。

\n\n

但是,您可能对为什么会发生这种特定行为(而不是段错误)感兴趣。一个可能的原因是您的map实现使用了虚拟节点。一般来说,这些对于基于节点的容器的实现来说实际上非常流行。

\n\n

例如,map迭代器可以是围绕节点指针(例如,指向平衡二叉搜索树中的节点)的薄包装器。现在,实现结束迭代器的明显方法是在这里使用空指针。operator --()然而,如果没有指向容器的指针 \xe2\x80\x93 ,更糟糕的是,没有一个附加分支,就不可能在结束迭代器上实现,因为现在您必须检查对此运算符的每次调用是否是节点指针为 null (我稍微缩写了代码示例,省略了不相关的模板参数):

\n\n
template<typename U, typename V>\ntypename map<U,V>::iterator &map<U,V>::iterator::operator --()\n{\n    if ( node_ != nullptr )\n    {\n        // go to predecessor\n        if ( node_->leftChild_ != nullptr )\n        {\n            for ( node_ = node_->leftChild_; node_->rightChild_ != nullptr; node_ = node_->rightChild_ );\n        }\n        else\n        {\n            node *n;\n            do\n            {\n                n = node_;\n                node_ = node->parent_;\n                // may not go before begin()\n                assert( node_ != nullptr );\n            } while ( n == node_->leftChild_ );\n        }\n    }\n    else\n    {\n        // point to last node - map must not be empty\n        assert( container_->root_ != nullptr );\n        for ( node_ = container_->root_; node_->rightChild_ != nullptr; node = node->rightChild_ );\n    }\n    return *this;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是,如果您有一个始终是树中最右边节点的虚拟节点,并且将结束迭代器实现为指向虚拟节点的指针的包装器,则空性检查以及第二个分支将变得多余并可以删除。同样,指针的使用container_变得完全没有必要,因此可以从 中删除指针本身iterator,从而节省空间并降低创建和复制迭代器的成本。(实际上,自 C++11 起,甚至不再允许使用此“容器指针”,因为容器可能会被移动,并且根据定义,这不会使迭代器无效。有效的解决方案仍然会更加复杂。次要更新:这可能一直是被禁止的,因为容器可以在不使迭代器无效的情况下进行交换。)

\n\n

现在,这解释了为什么end()迭代器实际上可能指向“真实”内存。因此,如果operator *()实现为:

\n\n
template<typename U, typename V>\ntypename map<U,V>::reference map<U,V>::iterator::operator *() const\n{\n    // cannot dereference end iterator\n    assert( hasSuccessor(node_) );\n    return *node_;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

如上所述,我在这里添加了调试断言。如何hasSuccessor实施?在最坏的情况下,它实际上必须一路上升到顶端,看看node_它的祖先是否有一个合适的孩子。此检查具有令人望而却步的运行时间成本\xe2\x80\x93,即标准实际上禁止它。虽然其平均复杂度仅为 O(1),但最坏情况复杂度为 O(log N),但即使在该操作的最坏情况下,标准也要求 \xce\x98(1) 。当然,在调试版本中,谁在乎呢?还有其他方法可以实现检查,例如在节点中的某处设置“虚拟位”。要点是无论如何都不需要这样的检查。因此,您尝试取消引用迭代器可能会为您提供对虚拟节点的引用,并且由于它是“真实”分配的内存,因此您可以继续读取映射值。请注意,就标准而言,这并不会使迭代器“可取消引用”,它只是意味着您既不会得到段错误,也不会得到程序终止作为可能随时更改的实现细节,恕不另行通知。

\n\n

现在,可能还有一个问题是为什么你会得到 0,而不是 -135,或者一些“随机”8-9 位数字。map不允许调用任何默认构造函数,例如,有副作用的构造函数除非您使用operator [],否则甚至不允许假设您的映射类型默认可构造的。然而,还有许多其他原因可能导致您得到零分:

\n\n
    \n
  • 对于没有带有副作用的构造函数的类型,特别是对于诸如 之类的普通可构造类型intmap确实可以像 中那样自由地初始化它们int{},例如通过放置new
  • \n
  • 该实现还允许memset在使用前将虚拟节点清零。这可能是确保指针为零的最简单方法。
  • \n
  • 然而,最可能的解释是,您没有在测试程序中分配和释放类似大小的其他内存,因此分配器从进程从操作系统接收的页面中为您提供了一些“新鲜”内存。任何多用户操作系统都会确保分配给进程的内存不能包含来自可能占用内存的先前进程的信息。通常的方法是操作系统将memset整个页面清零,然后再让进程访问它们。
  • \n
\n