Hour 12. Creating References

What Is a Reference?

In the past 2 hours, you learned how to use pointers to manipulate objects on the heap and how to refer to those objects indirectly. References, the topic of this hour, give you almost all the power of pointers but with a much easier syntax.

A reference is an alias. When you create a reference, you initialize it with the name of another object, the target. From that moment on, the reference acts as an alternative name for the target, and anything you do to the reference is really done to the target.

You might read elsewhere that references are pointers, but that is not correct. Although references are often implemented using pointers, that is a matter of concern only to creators of compilers; as a programmer, you must keep these two ideas distinct.

Pointers are variables that hold the address of another object. References are aliases to an object.

Creating a Reference

You create a reference by writing the type of the target object, followed by the reference operator &, followed by the name of the reference. References can use any legal variable name, but in this book all reference names are prefixed with r and the second letter is capitalized. So, if you have an integer variable named someInt, you can make a reference to that variable by writing the following:

int &rSomeRef = someInt;

This is read as “rSomeRef is a reference to an integer that is initialized to refer to someInt.” The Reference program in Listing 12.1 shows how references are created and used.

Listing 12.1 The Full Text of Reference.cpp


 1: #include <iostream>
 2:
 3: int main()
 4: {
 5:     int intOne;
 6:     int &rSomeRef = intOne;
 7:
 8:     intOne = 5;
 9:     std::cout << "intOne: " << intOne << " ";
10:     std::cout << "rSomeRef: " << rSomeRef << " ";
11:
12:     rSomeRef = 7;
13:     std::cout << "intOne: " << intOne << " ";
14:     std::cout << "rSomeRef: " << rSomeRef << " ";
15:     return 0;
16: }


Reference produces the following output:

intOne: 5
rSomeRef: 5
intOne: 7
rSomeRef: 7

On line 5, a local int variable, intOne, is declared. On line 6, a reference to an int, rSomeRef, is declared and initialized to refer to intOne. If you declare a reference but don’t initialize it, you get a compiler error. References must be initialized.

On line 8, intOne is assigned the value 5. On lines 9 and 10, the values in intOne and rSomeRef are displayed, and are the same because rSomeRef is simply the reference to intOne.

On line 12, 7 is assigned to rSomeRef. Because this is a reference, it is an alias for intOne, and therefore the 7 is really assigned to intOne, as is shown by the display on lines 13 and 14.


By the Way

The reference operator & is the same symbol as the one used for the address of operator. In this case, it is used in the declaration.

Remember, with pointers, an asterisk (*) in the declaration means that the variable is a pointer. When used in a statement, it is the indirection operator when used with pointers or the multiplication operator when used in a mathematical expression.


Using the Address of Operator on References

If you ask a reference for its address, it returns the address of its target. That is the nature of references—they are aliases for the target.

The Reference2 program in Listing 12.2 demonstrates this concept.

Listing 12.2 The Full Text of Reference2.cpp


 1: #include <iostream>
 2:
 3: int main()
 4: {
 5:     int  intOne;
 6:     int &rSomeRef = intOne;
 7:
 8:     intOne = 5;
 9:     std::cout << "intOne: " << intOne << " ";
10:     std::cout << "rSomeRef: " << rSomeRef << " ";
11:
12:     std::cout << "&intOne: "  << &intOne << " ";
13:     std::cout << "&rSomeRef: " << &rSomeRef << " ";
14:
15:     return 0;
16: }


Here’s the output for the Reference2 program:

intOne: 5
rSomeRef: 5
&intOne: 0x0012FF7C
&rSomeRef: 0x0012FF7C

Once again, rSomeRef is initialized as a reference to intOne. This time the addresses of the two variables are displayed and they are identical. C++ gives you no way to access the address of the reference itself because it is not meaningful, as it would be if you were using a pointer or other variable. References are initialized when created and always act as a synonym for their target, even when the address of operator is applied.

For example, if you have a class called President, you might declare an instance of that class as follows:

President Barack_Obama;

You might then declare a reference to President and initialize it with this object:

President &Obama = Barack_Obama;

There is only one President; both identifiers refer to the same object of the same class. Any action you take on Obama will be taken on Barack_Obama, as well.

Be careful to distinguish between the & symbol on line 6 of Listing 12.2, which declares a reference to int named rSomeRef, and the & symbols on lines 12 and 13, which return the addresses of the integer variable intOne and the reference rSomeRef.

Normally, when you use a reference, you do not use the address of operator. You just use the reference as you would use the target variable. This is shown on line 10.

Even experienced C++ programmers, who know the rule that references cannot be reassigned and are always aliases for their target, can be confused by what happens when you try to reassign a reference: What appears to be a reassignment turns out to be the assignment of a new value to the target.

This is demonstrated by the Assignment program in Listing 12.3.

Listing 12.3 The Full Text of Assignment.cpp


 1: #include <iostream>
 2:
 3: int main()
 4: {
 5:     int intOne;
 6:     int &rSomeRef = intOne;
 7:
 8:     intOne = 5;
 9:     std::cout << "intOne: " << intOne << " ";
10:     std::cout << "rSomeRef: " << rSomeRef << " ";
11:     std::cout << "&intOne: "  << &intOne << " ";
12:     std::cout << "&rSomeRef: " << &rSomeRef << " ";
13:
14:     int intTwo = 8;
15:     rSomeRef = intTwo; // not what you think!
16:     std::cout << " intOne: " << intOne << " ";
17:     std::cout << "intTwo: " << intTwo << " ";
18:     std::cout << "rSomeRef: " << rSomeRef << " ";
19:     std::cout << "&intOne: "  << &intOne << " ";
20:     std::cout << "&intTwo: "  << &intTwo << " ";
21:     std::cout << "&rSomeRef: " << &rSomeRef << " ";
22:     return 0;
23: }


The program displays this output:

intOne:    5
rSomeRef:  5
&intOne:   1245064
&rSomeRef: 1245064

intOne:    8
intTwo:    8
rSomeRef:  8
&intOne:   1245064
&intTwo:   1245056
&rSomeRef: 1245064

Once again, an integer variable and a reference to an integer are declared, on lines 5 and 6. The integer is assigned the value 5 on line 8, and the values and their addresses are printed on lines 9–11.

On line 14 a new variable, intTwo, is created and initialized with the value 8. On line 15, the program tries to reassign rSomeRef to now be an alias to the variable intTwo, but that is not what happens.

Instead, rSomeRef continues to act as an alias for intOne, so this assignment is exactly equivalent to the following:

intOne = intTwo;

Sure enough, when the values of intOne and rSomeRef are displayed (lines 16 and 18) they are the same as intTwo. In fact, when the addresses are printed on lines 19–21, you see that rSomeRef continues to refer to intOne and not intTwo.

What Can Be Referenced?

Any object can be referenced, including user-defined objects. Note that you create a reference to an object, not to a class or a data type such as int. You do not write this:

int &rIntRef = int; // wrong

You must initialize rIntRef to a particular integer, such as this:

int howBig = 200;
int &rIntRef = howBig;

In the same way, you don’t initialize a reference to a Cat:

Cat &rCatRef = Cat;   // wrong

You must initialize rCatRef to a particular Cat object:

Cat Frisky;
Cat & rCatRef = Frisky;

References to objects are used just like the object itself. Member data and functions are accessed using the normal class member access operator (.). As with the built-in types, the reference acts as an alias to the object.

Null Pointers and Null References

When pointers are not initialized, or when they are deleted, they ought to be assigned to NULL. This is not true for references. In fact, a reference cannot be null, and a program with a reference to a null object is considered an invalid program. When a program is invalid, just about anything can happen. It can appear to work, or it can erase important files on your hard drive. Both are possible outcomes of an invalid program.

Most compilers support null references without much complaint, crashing only if you try to use the reference in some way. Taking advantage of this, however, is not a good idea. When you move your program to another computer or a different compiler, mysterious bugs might occur if you have null references.

Passing Function Arguments by Reference

In Hour 5, “Calling Functions,” you learned that functions have two limitations: Arguments are passed by value and the return statement only can return one value.

Passing values to a function by reference can overcome both of these limitations. In C++, passing by reference is accomplished in two ways: using pointers and using references. The syntax is different, but the net effect is the same: Rather than a copy being created within the scope of the function, the actual original object is passed into the function.

Passing an object by reference enables the function to change the object being referred to.

The ValuePasser program in Listing 12.4 creates a swap function and passes in its parameters by value.

Listing 12.4 The Full Text of ValuePasser.cpp


 1: #include <iostream>
 2:
 3: void swap(int x, int y);
 4:
 5: int main()
 6: {
 7:     int x = 5, y = 10;
 8:
 9:     std::cout << "Main. Before swap, x: " << x
10:               << " y: " << y << " ";
11:     swap(x,y);
12:     std::cout << "Main. After swap, x: " << x
13:               << " y: " << y << " ";
14:     return 0;
15: }
16:
17: void swap (int x, int y)
18: {
19:     int temp;
20:
21:     std::cout << "Swap. Before swap, x: " << x
22:               << " y: " << y << " ";
23:
24:     temp = x;
25:     x = y;
26:     y = temp;
27:
28:     std::cout << "Swap. After swap, x: " << x
29:               << " y: " << y << " ";
30:
31: }


The following output is displayed:

Main. Before swap. x: 5 y: 10
Swap. Before swap. x: 5 y: 10
Swap. After swap. x: 10 y: 5
Main. After swap. x: 5 y: 10

This program initializes two variables in main() and then passes them to the swap() function, which appears to swap them. But when they are examined again in main(), they are unchanged!

The problem here is that x and y are being passed to swap() by value. Local copies were made in the function and those copies were swapped, but the originals remained unchanged. What you want to do is pass x and y by reference.

There are two ways to solve this problem in C++: You can make the parameters of swap() pointers to the original values, or you can pass in references to the original values.

Making swap() Work with Pointers

When you pass in a pointer, you pass in the actual address of the object. Thus, the function can manipulate the value at that address.

To make swap() change the actual values using pointers, the function should be declared to accept two int pointers. Then, by dereferencing the pointers, the values of x and y will, in fact, be swapped. The PointerSwap program in Listing 12.5 demonstrates this idea.

Listing 12.5 The Full Text of PointerSwap.cpp


 1: #include <iostream>
 2:
 3: void swap(int *x, int *y);
 4:
 5: int main()
 6: {
 7:     int x = 5, y = 10;
 8:
 9:     std::cout << "Main. Before swap, x: " << x
10:               << " y: " << y << " ";
11:     swap(&x, &y);
12:     std::cout << "Main. After swap, x: " << x
13:               << " y: " << y << " ";
14:     return 0;
15: }
16:
17: void swap(int *px, int *py)
18: {
19:     int temp;
20:
21:     std::cout << "Swap. Before swap, *px: " << *px
22:               << " *py: " << *py << " ";
23:
24:     temp = *px;
25:     *px = *py;
26:     *py = temp;
27:
28:     std::cout << "Swap. After swap, *px: " << *px
29:               << " *py: " << *py << " ";
30: }


The PointerSwap program demonstrates the results of the swap attempt in the output:

Main. Before swap. x: 5 y: 10
Swap. Before swap. *px: 5 *py: 10
Swap. After swap. *px: 10 *py: 5
Main. After swap. x: 10 y: 5

Success! On line 3, the prototype of swap() is changed to indicate that its two parameters will be pointers to int rather than int variables. The asterisk between the variable type and its name indicates that it’s a pointer.

When swap() is called on line 11, the addresses of x and y are passed as the arguments.

On line 19, the local variable temp is declared in the swap() function. There’s no need for temp to be a pointer; it will just hold the value of *px (the value of x in the calling function) for the life of the function. After the function returns, temp is no longer needed.

On line 24, temp is assigned the value at px. On line 25, the value at px is assigned to the value at py. On line 26, the value stashed in temp (that is, the original value at px) is put into py.

The values in the calling function, whose address was passed to swap(), are swapped.

Implementing swap() with References

The preceding program works, but the syntax of the swap() function is cumbersome in two ways. First, the repeated need to dereference the pointers in the swap() function makes it error-prone and hard to read. Second, the need to pass the address of the variables in the calling function makes the inner workings of swap() overly apparent to its users.

A useful goal in C++ is to prevent the user of a function from worrying about how it works, instead of just focusing on what it does and the value it returns. Passing by pointers puts the burden on the calling function, which is not where it belongs. The calling function must know to pass in the address of the object it wants to swap.

The burden of understanding the reference semantics should be on the function implementing the swap. To accomplish this, you use references. The ReferenceSwap program in Listing 12.6 rewrites the swap() function using references.

Now the calling function just passes in the object, and because the parameters are declared to be references, the semantics are passed by reference. The calling function doesn’t need to do anything special.

Listing 12.6 The Full Text of ReferenceSwap.cpp


 1: #include <iostream>
 2:
 3: void swap(int &x, int &y);
 4:
 5: int main()
 6: {
 7:     int x = 5, y = 10;
 8:
 9:     std::cout << "Main. Before swap, x: " << x
10:               << " y: " << y << " ";
11:     swap(x, y);
12:     std::cout << "Main. After swap, x: " << x
13:               << " y: " << y << " ";
14:     return 0;
15: }
16:
17: void swap(int &rx, int &ry)
18: {
19:     int temp;
20:
21:     std::cout << "Swap. Before swap, rx: " << rx
22:               << " ry: " << ry << " ";
23:
24:     temp = rx;
25:     rx = ry;
26:     ry = temp;
27:
28:     std::cout << "Swap. After swap, rx: " << rx
29:               << " ry: " << ry << " ";
30:}


In the program’s output, the success of the swap is demonstrated:

Main. Before swap, x:5 y: 10
Swap. Before swap, rx:5 ry:10
Swap. After swap, rx:10 ry:5
Main. After swap, x:10, y:5

Just as in the example with pointers, two variables are declared (line 7), and their values are displayed on lines 9 and 10. On line 11 the function swap() is called, but note that x and y are passed, not their addresses. The calling function simply passes the variables.

When swap() is called, program execution jumps to line 17, where the variables are identified as references by the reference operator & between the argument’s type and name. The values of x and y are displayed on lines 20–21, but note that no special operators are required. These are aliases for the original values and can be used as such.

On lines 24–26 the values are swapped, and then they’re displayed on lines 28–29. Program execution jumps back to the calling function, and on lines 12 and 13 the values are displayed in main(). Because the parameters to swap() are declared to be references, the values from main() are passed by reference, and thus are changed in main(), as well.

References provide the convenience and ease of use of normal variables with the power and pass-by-reference capability of pointers.

Understanding Function Headers and Prototypes

The swap() function that takes references is easier to use and the code is easier to read. But how does the calling function know if the values are passed by reference or by value? As a user of swap(), the programmer must ensure that swap() will in fact change the parameters.

This is another use for the function prototype. By examining the parameters declared in the prototype, which is typically in a header file along with all the other prototypes, the programmer knows that the values passed into swap() are passed by reference and thus will be swapped properly.

If swap() had been a member function of a class, the class declaration, also available in a header file, would have supplied this information.

In C++, users of classes (any other class’s function using the class) rely on the header file to tell all that is needed; it acts as the interface to the class or function. The actual implementation is hidden from the client. This enables the programmer to focus on the problem at hand and to use the class or function without concern for how it is implemented.

Returning Multiple Values

As discussed, functions can return only one value. What if you need to get two values back from a function? One way to solve this problem is to pass two objects into the function by reference. The function then can fill the objects with the correct values. Because passing by reference enables a function to change the original objects, this effectively lets the function return two pieces of information. This approach bypasses the return value of the function, which then can be reserved for reporting errors.

Once again, this can be done with references or pointers. The ReturnPointer program in Listing 12.7 demonstrates a function that returns three values, two as pointer parameters and one as the return value of the function.

Listing 12.7 The Full Text of ReturnPointer.cpp


 1: #include <iostream>
 2:
 3: short factor(int, int*, int*);
 4:
 5: int main()
 6: {
 7:     int number, squared, cubed;
 8:     short error;
 9:
10:     std::cout << "Enter a number (0 - 20): ";
11:     std::cin >> number;
12:
13:     error = factor(number, &squared, &cubed);
14:
15:     if (!error)
16:     {
17:         std::cout << "number: " << number << " ";
18:         std::cout << "square: " << squared << " ";
19:         std::cout << "cubed: "  << cubed   << " ";
20:     }
21:     else
22:         std::cout << "Error encountered!! ";
23:     return 0;

24: }
25:
26: short factor(int n, int *pSquared, int *pCubed)
27: {
28:     short value = 0;
29:     if (n > 20)
30:     {
31:         value = 1;
32:     }
33:     else
34:     {
35:         *pSquared = n*n;
36:         *pCubed = n*n*n;
37:         value = 0;
38:     }
39:     return value;
40: }


The ReturnPointer program produces the following output:

Enter a number (0-20): 3
number: 3
square: 9
cubed: 27

On line 7, number, squared, and cubed are defined as int. number is assigned a value based on user input. This number and the addresses of squared and cubed are passed to the function factor().

factor() examines the first parameter, which is passed by value. If it is greater than 20 (the maximum value this function can handle), it sets value to a simple error value. Note that the return value from factor() is reserved for either this error value or the value 0, indicating all went well. The function returns this value on line 39.

The actual values needed by users calling the function, the square and cube of number, are returned not through the return mechanism, but rather by changing the values directly using the pointers that were passed into the function.

On lines 35 and 36, the pointers are assigned their return values. On line 37, value is assigned a success value of 0. On line 39, value is returned.

One improvement to this program might be to declare the following:

enum ERR_CODE { SUCCESS, ERROR };

Then, rather than returning 0 or 1, the program could return SUCCESS or ERROR. Enumerated constants are given integer values based on their order unless otherwise specified, so the first enumerated value (SUCCESS) is given the value 0 and the second is given the value 1.

Returning Values by Reference

Although the ReturnPointer program works, it can be made easier to read and maintain by using references rather than pointers. The ReturnReference program in Listing 12.8 shows the same program rewritten to use references and to incorporate the ERR_CODE enumeration.

Listing 12.8 The Full Text of ReturnReference.cpp


 1: #include <iostream>
 2:
 3: enum ERR_CODE { SUCCESS, ERROR };
 4:
 5: ERR_CODE factor(int, int&, int&);
 6:
 7: int main()
 8: {
 9:     int number, squared, cubed;
10:     ERR_CODE result;
11:
12:     std::cout << "Enter a number (0 - 20): ";
13:     std::cin >> number;
14:
15:     result = factor(number, squared, cubed);
16:
17:     if (result == SUCCESS)
18:     {
19:         std::cout << "number: " << number << " ";
20:         std::cout << "square: " << squared << " ";
21:         std::cout << "cubed: "  << cubed   << " ";
22:     }
23:     else
24:     {
25:         std::cout << "Error encountered!! ";
26:     }
27:     return 0;
28: }
29:
30: ERR_CODE factor(int n, int &rSquared, int &rCubed)
31: {
32:     if (n > 20)
33:     {
34:         return ERROR;   // simple error code
35:     }
36:     else
37:     {
38:         rSquared = n*n;
39:         rCubed = n*n*n;
40:         return SUCCESS;
41:     }
42: }


Here’s sample output for the ReturnReference program:

Enter a number (0-20): 3
number: 3
square: 9
cubed: 27

The ReturnReference program is identical to the ReturnPointer program with two exceptions. The ERR_CODE enumeration makes the error reporting a bit more explicit on lines 34 and 40, and the error handling on line 17.

The larger change, however, is that factor() is now declared to take references to squared and cubed rather than pointers. This makes the manipulation of these parameters much simpler and easier to understand.

Summary

During this hour, you worked with references, which serve a similar purpose to pointers and are sometimes mistaken for them. The difference is that pointers are variables holding the address of an object, whereas references are aliases to an object.

It’s important to understand how pointers and references are distinct.

A reference is an alias to another object, which is called the target. The reference serves as an alternate name for the target. Any actions taken to the reference actually affect the target.

References provide the power of pointers with simpler syntax.

Q&A

Q. Why use references if pointers can do everything references can?

A. References are easier to use and understand. The indirection is hidden, and there is no need to repeatedly dereference the variable.

Q. Why use pointers if references are easier?

A. References cannot be NULL, and they cannot be reassigned. Pointers offer greater flexibility but are slightly more difficult to use.

Q. Is it true that a species went extinct at Disney World?

A. The Discovery Island nature preserve at Disney World in Orlando, Fl., was the last home of the Dusky Seaside Sparrow, a non-migratory bird whose primary habitat was Florida’s Merritt Island and the St. Johns River.

The spraying of the pesticide DDT, intentional flooding to control mosquitos and highway construction all devastated the bird’s habitat, reducing its population over the second half of the 20th century. By 1979, only six of the sparrows were known to exist—and all of them were males.

The last four sparrows were taken to Disney’s nature preserve for a crossbreeding program with Scott’s Seaside Sparrows, but it was not successful.

The species was declared extinct in 1990.

Workshop

Now that you’ve had the chance to see references in action, you can answer a few questions and complete a couple of exercises to check your knowledge.

Quiz

1. What is a reference?

A. An alias

B. A synonym

C. Both a and b

2. What operator is used to create a reference?

A. ->

B. &

C. *

3. What is the default mechanism for passing variables to a called function in C++? What are some of the techniques to override that mechanism?

A. Pass by value

B. Pass by reference

C. Pass the salad

Answers

1. C. A reference is an alias or synonym for another variable or object.

2. B. The ampersand (&) is used when declaring a reference. References must be initialized when declared. You cannot have a null reference like you can with pointers.

3. A. Pass by value, where a copy of the variable is passed to the function, not the original—which prevents the function from changing the original value. Pointers are one way to get around pass by value since the address of the original value is passed. References are another since the alias for the original variable is passed.

Activities

1. Modify the ReturnPointer program to use references rather than pointers.

2. Rewrite the ReferenceSwap program to swap three numbers.

To see solutions to these activities, visit this book’s website at http://cplusplus.cadenhead.org.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.122.235