【问题标题】:How does a "for each" loop in C++ know the length of an arrayC ++中的“for each”循环如何知道数组的长度
【发布时间】:2014-10-03 15:23:46
【问题描述】:

我正在查看来自http://www.cplusplus.com/doc/tutorial/arrays/ 的以下示例,但我无法弄清楚第二个 for 循环是如何工作的。 for 循环如何知道数组何时结束。如果它可以弄清楚为什么第一个循环不使用类似的方法?我的印象是无法确定数组的长度。我不确定如何调和这些概念。谢谢!

编辑:感谢所有出色的答案!

#include <iostream>
using namespace std;
int main()
{
  int myarray[3] = {10,20,30};

  for (int i=0; i<3; ++i)
    ++myarray[i];

  for (int elem : myarray)
    cout << elem << '\n';
}

【问题讨论】:

  • 它使用std::end,它接受数组并且可以使用大小,因为大小是类型的一部分。
  • 编译器知道数组的长度。
  • 数组在其类型中有大小信息。这用于基于范围的循环。参见例如如何在std::end 中获得长度。
  • 请注意,在这方面,正确的数组(对于某些 TN 键入 T[N])和指针(经常与指针混淆,并被认为是与“数组”相同)。
  • 它是语法糖,它实际上使用std::begin()std::end()

标签: c++ arrays for-loop foreach


【解决方案1】:

之所以有效,是因为for 循环有效1 使用了std::beginstd::end。反过来,它们也可以工作,因为它们专门为内置数组提供重载,如下所示:

template <class T, size_t N>
T *begin(T (&array)[N]) {
    return array;
}

template <class T, size_t N>
T *end(T (&array)[N]) {
    return array+N;
}

虽然它(显然)在最初的(1998 年)C++ 标准发布之前尚未实现,但这种技术不需要任何 C++98 中可用的语言特性。 C++11 编纂了该技术并投入使用。

由于在这种情况下,参数被指定为对数组的引用,因此只有当参数确实是数组时,类型推导才会成功。对于std::begin,还有一些版本支持其他参数类型并使用(例如)集合的begin()end() 成员(如果该类型匹配)。


1. 在这种情况下,“有效”意味着在某些情况下,基于范围的 for 循环使用 beginend,而在其他情况下则不使用。如果您对技术感兴趣,它们不会用于数组,而是直接进行类似的计算。同样,对于具有 beginend 成员的容器类型,直接使用它们。如果两者都不成立,则使用 begin(range)end(range),它们可以使用 std::begin/std::endbegin(x)/end(x) 对,由参数相关查找找到。

【讨论】:

  • 对于容器的成员 begin()end() 的使用是为 ranged-for 循环自动完成的,而不依赖于 std::begin()std::end() 重载。参见标准中的 6.5.4。
  • 我是 C++ 新手。我了解大部分情况,但我不确定 N。它是如何计算的?如果目标是找到数组的末尾,它似乎必须是数组的大小/元素的大小 - 1。这是正在发生的事情吗? ——
  • 是的,编译器知道数组的声明类型(例如,知道int a[2];ints的数组)。
【解决方案2】:

由于数组定义,编译器知道数组元素的数量。所以它使用表达式myarraymyarray + 3 来遍历数组。

实际上循环如下所示

for ( auto first = myarray, last = myarray + 3; first != last; ++first )
{
   auto elem = *first;
   cout << elem << '\n';
}

考虑到基于范围的 for 语句既不使用 std::begin() 也不使用 std::end() 作为数组,正如其他人在此处所写的那样。:)

根据 C++ 标准

——如果 _RangeT 是数组类型,begin-expr 和 end-expr 是 __range__range + __bound,其中 __bound 是数组边界。如果 _RangeT 是一个未知大小的数组或 类型不完整,程序格式错误;

【讨论】:

  • +1 表示最后一点。在 C++11 中对数组使用 std::beginstd::end 没有多大意义。在 C++14 中,它们返回 constexpr,所以它可能有意义(但似乎没有必要。)
  • @Jerry Coffin std::begin 和 std:;end 均未用于基于数组的 for 语句的范围。
【解决方案3】:

编译器知道数组的实际类型并使用标准的基于迭代器的循环,其中array(相当于array + 0)和array + length(相当于*(&amp;array + 1))分别作为范围的开始和结束。

对于非数组,它首先在给定的范围内寻找beginend,最后使用ADL寻找空闲函数。

除了使用 ADL 来查找免费函数之外,所有其他操作都将由 std::beginstd::end 同等完成,因此如果您在代码中使用 using std::begin;using std::end;,未限定名称查找将模仿范围的规则-for-循环。

来自 C++1y 的正确部分:

6.5.4 基于范围的for语句[stmt.ranged]

1 对于基于范围的for 形式的语句

for ( for-range-declaration : expression ) statement

range-init 等价于用括号括起来的expression90( expression ) 和基于范围的for 形式的语句

for ( for-range-declaration : braced-init-list ) statement

range-init 等价于braced-init-list。在每种情况下,基于范围的for 语句都等效于

{
    auto && __range = range-init;
    for ( auto __begin = begin-expr, __end = end-expr;
              __begin != __end; ++__begin ) {
        for-range-declaration = *__begin;
        statement
    }
}

其中__range__begin__end是仅为说明定义的变量,_RangeT是表达式的类型,begin-exprend-expr确定如下:

  • 如果_RangeT 是数组类型,则begin-exprend-expr 分别是__range__range + __bound,其中__bound 是数组绑定。如果_RangeT 是一个未知大小的数组或一个不完整类型的数组,则程序是非良构的;
  • 如果_RangeT 是类类型,则在类_RangeT 的范围内查找非限定ID beginend,就像通过类成员访问查找(3.4.5) 一样,如果有(或两者)找到至少一个声明,beginexprend-expr 分别是 __range.begin()__range.end()
  • 否则,begin-exprend-expr 分别是 begin(__range)end(__range),其中 beginend 在关联的命名空间 (3.4.2) 中查找。 [ 注意:不执行普通的非限定查找 (3.4.1)。 ——尾注]

2 在 for-range-declarationdecl-specifier-seq 中,每个 decl-specifier 应该是一个 type-specifierconstexpr.

【讨论】:

    【解决方案4】:

    您误以为 ranged-for 需要提前知道迭代次数(“长度”)。它没有。

    它确实需要一个终止条件,其形式为it != __end,其中__end = x.end()__end = end(x)

    当然,数组不能改变它们的大小,所以在这种情况下检测结束和知道长度是等价的(长度可以从开始、结束和指针相减得到)。

    正如 Oli 在 cmets 中提到的,数组的类型确实包含长度信息,std::end 对其参数使用数组引用,以避免丢失此信息的指针衰减。

    定义基本上是:

    namespace std
    {
        template<typename T, size_t N> 
        T* end(T (&array)[N])
        {
            return array + N;
            // or if you want to be cute
            // return 1[&array];
        }
    }
    

    当数组引用中的边界是非类型模板参数时,它是可推导出的。在编写接受数组的函数时,请利用这一点。

    (编译器实际上对数组的 end 内置了 array + N 计算,它不使用 std::end() 。但是无论如何内联后没有区别,看看你如何可以发挥与编译器相同的技巧。)

    【讨论】:

    • 那是......不像标准那样。
    • @Deduplicator: 哪个部分,__end的初始化,还是end()的定义?
    • @Deduplicator:根据 24.7,如果添加 constexprnoexcept 修饰符,这是 std::end() 的正确定义。
    • @BenVoigt 24.7 似乎是 C++11 之后的版本。我在 C++11 标准中找不到它。
    • @Deduplicator:啊,找到你的意思了。无论如何,内联后没有区别。它确实使用终止条件,而不是计数器。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-12
    • 1970-01-01
    • 1970-01-01
    • 2019-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多