基于范围的 for 如何适用于普通数组?

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

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

它怎么知道什么时候该停下来?它是否只能处理在使用 for的相同作用域中声明的静态数组?如何在动态数组中使用这个 for

76827 次浏览

It works for any expression whose type is an array. For example:

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

For a more detailed explanation, if the type of the expression passed to the right of : is an array type, then the loop iterates from ptr to ptr + size (ptr pointing to the first element of the array, size being the element count of the array).

This is in contrast to user defined types, which work by looking up begin and end as members if you pass a class object or (if there is no members called that way) non-member functions. Those functions will yield the begin and end iterators (pointing to directly after the last element and the begin of the sequence respectively).

This question clears up why that difference exists.

It knows when to stop because it knows the bounds of static arrays.

I'm not sure what do you mean by "dynamic arrays", in any case, if not iterating over static arrays, informally, the compiler looks up the names begin and end in the scope of the class of the object you iterate over, or looks up for begin(range) and end(range) using argument-dependent lookup and uses them as iterators.

For more information, in the C++11 standard (or public draft thereof), "6.5.4 The range-based for statement", pg.145

According to the latest C++ Working Draft (n3376) the ranged for statement is equivalent to the following:

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

So it knows how to stop the same way a regular for loop using iterators does.

I think you may be looking for something like the following to provide a way to use the above syntax with arrays which consist of only a pointer and size (dynamic arrays):

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;
};

This class template can then be used to create a range, over which you can iterate using the new ranged for syntax. I am using this to run through all animation objects in a scene which is imported using a library that only returns a pointer to an array and a size as separate values.

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

This syntax is, in my opinion, much clearer than what you would get using std::for_each or a plain for loop.

I think that the most important part of this question is, how C++ knows what the size of an array is (at least I wanted to know it when I found this question).

C++ knows the size of an array, because it's a part of the array's definition - it's the type of the variable. A compiler has to know the type.

Since C++11 std::extent can be used to obtain the size of an array:

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

Of course, this doesn't make much sense, because you have to explicitly provide the size in the first line, which you then obtain in the second line. But you can also use decltype and then it gets more interesting:

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

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

Is that to read as, "Tell me what a ranged-for does (with arrays)?"

I'll answer assuming that - Take the following example using nested arrays:

int ia[3][4] = \{\{1,2,3,4},{5,6,7,8},{9,10,11,12}};


for (auto &pl : ia)

Text version:

ia is an array of arrays ("nested array"), containing [3] arrays, with each containing [4] values. The above example loops through ia by it's primary 'range' ([3]), and therefore loops [3] times. Each loop produces one of ia's [3] primary values starting from the first and ending with the last - An array containing [4] values.

  • First loop: pl equals {1,2,3,4} - An array
  • Second loop: pl equals {5,6,7,8} - An array
  • Third loop: pl equals {9,10,11,12} - An array

Before we explain the process, here are some friendly reminders about arrays:

  • Arrays are interpreted as pointers to their first value - Using an array without any iteration returns the address of the first value
  • pl must be a reference because we cannot copy arrays
  • With arrays, when you add a number to the array object itself, it advances forward that many times and 'points' to the equivalent entry - If n is the number in question, then ia[n] is the same as *(ia+n) (We're dereferencing the address that's n entries forward), and ia+n is the same as &ia[n] (We're getting the address of the that entry in the array).

Here's what's going on:

  • On each loop, pl is set as a reference to ia[n], with n equaling the current loop count starting from 0. So, pl is ia[0] on the first round, on the second it's ia[1], and so on. It retrieves the value via iteration.
  • The loop goes on so long as ia+n is less than end(ia).

...And that's about it.

It's really just a simplified way to write this:

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];

If your array isn't nested, then this process becomes a bit simpler in that a reference is not needed, because the iterated value isn't an array but rather a 'normal' value:

 int ib[3] = {1,2,3};


// short
for (auto pl : ib)
cout << pl;


// long
for (int n = 0; n != 3; ++n)
cout << ib[n];

Some additional information

What if we didn't want to use the auto keyword when creating pl? What would that look like?

In the following example, pl refers to an array of four integers. On each loop pl is given the value ia[n]:

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

And... That's how it works, with additional information to brush away any confusion. It's just a 'shorthand' for loop that automatically counts for you, but lacks a way to retrieve the current loop without doing it manually.

Some sample code to demonstrate the difference between arrays on Stack vs arrays on Heap


/**
* 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
}