References and Reference Parameters
Two ways to pass arguments to functions in many programming languages are pass-by-value and pass-by-reference. When an argument is passed by value, a copy of the argument's value is made and passed (on the function call stack) to the called function. Changes to the copy do not affect the original variable's value in the caller. This prevents the accidental side effects that so greatly hinder the development of correct and reliable software systems. Each argument that has been passed in the programs in this chapter so far has been passed by value.
Performance Tip 6.5
One disadvantage of pass-by-value is that, if a large data item is being passed, copying that data can take a considerable amount of execution time and memory space. |
Reference Parameters
This section introduces reference parametersthe first of two means C++ provides for performing pass-by-reference. With pass-by-reference, the caller gives the called function the ability to access the caller's data directly, and to modify that data if the called function chooses to do so.
Performance Tip 6.6
Pass-by-reference is good for performance reasons, because it can eliminate the pass-by-value overhead of copying large amounts of data. |
Software Engineering Observation 6.13
Pass-by-reference can weaken security, because the called function can corrupt the caller's data. |
Later, we will show how to achieve the performance advantage of pass-by-reference while simultaneously achieving the software engineering advantage of protecting the caller's data from corruption.
A reference parameter is an alias for its corresponding argument in a function call. To indicate that a function parameter is passed by reference, simply follow the parameter's type in the function prototype by an ampersand (&); use the same convention when listing the parameter's type in the function header. For example, the following declaration in a function header
int &count
when read from right to left is pronounced "count is a reference to an int." In the function call, simply mention the variable by name to pass it by reference. Then, mentioning the variable by its parameter name in the body of the called function actually refers to the original variable in the calling function, and the original variable can be modified directly by the called function. As always, the function prototype and header must agree.
Passing Arguments by Value and by Reference
Figure 6.19 compares pass-by-value and pass-by-reference with reference parameters. The "styles" of the arguments in the calls to function squareByValue and function squareByReference are identicalboth variables are simply mentioned by name in the function calls. Without checking the function prototypes or function definitions, it is not possible to tell from the calls alone whether either function can modify its arguments. Because function prototypes are mandatory, however, the compiler has no trouble resolving the ambiguity.
Figure 6.19. Passing arguments by value and by reference.
(This item is displayed on pages 276 - 277 in the print version)
1 // Fig. 6.19: fig06_19.cpp 2 // Comparing pass-by-value and pass-by-reference with references. 3 #include 4 using std::cout; 5 using std::endl; 6 7 int squareByValue( int ); // function prototype (value pass) 8 void squareByReference( int & ); // function prototype (reference pass) 9 10 int main() 11 { 12 int x = 2; // value to square using squareByValue 13 int z = 4; // value to square using squareByReference 14 15 // demonstrate squareByValue 16 cout << "x = " << x << " before squareByValue "; 17 cout << "Value returned by squareByValue: " 18 << squareByValue( x ) << endl; 19 cout << "x = " << x << " after squareByValue " << endl; 20 21 // demonstrate squareByReference 22 cout << "z = " << z << " before squareByReference" << endl; 23 squareByReference( z ); 24 cout << "z = " << z << " after squareByReference" << endl; 25 return 0; // indicates successful termination 26 } // end main 27 28 // squareByValue multiplies number by itself, stores the 29 // result in number and returns the new value of number 30 int squareByValue( int number ) 31 { 32 return number *= number; // caller's argument not modified 33 } // end function squareByValue 34 35 // squareByReference multiplies numberRef by itself and stores the result 36 // in the variable to which numberRef refers in function main 37 void squareByReference( int &numberRef ) 38 { 39 numberRef *= numberRef; // caller's argument modified 40 } // end function squareByReference
|
Common Programming Error 6.14
Because reference parameters are mentioned only by name in the body of the called function, the programmer might inadvertently treat reference parameters as pass-by-value parameters. This can cause unexpected side effects if the original copies of the variables are changed by the function. |
Chapter 8 discusses pointers; pointers enable an alternate form of pass-by-reference in which the style of the call clearly indicates pass-by-reference (and the potential for modifying the caller's arguments).
Performance Tip 6.7
For passing large objects, use a constant reference parameter to simulate the appearance and security of pass-by-value and avoid the overhead of passing a copy of the large object. |
Software Engineering Observation 6.14
Many programmers do not bother to declare parameters passed by value as const, even though the called function should not be modifying the passed argument. Keyword const in this context would protect only a copy of the original argument, not the original argument itself, which when passed by value is safe from modification by the called function. |
To specify a reference to a constant, place the const qualifier before the type specifier in the parameter declaration.
Note in line 37 of Fig. 6.19 the placement of & in the parameter list of function squareByReference. Some C++ programmers prefer to write int& numberRef.
Software Engineering Observation 6.15
For the combined reasons of clarity and performance, many C++ programmers prefer that modifiable arguments be passed to functions by using pointers (which we study in Chapter 8), small nonmodifiable arguments be passed by value and large nonmodifiable arguments be passed to functions by using references to constants. |
References as Aliases within a Function
References can also be used as aliases for other variables within a function (although they typically are used with functions as shown in Fig. 6.19). For example, the code
int count = 1; // declare integer variable count int &cRef = count; // create cRef as an alias for count cRef++; // increment count (using its alias cRef)
increments variable count by using its alias cRef. Reference variables must be initialized in their declarations (see Fig. 6.20 and Fig. 6.21) and cannot be reassigned as aliases to other variables. Once a reference is declared as an alias for another variable, all operations supposedly performed on the alias (i.e., the reference) are actually performed on the original variable. The alias is simply another name for the original variable. Taking the address of a reference and comparing references do not cause syntax errors; rather, each operation actually occurs on the variable for which the reference is an alias. Unless it is a reference to a constant, a reference argument must be an lvalue (e.g., a variable name), not a constant or expression that returns an rvalue (e.g., the result of a calculation). See Section 5.9 for definitions of the terms lvalue and rvalue.
Figure 6.20. Initializing and using a reference.
1 // Fig. 6.20: fig06_20.cpp 2 // References must be initialized. 3 #include 4 using std::cout; 5 using std::endl; 6 7 int main() 8 { 9 int x = 3; 10 int &y = x; // y refers to (is an alias for) x 11 12 cout << "x = " << x << endl << "y = " << y << endl; 13 y = 7; // actually modifies x 14 cout << "x = " << x << endl << "y = " << y << endl; 15 return 0; // indicates successful termination 16 } // end main
|
Figure 6.21. Uninitialized reference causes a syntax error.
(This item is displayed on pages 278 - 279 in the print version)
1 // Fig. 6.21: fig06_21.cpp 2 // References must be initialized. 3 #include 4 using std::cout; 5 using std::endl; 6 7 int main() 8 { 9 int x = 3; 10 int &y; // Error: y must be initialized 11 12 cout << "x = " << x << endl << "y = " << y << endl; 13 y = 7; 14 cout << "x = " << x << endl << "y = " << y << endl; 15 return 0; // indicates successful termination 16 } // end main Borland C++ command-line compiler error message:
Microsoft Visual C++ compiler error message:
GNU C++ compiler error message:
|
Returning a Reference from a Function
Functions can return references, but this can be dangerous. When returning a reference to a variable declared in the called function, the variable should be declared static within that function. Otherwise, the reference refers to an automatic variable that is discarded when the function terminates; such a variable is said to be "undefined," and the program's behavior is unpredictable. References to undefined variables are called dangling references.
Common Programming Error 6.15
Not initializing a reference variable when it is declared is a compilation error, unless the declaration is part of a function's parameter list. Reference parameters are initialized when the function in which they are declared is called. |
Common Programming Error 6.16
Attempting to reassign a previously declared reference to be an alias to another variable is a logic error. The value of the other variable is simply assigned to the variable for which the reference is already an alias. |
Common Programming Error 6.17
Returning a reference to an automatic variable in a called function is a logic error. Some compilers issue a warning when this occurs. |
Error Messages for Uninitialized References
Note that the C++ standard does not specify the error messages that compilers use to indicate particular errors. For this reason, Fig. 6.21 shows the error messages produced by the Borland C++ 5.5 command-line compiler, Microsoft Visual C++.NET compiler and GNU C++ compiler when a reference is not initialized.