7.2. Argument PassingEach parameter is created anew on each call to the function. The value used to initialize a parameter is the corresponding argument passed in the call.
7.2.1. Nonreference ParametersParameters that are plain, nonreference types are initialized by copying the corresponding argument. When a parameter is initialized with a copy, the function has no access to the actual arguments of the call. It cannot change the arguments. Let's look again at the definition of gcd:
// return the greatest common divisor
int gcd(int v1, int v2)
{
while (v2) {
int temp = v2;
v2 = v1 % v2;
v1 = temp;
}
return v1;
}
Inside the body of the while, we change the values of both v1 and v2. However, these changes are made to the local parameters and are not reflected in the arguments used to call gcd. Thus, when we call gcd(i, j) the values i and j are unaffected by the assignments performed inside gcd.
Pointer ParametersA parameter can be a pointer (Section 4.2, p. 114), in which case the argument pointer is copied. As with any nonreference type parameter, changes made to the parameter are made to the local copy. If the function assigns a new pointer value to the parameter, the calling pointer value is unchanged. Recalling the discussion in Section 4.2.3 (p. 121), the fact that the pointer is copied affects only assignments to the pointer. If the function takes a pointer to a nonconst type, then the function can assign through the pointer and change the value of the object to which the pointer points: void reset(int *ip) { *ip = 0; // changes the value of the object to which ip points ip = 0; // changes only the local value of ip; the argument is unchanged } After a call to reset, the argument is unchanged but the object to which the argument points will be 0: int i = 42; int *p = &i; cout << "i: " << *p << '\n'; // prints i: 42 reset(p); // changes *p but not p cout << "i: " << *p << endl; // ok: prints i: 0 If we want to prevent changes to the value to which the pointer points, then the parameter should be defined as a pointer to const: void use_ptr(const int *p) { // use_ptr may read but not write to *p } Whether a pointer parameter points to a const or nonconst type affects the arguments that we can use to call the function. We can call use_ptr on either an int* or a const int*; we can pass only on an int* to reset. This distinction follows from the initialization rules for pointers (Section 4.2.5, p. 126). We may initialize a pointer to const to point to a nonconst object but may not use a pointer to nonconst to point to a const object. const ParametersWe can call a function that takes a nonreference, nonconst parameter passing either a const or nonconst argument. For example, we could pass two const ints to our gcd function: const int i = 3, j = 6; int k = rgcd(3, 6); // ok: k initialized to 3 This behavior follows from the normal initialization rules for const objects (Section 2.4, p. 56). Because the initialization copies the value of the initializer, we can initialize a nonconst object from a const object, or vice versa. If we make the parameter a const nonreference type: void fcn(const int i) { /* fcn can read but not write to i */ } then the function cannot change its local copy of the argument. The argument is still passed as a copy so we can pass fcn either a const or nonconst object. What may be surprising, is that although the parameter is a const inside the function, the compiler otherwise treats the definition of fcn as if we had defined the parameter as a plain int: void fcn(const int i) { /* fcn can read but not write to i */ } void fcn(int i) { /* ... */ } // error: redefines fcn(int) This usage exists to support compatibility with the C language, which makes no distinction between functions taking const or nonconst parameters. Limitations of Copying ArgumentsCopying an argument is not suitable for every situation. Cases where copying doesn't work include:
In these cases we can instead define the parameters as references or pointers.
7.2.2. Reference ParametersAs an example of a situation where copying the argument doesn't work, consider a function to swap the values of its two arguments: // incorrect version of swap: The arguments are not changed! void swap(int v1, int v2) { int tmp = v2; v2 = v1; // assigns new value to local copy of the argument v1 = tmp; } // local objects v1 and v2 no longer exist In this case, we want to change the arguments themselves. As defined, though, swap cannot affect those arguments. When it executes, swap exchanges the local copies of its arguments. The arguments passed to swap are unchanged: int main() { int i = 10; int j = 20; cout << "Before swap():\ti: " << i << "\tj: " << j << endl; swap(i, j); cout << "After swap():\ti: " << i << "\tj: " << j << endl; return 0; } Compiling and executing this program results in the following output: Before swap(): i: 10 j: 20 After swap(): i: 10 j: 20 For swap to work as intended and swap the values of its arguments, we need to make the parameters references: // ok: swap acts on references to its arguments void swap(int &v1, int &v2) { int tmp = v2; v2 = v1; v1 = tmp; } Like all references, reference parameters refer directly to the objects to which they are bound rather than to copies of those objects. When we define a reference, we must initialize it with the object to which the reference will be bound. Reference parameters work exactly the same way. Each time the function is called, the reference parameter is created and bound to its corresponding argument. Now, when we call swap swap(i, j); the parameter v1 is just another name for the object i and v2 is another name for j. Any change to v1 is actually a change to the argument i. Similarly, changes to v2 are actually made to j. If we recompile main using this revised version of swap, we can see that the output is now correct: Before swap(): i: 10 j: 20 After swap(): i: 20 j: 10
Using Reference Parameters to Return Additional InformationWe've seen one example, swap, in which reference parameters were used to allow the function to change the value of its arguments. Another use of reference parameters is to return an additional result to the calling function. Functions can return only a single value, but sometimes a function has more than one thing to return. For example, let's define a function named find_val that searches for a particular value in the elements of a vector of integers. It returns an iterator that refers to the element, if the element was found, or to the end value if the element isn't found. We'd also like the function to return an occurrence count if the value occurs more than once. In this case the iterator returned should be to the first element that has the value for which we're looking. How can we define a function that returns both an iterator and an occurrence count? We could define a new type that contains an iterator and a count. An easier solution is to pass an additional reference argument that find_val can use to return a count of the number of occurrences: // returns an iterator that refers to the first occurrence of value // the reference parameter occurs contains a second return value vector<int>::const_iterator find_val( vector<int>::const_iterator beg, // first element vector<int>::const_iterator end, // one past last element int value, // the value we want vector<int>::size_type &occurs) // number of times it occurs { // res_iter will hold first occurrence, if any vector<int>::const_iterator res_iter = end; occurs = 0; // set occurrence count parameter for ( ; beg != end; ++beg) if (*beg == value) { // remember first occurrence of value if (res_iter == end) res_iter = beg; ++occurs; // increment occurrence count } return res_iter; // count returned implicitly in occurs } When we call find_val, we have to pass four arguments: a pair of iterators that denote the range of elements (Section 9.2.1, p. 314) in the vector in which to look, the value to look for, and a size_type (Section 3.2.3, p. 84) object to hold the occurrence count. Assuming ivec is a vector<int>, it is an iterator of the right type, and ctr is a size_type, we could call find_val as follows: it = find_val(ivec.begin(), ivec.end(), 42, ctr); After the call, the value of ctr will be the number of times 42 occurs, and it will refer to the first occurrence if there is one. Otherwise, it will be equal to ivec.end() and ctr will be zero. Using (const) References to Avoid CopiesThe other circumstance in which reference parameters are useful is when passing a large object to a function. Although copying an argument is okay for objects of built-in data types and for objects of class types that are small in size, it is (often) too inefficient for objects of most class types or large arrays. Moreover, as we'll learn in Chapter 13, some class types cannot be copied. By using a reference parameter, the function can access the object directly without copying it. As an example, we'll write a function that compares the length of two strings. Such a function needs to access the size of each string but has no need to write to the strings. Because strings can be long, we'd like to avoid copying them. Using const references we can avoid the copy:
// compare the length of two strings
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
Each parameter is a reference to const string. Because the parameters are references the arguments are not copied. Because the parameters are const references, is Shorter may not use the references to change the arguments.
References to const Are More FlexibleIt should be obvious that a function that takes a plain, nonconst reference may not be called on behalf of a const object. After all, the function might change the object it is passed and thus violate the constness of the argument. What may be less obvisous is that we also cannot call such a function with an rvalue (Section 2.3.1, p. 45) or with an object of a type that requires a conversion: // function takes a non-const reference parameter int incr(int &val) { return ++val; } int main() { short v1 = 0; const int v2 = 42; int v3 = incr(v1); // error: v1 is not an int v3 = incr(v2); // error: v2 is const v3 = incr(0); // error: literals are not lvalues v3 = incr(v1 + v2); // error: addition doesn't yield an lvalue int v4 = incr(v3); // ok: v3 is a non const object type int } The problem is that a nonconst reference (Section 2.5, p. 59) may be bound only to nonconst object of exactly the same type. Parameters that do not change the value of the corresponding argument should be const references. Defining such parameters as nonconst references needlessly restricts the usefulness of a function. As an example, we might write a program to find a given character in a string: // returns index of first occurrence of c in s or s.size() if c isn't in s // Note: s doesn't change, so it should be a reference to const string::size_type find_char(string &s, char c) { string::size_type i = 0; while (i != s.size() && s[i] != c) ++i; // not found, look at next character return i; } This function takes its string argument as a plain (nonconst) reference, even though it doesn't modify that parameter. One problem with this definition is that we cannot call it on a character string literal: if (find_char("Hello World", 'o')) // ... This call fails at compile time, even though we can convert the literal to a string. Such problems can be surprisingly pervasive. Even if our program has no const objects and we only call find_char on behalf of string objects (as opposed to on a string literal or an expression that yields a string), we can encounter compile-time problems. For example, we might have another function is_sentence that wants to use find_char to determine whether a string represents a sentence: bool is_sentence (const string &s) { // if there's a period and it's the last character in s // then s is a sentence return (find_char(s, '.') == s.size() - 1); } As written, the call to find_char from inside is_sentence is a compile-time error. The parameter to is_sentence is a reference to const string and cannot be passed to find_char, which expects a reference to a nonconst string.
Passing a Reference to a PointerSuppose we want to write a function that swaps two pointers, similar to the program we wrote earlier that swaps two integers. We know that we use * to define a pointer and & to define a reference. The question here is how to combine these operators to obtain a reference to a pointer. Here is an example: // swap values of two pointers to int void ptrswap(int *&v1, int *&v2) { int *tmp = v2; v2 = v1; v1 = tmp; } The parameter int *&v1 should be read from right to left: v1 is a reference to a pointer to an object of type int. That is, v1 is just another name for whatever pointer is passed to ptrswap. We could rewrite the main function from page 233 to use ptrswap and swap pointers to the values 10 and 20: int main() { int i = 10; int j = 20; int *pi = &i; // pi points to i int *pj = &j; // pj points to j cout << "Before ptrswap():\t*pi: " << *pi << "\t*pj: " << *pj << endl; ptrswap(pi, pj); // now pi points to j; pj points to i cout << "After ptrswap():\t*pi: " << *pi << "\t*pj: " << *pj << endl; return 0; } When compiled and executed, the program generates the following output: Before ptrswap(): *pi: 10 *pj: 20 After ptrswap(): *pi: 20 *pj: 10 What happens is that the pointer values are swapped. When we call ptrswap, pi points to i and pj points to j. Inside ptrswap the pointers are swapped so that after ptrswap, pi points to the object pj had addressed. In other words, pi now points to j. Similarly, pj points to i. 7.2.3. vector and Other Container Parameters
In order to avoid copying the vector, we might think that we'd make the parameter a reference. However, for reasons that will be clearer after reading Chapter 11, in practice, C++ programmers tend to pass containers by passing iterators to the elements we want to process:
// pass iterators to the first and one past the last element to print void print(vector<int>::const_iterator beg, vector<int>::const_iterator end) { while (beg != end) { cout << *beg++; if (beg != end) cout << " "; // no space after last element } cout << endl; } This function prints the elements starting with one referred to by beg up to but not including the one referred to by end. We print a space after each element but the last. 7.2.4. Array ParametersArrays have two special properties that affect how we define and use functions that operate on arrays: We cannot copy an array (Section 4.1.1, p. 112) and when we use the name of an array it is automatically converted to a pointer to the first element (Section 4.2.4, p. 122). Because we cannot copy an array, we cannot write a function that takes an array type parameter. Because arrays are automatically converted to pointers, functions that deal with arrays usually do so indirectly by manipulating pointers to elements in the array. Defining an Array ParameterLet's assume that we want to write a function that will print the contents of an array of ints. We could specify the array parameter in one of three ways:
// three equivalent definitions of printValues
void printValues(int*) { /* ... */ }
void printValues(int[]) { /* ... */ }
void printValues(int[10]) { /* ... */ }
Even though we cannot pass an array directly, we can write a function parameter that looks like an array. Despite appearances, a parameter that uses array syntax is treated as if we had written a pointer to the array element type. These three definitions are equivalent; each is interpreted as taking a parameter of type int*.
Parameter Dimensions Can Be MisleadingThe compiler ignores any dimension we might specify for an array parameter. Relying, incorrectly, on the dimension, we might write printValues as // parameter treated as const int*, size of array is ignored void printValues(const int ia[10]) { // this code assumes array has 10 elements; // disaster if argument has fewer than 10 elements! for (size_t i = 0; i != 10; ++i) { cout << ia[i] << endl; } } Although this code assumes that the array it is passed has at least 10 elements, nothing in the language enforces that assumption. The following calls are all legal: int main() { int i = 0, j[2] = {0, 1}; printValues(&i); // ok: &i is int*; probable run-time error printValues(j); // ok: j is converted to pointer to 0th // element; argument has type int*; // probable run-time error return 0; } Even though the compiler issues no complaints, both calls are in error, and probably will fail at run time. In each case, memory beyond the array will be accessed because printValues assumes that the array it is passed has at least 10 elements. Depending on the values that happen to be in that memory, the program will either produce spurious output or crash.
Array ArgumentsAs with any other type, we can define an array parameter as a reference or nonreference type. Most commonly, arrays are passed as plain, nonreference types, which are quietly converted to pointers. As usual, a nonreference type parameter is initialized as a copy of its corresponding argument. When we pass an array, the argument is a pointer to the first element in the array. That pointer value is copied; the array elements themselves are not copied. The function operates on a copy of the pointer, so it cannot change the value of the argument pointer. The function can, however, use that pointer to change the element values to which the pointer points. Any changes through the pointer parameter are made to the array elements themselves.
// f won't change the elements in the array void f(const int*) { /* ... */ } Passing an Array by ReferenceAs with any type, we can define an array parameter as a reference to the array. If the parameter is a reference to the array, then the compiler does not convert an array argument into a pointer. Instead, a reference to the array itself is passed. In this case, the array size is part of the parameter and argument types. The compiler will check that the size of the array argument matches the size of the parameter: // ok: parameter is a reference to an array; size of array is fixed void printValues(int (&arr)[10]) { /* ... */ } int main() { int i = 0, j[2] = {0, 1}; int k[10] = {0,1,2,3,4,5,6,7,8,9}; printValues(&i); // error: argument is not an array of 10 ints printValues(j); // error: argument is not an array of 10 ints printValues(k); // ok: argument is an array of 10 ints return 0; } This version of printValues may be called only for arrays of exactly 10 ints, limiting which arrays can be passed. However, because the parameter is a reference, it is safe to rely on the size in the body of the function:
// ok: parameter is a reference to an array; size of array is fixed
void printValues(int (&arr)[10])
{
for (size_t i = 0; i != 10; ++i) {
cout << arr[i] << endl;
}
}
f(int &arr[10]) // error: arr is an array of references f(int (&arr)[10]) // ok: arr is a reference to an array of 10 ints We'll see in Section 16.1.5 (p. 632) how we might write this function in a way that would allow us to pass a reference parameter to an array of any size. Passing a Multidimensioned ArrayRecall that there are no multidimensioned arrays in C++ (Section 4.4, p. 141). Instead, what appears to be a multidimensioned array is an array of arrays. As with any array, a multidimensioned array is passed as a pointer to its zeroth element. An element in a multidimenioned array is an array. The size of the second (and any subsequent dimensions) is part of the element type and must be specified: // first parameter is an array whose elements are arrays of 10 ints void printValues(int (matrix*)[10], int rowSize); declares matrix as a pointer to an array of ten ints.
int *matrix[10]; // array of 10 pointers int (*matrix)[10]; // pointer to an array of 10 ints We could also declare a multidimensioned array using array syntax. As with a single-dimensioned array, the compiler ignores the first dimension and so it is best not to include it: // first parameter is an array whose elements are arrays of 10 ints void printValues(int matrix[][10], int rowSize); declares matrix to be what looks like a two-dimensioned array. In fact, the parameter is a pointer to an element in an array of arrays. Each element in the array is itself an array of ten ints. 7.2.5. Managing Arrays Passed to FunctionsAs we've just seen, type checking for a nonreference array parameter confirms only that the argument is a pointer of the same type as the elements in the array. Type checking does not verify that the argument actually points to an array of a specified size.
There are three common programming techniques to ensure that a function stays within the bounds of its array argument(s). The first places a marker in the array itself that can be used to detect the end of the array. C-style character strings are an example of this approach. C-style strings are arrays of characters that encode their termination point with a null character. Programs that deal with C-style strings use this marker to stop processing elements in the array. Using the Standard Library ConventionsA second approach is to pass pointers to the first and one past the last element in the array. This style of programming 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 could rewrite printValues and call the new version as follows: void printValues(const int *beg, const int *end) { while (beg != end) { cout << *beg++ << endl; } } int main() { int j[2] = {0, 1}; // ok: j is converted to pointer to 0th element in j // j + 2 refers one past the end of j printValues(j, j + 2); return 0; } The loop inside printValues looks like other programs we've written that used vector iterators. We march the beg pointer one element at a time through the array. We stop the loop when beg is equal to the end marker, which was passed as the second parameter to the function. When we call this version, we pass two pointers: one to the first element we want to print and one just past the last element. The program is safe, as long as we correctly calculate the pointers so that they denote a range of elements. Explicitly Passing a Size ParameterA third approach, which is common in C programs and pre-Standard C++ programs, is to define a second parameter that indicates the size of the array. Using this approach, we could rewrite printValues one more time. The new version and a call to it looks like: // const int ia[] is equivalent to const int* ia // size is passed explicitly and used to control access to elements of ia void printValues(const int ia[], size_t size) { for (size_t i = 0; i != size; ++i) { cout << ia[i] << endl; } } int main() { int j[] = { 0, 1 }; // int array of size 2 printValues(j, sizeof(j)/sizeof(*j)); return 0; } This version uses the size parameter to determine how many elements there are to print. When we call printValues, we must pass an additional parameter. The program executes safely as long as the size passed is no greater than the actual size of the array.
7.2.6. main: Handling Command-Line OptionsIt turns out that main is a good example of how C programs pass arrays to functions. Up to now, we have defined main with an empty parameter list: int main() { ... } However, we often need to pass arguments to main. TRaditionally, such arguments are options that determine the operation of the program. For example, assuming our main program was in an executable file named prog, we might pass options to the program as follows: prog -d -o ofile data0 The way this usage is handled is that main actually defines two parameters: int main(int argc, char *argv[]) { ... } The second parameter, argv, is an array of C-style character strings. The first parameter, argc, passes the number of strings in that array. Because the second parameter is an array, we might alternatively define main as int main(int argc, char **argv) { ... } indicating that argv points to a char*. When arguments are passed to main, the first string in argv, if any, is always the name of the program. Subsequent elements pass additional optional strings to main. Given the previous command line, argc would be set to 5, and argv would hold the following C-style character strings: argv[0] = "prog"; argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "data0";
7.2.7. Functions with Varying Parameters
Ellipses parameters are used when it is impossible to list the type and number of all the arguments that might be passed to a function. Ellipses suspend type checking. Their presence tells the compiler that when the function is called, zero or more arguments may follow and that the types of the arguments are unknown. Ellipses may take either of two forms: void foo(parm_list, ...); void foo(...); The first form provides declarations for a certain number of parameters. In this case, type checking is performed when the function is called for the arguments that correspond to the parameters that are explicitly declared, whereas type checking is suspended for the arguments that correspond to the ellipsis. In this first form, the comma following the parameter declarations is optional. Most functions with an ellipsis use some information from a parameter that is explicitly declared to obtain the type and number of optional arguments provided in a function call. The first form of function declaration with ellipsis is therefore most commonly used. ![]() |