Arrays have two special properties that affect how we define and use functions that operate on arrays: We cannot copy an array (§ 3.5.1, p. 114), and when we use an array it is (usually) converted to a pointer (§ 3.5.3, p. 117). Because we cannot copy an array, we cannot pass an array by value. Because arrays are converted to pointers, when we pass an array to a function, we are actually passing a pointer to the array’s first element.
Even though we cannot pass an array by value, we can write a parameter that looks like an array:
Exercise 6.16: The following function, although legal, is less useful than it might be. Identify and correct the limitation on this function:
bool is_empty(string& s) { return s.empty(); }
Exercise 6.17: Write a function to determine whether a string
contains any capital letters. Write a function to change a string
to all lowercase. Do the parameters you used in these functions have the same type? If so, why? If not, why not?
Exercise 6.18: Write declarations for each of the following functions. When you write these declarations, use the name of the function to indicate what the function does.
(a) A function named compare
that returns a bool
and has two parameters that are references to a class named matrix
.
(b) A function named change_val
that returns a vector<int>
iterator and takes two parameters: One is an int
and the other is an iterator for a vector<int>
.
Exercise 6.19: Given the following declarations, determine which calls are legal and which are illegal. For those that are illegal, explain why.
double calc(double);
int count(const string &, char);
int sum(vector<int>::iterator, vector<int>::iterator, int);
vector<int> vec(10);
(a) calc(23.4, 55.1);
(b) count("abcda", 'a'),
(c) calc(66);
(d) sum(vec.begin(), vec.end(), 3.8);
Exercise 6.20: When should reference parameters be references to const
? What happens if we make a parameter a plain reference when it could be a reference to const
?
// despite appearances, these three declarations of print are equivalent
// each function has a single parameter of type const int*
void print(const int*);
void print(const int[]); // shows the intent that the function takes an array
void print(const int[10]); // dimension for documentation purposes (at best)
Regardless of appearances, these declarations are equivalent: Each declares a function with a single parameter of type const int*
. When the compiler checks a call to print
, it checks only that the argument has type const int*
:
int i = 0, j[2] = {0, 1};
print(&i); // ok: &i is int*
print(j); // ok: j is converted to an int* that points to j[0]
If we pass an array to print
, that argument is automatically converted to a pointer to the first element in the array; the size of the array is irrelevant.
As with any code that uses arrays, functions that take array parameters must ensure that all uses of the array stay within the array bounds.
Because arrays are passed as pointers, functions ordinarily don’t know the size of the array they are given. They must rely on additional information provided by the caller. There are three common techniques used to manage pointer parameters.
The first approach to managing array arguments requires the array itself to contain an end marker. C-style character strings (§ 3.5.4, p. 122) are an example of this approach. C-style strings are stored in character arrays in which the last character of the string is followed by a null character. Functions that deal with C-style strings stop processing the array when they see a null character:
void print(const char *cp)
{
if (cp) // if cp is not a null pointer
while (*cp) // so long as the character it points to is not a null character
cout << *cp++; // print the character and advance the pointer
}
This convention works well for data where there is an obvious end-marker value (like the null character) that does not appear in ordinary data. It works less well with data, such as int
s, where every value in the range is a legitimate value.
A second technique used to manage array arguments is to pass pointers to the first and one past the last element in the array. This approach is inspired by techniques used in the standard library. We’ll learn more about this style of programming in Part II. Using this approach, we’ll print the elements in an array as follows:
void print(const int *beg, const int *end)
{
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element
// and advance the pointer
}
The while
uses the dereference and postfix increment operators (§ 4.5, p. 148) to print the current element and advance beg
one element at a time through the array. The loop stops when beg
is equal to end
.
To call this function, we pass two pointers—one to the first element we want to print and one just past the last element:
int j[2] = {0, 1};
// j is converted to a pointer to the first element in j
// the second argument is a pointer to one past the end of j
print(begin(j), end(j)); // begin and end functions, see § 3.5.3 (p. 118)
This function is safe, as long as the caller correctly calculates the pointers. Here we let the library begin
and end
functions (§ 3.5.3, p. 118) provide those pointers.
A third approach for array arguments, which is common in C programs and older C++ programs, is to define a second parameter that indicates the size of the array. Using this approach, we’ll rewrite print
as follows:
// const int ia[] is equivalent to const int* ia
// size is passed explicitly and used to control access to elements of ia
void print(const int ia[], size_t size)
{
for (size_t i = 0; i != size; ++i) {
cout << ia[i] << endl;
}
}
This version uses the size
parameter to determine how many elements there are to print. When we call print
, we must pass this additional parameter:
int j[] = { 0, 1 }; // int array of size 2
print(j, end(j) - begin(j));
The function executes safely as long as the size passed is no greater than the actual size of the array.
const
Note that all three versions of our print
function defined their array parameters as pointers to const
. The discussion in § 6.2.3 (p. 213) applies equally to pointers as to references. When a function does not need write access to the array elements, the array parameter should be a pointer to const
(§ 2.4.2, p. 62). A parameter should be a plain pointer to a nonconst
type only if the function needs to change element values.
Just as we can define a variable that is a reference to an array (§ 3.5.1, p. 114), we can define a parameter that is a reference to an array. As usual, the reference parameter is bound to the corresponding argument, which in this case is an array:
// ok: parameter is a reference to an array; the dimension is part of the type
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
The parentheses around &arr
are necessary (§ 3.5.1, p. 114):
f(int &arr[10]) // error: declares arr as an array of references
f(int (&arr)[10]) // ok: arr is a reference to an array of ten ints
Because the size of an array is part of its type, it is safe to rely on the dimension in the body of the function. However, the fact that the size is part of the type limits the usefulness of this version of print
. We may call this function only for an array of exactly ten int
s:
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); // error: argument is not an array of ten ints
print(j); // error: argument is not an array of ten ints
print(k); // ok: argument is an array of ten ints
We’ll see in § 16.1.1 (p. 654) how we might write this function in a way that would allow us to pass a reference parameter to an array of any size.
Recall that there are no multidimensional arrays in C++ (§ 3.6, p. 125). Instead, what appears to be a multidimensional array is an array of arrays.
As with any array, a multidimensional array is passed as a pointer to its first element (§ 3.6, p. 128). Because we are dealing with an array of arrays, that element is an array, so the pointer is a pointer to an array. The size of the second (and any subsequent) dimension is part of the element type and must be specified:
// matrix points to the first element in an array whose elements are arrays of ten ints
void print(int (*matrix)[10], int rowSize) { /* . . . */ }
declares matrix
as a pointer to an array of ten int
s.
Again, the parentheses around *matrix
are necessary:
int *matrix[10]; // array of ten pointers
int (*matrix)[10]; // pointer to an array of ten ints
We can also define our function using array syntax. As usual, the compiler ignores the first dimension, so it is best not to include it:
// equivalent definition
void print(int matrix[][10], int rowSize) { /* . . . */ }
declares matrix
to be what looks like a two-dimensional array. In fact, the parameter is a pointer to an array of ten int
s.