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

时间:2018-02-06 11:37:49

标签: c++ iterator


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

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

 printf("%d",a.begin()->second);
 return 1;
}

谢谢!

3 个答案:

答案 0 :(得分:5)

来自this std::map::begin reference

  

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

然后看this std::map::end reference

  

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

[强调我的]

您正在体验名为undefined behavior的内容,这就是它的全部内容。只是不要做那样愚蠢的事。

答案 1 :(得分:1)

无效。由于地图为空,因此->second的行为未定义只是因为地图为空。

零的打印是这种未定义行为的可能表现,但你不应该依赖它。

答案 2 :(得分:0)

正如其他人所指出的,您的代码具有未定义的行为,而打印0与未定义的行为一致 - 就像任何其他行为一样。

但是,您可能对此特定行为发生的原因感兴趣,而不是例如段错误。可能的原因是您的map实现使用虚拟节点。实际上,这些通常非常适用于基于节点的容器的实现。

例如,map迭代器可以是节点指针周围的薄包装器(例如,平衡二叉搜索树中的节点)。现在,实现end迭代器的显而易见的方法是在这里使用空指针。但是,如果没有例如,那么就不可能在结束迭代器上实现operator --()。一个指向容器的指针 - 更糟糕的是,一个额外的分支,因为现在你必须检查每次调用这个操作符是否节点指针为空(我通过省略不相关的模板参数来缩略代码示例) ):

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

但是,如果您的虚拟节点始终是树中最右边的节点,并且实现了结束迭代器作为指向虚节点的指针的包装器,那么空值检查以及第二个分支变得多余并且可以被删除。同样,使用container_指针变得完全没必要,因此可以从iterator中删除指针本身,从而节省空间并降低制作和复制迭代器的成本。 (实际上,由于C ++ 11不再允许使用这个“容器指针”,因为容器可能会被移动,而且根据定义,这不会使迭代器失效。有效的解决方案仍然会更加复杂。 MINOR UPDATE :这可能是被禁止的,因为容器可以交换而不会使迭代器失效。)

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

template<typename U, typename V>
typename map<U,V>::reference map<U,V>::iterator::operator *() const
{
    // cannot dereference end iterator
    assert( hasSuccessor(node_) );
    return *node_;
}

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

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

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