对于普通数组,基于范围的for是如何工作的?

How does the range-based for work for plain arrays?

本文关键字:for 何工作 工作 范围 数组 于普通 于范围      更新时间:2023-10-16

在c++ 11中,您可以使用基于范围的for,它充当其他语言的foreach。它甚至可以使用普通的C数组:

int numbers[] = { 1, 2, 3, 4, 5 };
for (int& n : numbers) {
    n *= 2;
}

它怎么知道什么时候停止?它是否只与在for使用的相同范围内声明的静态数组一起工作?您将如何使用这个for与动态数组?

它适用于任何类型为数组的表达式。例如:

int (*arraypointer)[4] = new int[1][4]{{1, 2, 3, 4}};
for(int &n : *arraypointer)
  n *= 2;
delete [] arraypointer;

更详细的解释是,如果传递给:右侧的表达式类型是数组类型,则循环从ptr迭代到ptr + size (ptr指向数组的第一个元素,size是数组的元素计数)。

这与用户定义类型相反,如果传递类对象或(如果没有以这种方式调用成员)非成员函数,则通过查找beginend作为成员来工作。这些函数将产生begin和end迭代器(分别直接指向序列的最后一个元素和开始元素)。

这个问题解释了为什么存在这种差异。

我认为这个问题最重要的部分是,c++如何知道数组的大小(至少当我发现这个问题时我想知道它)。

c++知道数组的大小,因为它是数组定义的一部分——它是变量的类型。编译器必须知道类型

从c++ 11开始,std::extent可以用来获取数组的大小:

int size1{ std::extent< char[5] >::value };
std::cout << "Array size: " << size1 << std::endl;

当然,这没有多大意义,因为必须在第一行显式地提供大小,然后在第二行中获得。但你也可以使用decltype,然后它变得更有趣:

char v[] { 'A', 'B', 'C', 'D' };
int size2{ std::extent< decltype(v) >::value };
std::cout << "Array size: " << size2 << std::endl;

根据最新的c++ Working Draft (n3376), range for语句等价如下:

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

所以它知道如何像使用迭代器的常规for循环那样停止。

我想你可能正在寻找类似以下的东西,以提供一种方法来使用上述语法的数组,只有一个指针和大小(动态数组):

template <typename T>
class Range
{
public:
    Range(T* collection, size_t size) :
        mCollection(collection), mSize(size)
    {
    }
    T* begin() { return &mCollection[0]; }
    T* end () { return &mCollection[mSize]; }
private:
    T* mCollection;
    size_t mSize;
};

这个类模板可以用来创建一个范围,您可以使用语法的新的范围进行迭代。我使用它来运行场景中的所有动画对象,这些对象是使用库导入的,该库只返回指向数组的指针和大小作为单独的值。

for ( auto pAnimation : Range<aiAnimation*>(pScene->mAnimations, pScene->mNumAnimations) )
{
    // Do something with each pAnimation instance here
}

在我看来,这种语法比使用std::for_each或普通for循环要清晰得多。

它知道什么时候停止,因为它知道静态数组的边界。

我不确定你所说的"动态数组"是什么意思,在任何情况下,如果不是在静态数组上迭代,非正式地,编译器在你迭代的对象的类范围内查找beginend,或者使用依赖参数的查找查找begin(range)end(range),并将它们用作迭代器。

有关更多信息,在c++ 11标准(或其公开草案)中,"6.5.4基于范围的for语句",pg.145

基于范围的for是如何为普通数组工作的?

读作"告诉我range -for(与数组)做什么?" "

以下是使用嵌套数组的示例:
int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (auto &pl : ia)

文本版本:

ia是一个数组的数组("嵌套数组"),包含[3]数组,每个数组包含[4]值。上面的例子通过ia的主'范围' ([3])循环,因此循环[3]次。每个循环产生ia[3]个主值中的一个,从第一个开始,以最后一个结束——一个包含[4]值的数组。

  • 第一个循环:pl = {1,2,3,4} -一个数组
  • 第二个循环:pl = {5,6,7,8} -一个数组
  • 第三个循环:pl = {9,10,11,12} -一个数组
在我们解释这个过程之前,这里有一些关于数组的友好提示:
  • 数组被解释为指向其第一个值的指针-使用数组而不进行任何迭代返回第一个值的地址
  • pl 必须是引用,因为不能复制数组
  • 对于数组,当你向数组对象本身添加一个数字时,它向前移动多次并"指向"等效条目-如果n是所讨论的数字,那么ia[n]*(ia+n)相同(我们正在解引用n条目的地址),ia+n&ia[n]相同(我们正在获取数组中该条目的地址)。

  • 在每个循环中,pl被设置为ia[n]参考n从0开始等于当前循环计数。所以,第一轮plia[0],第二轮是ia[1],以此类推。它通过迭代获取值。
  • 只要ia+n小于end(ia),循环就会继续。

…就这些了。

其实就是的简化写法:

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int n = 0; n != 3; ++n)
  auto &pl = ia[n];

如果数组没有嵌套,那么这个过程就会变得简单一些,因为不需要引用,因为迭代的值不是数组,而是一个"正常"值:

 int ib[3] = {1,2,3};
 // short
 for (auto pl : ib)
   cout << pl;
 // long
 for (int n = 0; n != 3; ++n)
   cout << ib[n];

一些附加信息

如果我们不想在创建pl时使用auto关键字该怎么办?那会是什么样子?

以"pl"为array of four integers为例。在每个循环中,pl被赋值为ia[n]:

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int (&pl)[4] : ia)

…这就是它的工作原理,加上额外的信息来消除任何困惑。它只是一个自动计数的"速记"for循环,但缺乏一种不手动检索当前循环的方法。

一些示例代码来演示堆栈上的数组与堆上的数组的区别


/**
 * Question: Can we use range based for built-in arrays
 * Answer: Maybe
 * 1) Yes, when array is on the Stack
 * 2) No, when array is the Heap
 * 3) Yes, When the array is on the Stack,
 *    but the array elements are on the HEAP
 */
void testStackHeapArrays() {
  int Size = 5;
  Square StackSquares[Size];  // 5 Square's on Stack
  int StackInts[Size];        // 5 int's on Stack
  // auto is Square, passed as constant reference
  for (const auto &Sq : StackSquares)
    cout << "StackSquare has length " << Sq.getLength() << endl;
  // auto is int, passed as constant reference
  // the int values are whatever is in memory!!!
  for (const auto &I : StackInts)
    cout << "StackInts value is " << I << endl;
  // Better version would be: auto HeapSquares = new Square[Size];
  Square *HeapSquares = new Square[Size];   // 5 Square's on Heap
  int *HeapInts = new int[Size];            // 5 int's on Heap
  // does not compile,
  // *HeapSquares is a pointer to the start of a memory location,
  // compiler cannot know how many Square's it has
  // for (auto &Sq : HeapSquares)
  //    cout << "HeapSquare has length " << Sq.getLength() << endl;
  // does not compile, same reason as above
  // for (const auto &I : HeapInts)
  //  cout << "HeapInts value is " << I << endl;
  // Create 3 Square objects on the Heap
  // Create an array of size-3 on the Stack with Square pointers
  // size of array is known to compiler
  Square *HeapSquares2[]{new Square(23), new Square(57), new Square(99)};
  // auto is Square*, passed as constant reference
  for (const auto &Sq : HeapSquares2)
    cout << "HeapSquare2 has length " << Sq->getLength() << endl;
  // Create 3 int objects on the Heap
  // Create an array of size-3 on the Stack with int pointers
  // size of array is known to compiler
  int *HeapInts2[]{new int(23), new int(57), new int(99)};
  // auto is int*, passed as constant reference
  for (const auto &I : HeapInts2)
    cout << "HeapInts2 has value " << *I << endl;
  delete[] HeapSquares;
  delete[] HeapInts;
  for (const auto &Sq : HeapSquares2) delete Sq;
  for (const auto &I : HeapInts2) delete I;
  // cannot delete HeapSquares2 or HeapInts2 since those arrays are on Stack
}