Arrays and Loops
Just as with vector, the real advantage of arrays is that you can automate the processing of a collection of related elements, like the grades for all the students in a class.
However, because arrays are lower-level structures, processing them is not quite as convenient. There are several ways to use loops to traverse an array.
- Calculate the number of elements in the array and use that as a limit on a traditional counter-controlled for or while loop.
- Use a sentinel value stored in the array to mark its end.
- Use a pair of pointers: one to the first element in the array, and one to the address right past the end of the array. These are called iterator-based loops.
- Use the C++ 11 range-based for loop.
Inside a function, only the first three are meaningful. You cannot use the range-based for loop on an array name after it has decayed to a pointer.
The range-based for Loop
You may, however, use the range-based for loop on arrays, provided that the array definition is in scope. Here's an example:
int a[] = {...};
for (int e : a)
{
cout <≪ e << " ";
}
Counter-Controlled Loops
Inside a function, most commonly, you'll calculate the size of an array at the point where it is declared, and then pass that size as an additional argument when you call the function.
For instance, here is a function which sums the elements in a vector:
int sum(const vector<int>& v)
{
int sum = 0;
for (size_t i = 0, len = v.size(); i < len; ++i)
sum += v.at(i);
return sum;
}
Notice that the function only requires one argument, since the vector "carries" its size along with it. With an array, you'd need to write the same function like this:
int sum(const int array[], size_t len)
{
int sum = 0;
for (size_t i = 0; i < len; ++i)
sum += array[i];
return sum;
}
Unlike string and vector, arrays have no built-in size() member function. And, because array is really a pointer, you can't use the sizeof "trick" inside the function. You must pass the length as an argument when calling the function. Note, also, that unlike vector you have no range-checked at() function. You must use the built-in subscript operator.
Sentinel Loops
If an array contains a special value marking the end of the data, then you can use a sentinel loop to process the array. In the long-ago data-processing days, for instance, an array of positive values would contain a -1 as a terminator or sentinel.
One advantage of the sentinel technique, is that it allows you to write functions that process variable amounts of data. (Later, we'll look at partially-filled arrays, which are a more general solution to this problem.)
Here's an example using a sentinel loop, which calculates the average grade on a test:
double average(const double grades[])
{
double sum = 0;
int count = 0;
for (size_t i = 0; grades[i] >= 0; ++i)
{
sum += grades[i];
count++;
}
return sum / count;
}
The disadvantage of this technique is that your function needs to trust that the array has been properly terminated, which is really a security hole. This technique is not used that widely for numeric arrays. However, C-style strings use a special sentinel (called the NUL byte) to mark the end of the string, and all C-string functions employ sentinel loops.
Iterator Loops
Another iteration approach is to pass the address of the first element you want to process, and the address of an imaginary element that is just past the last element you want to process. This is known as the range-based or iterator approach to passing array parameters. Click this link to run the function shown below.
double sum(const double* beg, const double* end)
{
double result = 0.0;
while (beg != end) { result += *beg++;}
return result;
}
In the iterator-based approach the caller passes the array (the address of the element at index zero) and, the address of the imaginary element just past the end of the range which you want to process.
When you call a function, such as sum(), which uses an iterator loop, you can calculate the pointers by using address arithmetic, like this:
int main()
{
double a[] = {1.5, 2.5, 3.5, 2.75, 1.75};
cout << "sum->" << sum(a, a + 5) << endl; // 5 elements
return 0;
}
Starting in C++11 you can automatically calculate these two pointers by using the functions begin() and end() like this:
double array[] = {.....};
cout << sum(begin(array), end(array)) << endl;
A C++ Idiom
Knowing pointer arithmetic helps you understand one of the most common idiomatic constructions in C++, which you'll find on line 4 of the previous example:
while (beg != end) { result += *beg++; }
Here's how to decode the expression *beg++:
- The * operator and the ++ operator compete for the operand beg. Because unary operators in C++ are right-associative, the ++ takes precedence over the *. The compiler interprets this as: *(beg++).
- The postfix ++ operator increments the value of beg so that it points to the next element, but returns the address that beg was pointing to prior to the increment operation. Since beg is a pointer, the increment operation uses pointer arithmetic; adding 1 to the address inside of beg creates a pointer to the next element in the array.
- If beg originally pointed to a[0], the increment causes it to point to a[1]. The address that is used for dereferencing *, is the address value it contained before the increment.
Thus, the expression *beg++ has the following meaning in English:
Dereference the pointer beg and return as an lvalue the object to which it currently points. As a side effect, increment the value of beg so that, if the original lvalue was an element in an array, the new value of beg points to the next element in that array.