Dan Saks is the owner of Saks & Associates, which offers consulting and training in C, C++ and Pascal. He is also a contributing editor of TECH Specialist. He serves as secretary of the ANSI C++ committee and is a member of the ANSI C committee. Readersc an write to him at 287 W. McCreight Ave., Springfield, OH 45504 or by email at dsaks@wittenberg. edu.
References provide an alternative to pointers as a way of binding a name to an object. A reference declaration is just like a pointer declaration, except for using & instead of *. For example,
int &ri;declares r to have type reference to int. Just as many programmers often pronounce int * as "int star," I often pronounce int & as "int ref."With few exceptions (described later), you must initialize a reference when you declare it. For example,
int i; ... int &ri = i;initializes ri to refer to i. Henceforth, using ri in an expression is just like using i. For example,
ri = 4; int j = ri + 2;assigns 4 to i, and then initializes j to 6.A reference is essentially a constant pointer that is automatically dereferenced each time it's used. The preceding example is equivalent to the following code using an explicit pointer:
int i = 3; int *const cpi = &i; ... *cpi = 4; int j = *cpi + 2;This analogy with pointers clearly shows that initializing a reference is different from assigning to a reference. Initializing a reference assigns the address of an object to the pointer implementing that reference. Assigning to the reference assigns through the pointer to the referenced object.In fact, no C++ operator operates on a reference. Every operator applied to a reference applies to the referenced object, not to the reference itself. For example, if ri is an int &, then ++ri increments the int referenced by ri, and &ri is the address of that int. If rs is a reference to a struct S with member m, then rs.m (not rs->m) accesses member m of the S object referenced by rs.
In short, a reference is a pointer with object semantics. That is, an expression of type reference to T is implemented as a pointer, but you use the expression as if it were an object of type T.
Initializing References
Once you initialize a reference you cannot change it to refer to a different object. That is why the pointer equivalent of
int &ri;is a constant pointer declared as
int *const cpi;Note that this is different from both
const int *pci; int const *pci;either of which declares pci as a (modifiable) pointer to a constant int.In general, the initializer for a reference to type T should be an lvalue of type T. For example,
char s[] = "hello"; char &rc = s[0];binds rc (of type char &) to the first character of s (of type char). Note that neither
char &rc = s; char &rc = &s[i];are valid, because they attempt to initialize a char & with a char *.A reference can refer to a const object. In this case, if the initializing expression is not an lvalue, the generated code uses a temporary object to hold the initial value and binds the reference to that temporary. For example,
const double &rcd = 1.0;compiles as something like
const double temp = 1.0; double *const cpd = &temp;The initializing expression for a const T & need not have type T; it can have an expression of any type with a standard or user-defined conversion to type T. For example,
const double &rcd = 1;promotes 1 to double to initialize the temporary. Or, if X is a class with a constructor X (int), then
const X &rcx: = 1;is roughly equivalent to
const X temp (1); X *const cpx = &temp;Here, the constructor call x(1) is a user-defined conversion from type int to type X. The resulting X object is the initial value of the temporary.Early dialects of C++ [1] created temporaries even for non-const references. That is,
double &rd = 1;was allowed, and bound the reference to a modifiable temporary. The commentary in the revised C++ language definition [2] explains that binding references to modifiable temporaries proved to be "a major source of errors and surprises." Consequently, the revised language definition prohibits creating temporaries to initialize references to non-const objects. Many of today's C++ implementations still follow the older, more forgiving, rule. Some implementations produce warnings when they create temporaries for non-const references. You should heed those warnings.The lifetime of a temporary created to initialize a reference lasts until the end of the scope in which that temporary is created. For example, in
void f() { ... const int &rci = 1; ... }the temporary bound to rci survives until f returns. If the temporary has a class type with a destructor, destroying the temporary includes calling that destructor. For example, suppose class X in the following has both a constructor and a destructor:
for (int i = 0; i < n; ++i) { ... const X &rcx = i; ... }Each iteration of the loop creates a new temporary (using the constructor), binds its address to rcx, and then destroys that temporary (using the destructor).A reference declaration with an explicit extern specifier need not have an initializer. For example,
extern int &ri;is valid. Furthermore, you cannot add an initializer to the declaration of a reference that is a member of a class. Rather, you must initialize the reference member in the class constructor(s), as shown in Listing 1.Class X in Listing 1 has two data members, int i and int &ri, and a constructor X(int *). The constructor uses a list of mem-initializers
i(*p), ri(*p)to initialize member i to the value of *p, and initialize member ri to reference the integer addressed by p. Calling this constructor with
X x1(&n);creates an X object x1 whose member i is initialized with the value of n, and whose member ri refers to n.When initialization is the same as assignment, as with int member i, you can use an assignment in the constructor's body instead of the mem-initializer. That is, you can write the constructor as
X(int *p) : r(ri) { i = *p; }However, initializing a reference is not the same as assigning to a reference, so you must initialize a reference member with a mem-initializer.
Passing Arguments By Reference
In practice, you're not likely to use references as ordinary variables or as class data members. References are much more useful as an alternative method for argument passing.C always passes function arguments by value. A function call copies each actual argument (with appropriate type conversion) to the storage for the corresponding formal argument. For example, given the function declaration
void f(long L, double d);the call
f(1, 2);promotes the first actual argument, 1, to long and copies it to storage for formal argument L, and promotes the second actual argument, 2, to double and copies it to storage for formal argument d. (Argument evaluation in C and C++ does not necessarily proceed from left to right; the order is implementation-defined.)Since the storage for a formal argument is distinct from the storage for its corresponding actual argument, changing the value of the formal argument inside the function doesn't affect the value of the actual argument. For example, the simple function
void inc(int i) { ++i; } ... inc(n);increments its local copy of the argument and discards the incremented value when it returns. Calling inc(n) has no effect on n.In C, if you want inc to modify its operand, you pass a pointer to the operand instead. That is, you write inc as
void inc(int *pi) { ++*pi; }and apply the address-of operator & to the operand n when you call the function, as in
inc(&n);In C++, you can also write inc to modify its operand by using a reference type as the argument type and omit the & from the function call:
void inc(int &ri) { ++ri; } ... inc(n);The function call initializes the formal reference argument ri to refer to the actual argument n. The increment expression ++ri applies to object referenced by ri, namely n, and not to ri itself.Once again, C++ makes an important distinction between initialization and assignment that C does not. At the time of the call, the formal reference argument is initialized to refer to the corresponding actual argument, just as if the reference had been declared with an initializer:
int &ri = n;This is not the same as assignment to a reference, like
ri = n;which copies the value of n to the object referenced by the (previously initialized) reference ri.C++ programmers don't agree on using reference arguments instead of pointer arguments. Many programmers with experience in languages that support call-by-reference argument passing (like FORTRAN, Pascal and PL/I) find reference arguments more convenient than pointer arguments. Others believe that functions should avoid modifying their arguments. They prefer using explicit pointer arguments, or function return values, like
int inc(int i) { return i + 1; }I prefer using function return values wherever possible, but having programmed in languages that support call-by-reference, I do not find using pointer arguments to be any more readable than using reference arguments.
Constant Reference Arguments
When a function does not modify its argument, passing by value provides the proper functionality; however, it might be expensive if the argument is large. For example, suppose you have a function
int f(X x);that examines but does not alter x, and that X is a class type declared as
struct X { int n; public: x() { ... } ... };Class X has only one data member, n, so passing an X object to f by value is only as costly as passing an int. Now, suppose you enhance the program, and in so doing, add several data fields to X:
struct X { int n; double d1, d2; char s[N]; public: x() { ... } ... };These additional members dramatically increase the size of X objects, making the calls to f fairly expensive because each call copies an entire X.Of course, you can rewrite function f to pass its argument via a const pointer:
int f(const X *p);Passing the X object via a pointer avoids unnecessary copying, and the const qualifier prevents inadvertent modification of the object via the pointer p. Unfortunately, you must rewrite the body of f to use -> instead of . (dot) whenever you access a member of *p. You must also change every call to f to pass the address of its argument explicitly.Using a const reference argument avoids this dilemma. Passing the argument as a constant reference is just as efficient as passing via a pointer, but much more convenient. You simply declare f as
int f(const X &x);You need not change any code in the body of f, nor any of the calls to f.
Copy Constructors
C++ requires a reference argument as the declaration of any copy constructors. A copy constructor for a class X is a constructor that initializes an X object by copying another X object. You must be able to call a copy constructor by passing a single argument of type X. For example, consider:
X x; ... X y(x);The declaration of x uses the default constructor. It initializes the members of x with default values. The declaration of y uses the copy constructor to initialize y using x's value.If you do not declare a copy constructor for a class X, the translator will generate one automatically, declared implicitly as either
X::X(const X &);or
X::X(X &);If all data members of X have primitive types or class types with copy constructors of the first form, then the generated copy constructor will also use that form. Otherwise, the generated copy constructor will be of the second form.A generated copy constructor initializes each member of the constructed object with the corresponding member of the copied object. That is, if class X has members a and b, then the generated copy constructor behaves just like
X::X(const X &x) : a(x.a), b(x.b) { }This default behavior is called memberwise initialization. If it does not produce appropriate behavior for class objects, you must write an explicit copy constructor with the desired behavior. Listing 2 shows a rudimentary class for variable-length character strings that needs an explicit copy constructor.The String class stores variable-length strings in character arrays allocated from the free store. Each String has a data member str that stores the pointer to the first character in the array, and another member len that stores the array length (plus one for a \0 at the end). Class String has two constructors. The first constructor, String(const char *), initializes a String to hold a copy of a null-terminated string. For example, the declaration
String s1("hello");initializes s1 to hold a copy of "hello".The second constructor, String(const String &), is the explicit copy constructor. It initializes a String to hold a copy of the text in another String. For example,
String s2(s1);initializes s2 to hold a copy of s1. This copy constructor allocates a new character array from the free store and copies the contents of s1's array into this new array. On the other hand, the default copy constructor would simply have initialized s2 to refer to the same character array used by s1 (s1.str and s2.str would point to the same array). With the default copy constructor, changes made to s1 would corrupt the value of s2.The sample main program demonstrates that s2's storage is distinct from s1's storage, by using the member function String::cat(char).s1. cat('!') appends the single character '!' to the end of s1. String::cat(char) allocates a new, larger character array from the free store to hold the new string and returns the old character array to the free store. If you execute the program using an explicit copy constructor, s1.cat('!') has no effect on s2. If you comment out the explicit copy constructor and run the program, calling s1.cat corrupts s2.
Functions Returning References
A function can return a reference. A call to a function returning a reference can be used as an 1value as well as an rvalue. That is, if you declare function f as
int &f(int);then
f(i) = j;stores the value of j into the int referenced by the return value of f(i). You can still use f(i) as an rvalue, as in
j = f(i);which copies the value of the int referenced by f(i) into j.Listing 3 illustrates a simple but practical use for functions returning references. It adds a new member function sub to class String, such that s.sub(i) returns a reference to the ith character of String s. For example, given
String s("hello");then s.sub(4) returns a reference to the fourth character of s (containing o). The assignment
s.sub(0) = 'H';changes the value of s to "Hello". The loop in the main function of Listing 3 uses s1. sub(i) as both an 1value and an rvalue to convert s1 to uppercase.If String::sub returns a char (rather than a char &), then you could not use s.sub(i) as an 1value. If String::sub returns a char *, you would have to dereference that pointer to access the character, as in
*s.sub(0) : 'H';Note that the return expression str[i] in String::sub is of type char, not char &. C++ converts the expression in the return statement to the return type of the function as if it were initializing (not assigning to) a reference variable. That is, the translator converts the return expression to the return type as if you had declared the return value as a reference variable, such as
char &return_value = str[i];Dangling References
A dangling reference is a reference bound to an object that no longer exists. Dangling references, like dangling pointers, can cause very subtle bugs in C++ programs.A function that returns a reference to one of its local variables produces a dangling reference. For example, function f in Listing 4 returns a reference to its local variable i. However, the return from f discards i's storage, so the assignment
f() = 2;writes to a nonexistent object (typically somewhere in an inactive stack frame).The String class in Listing 3 provides another opportunity to create a dangling reference. Consider the following code:
String s("hello"); char &rc = s.sub(0); ... s.cat('?'); ... rc = 'H';The declaration of rc binds it to first character of the current value of s. The call to s.cat('?') assigns a new value to s and destroys the old value (including the character bound to rc). rc is now dangling. The assignment to rc produces unpredictable results.The constructor in Listing 5 also produces a dangling reference. Class X has a data member that's a reference. The constructor X(int) binds that data member to the address of the formal argument to the constructor. But the return from the constructor destroys the storage for the formal argument, leaving x.r dangling.
In short, to avoid dangling references, don't bind a reference to an object whose lifetime ends before the reference's lifetime ends.
Limitations On References
Reference types have some interesting limitations. Remember that operators applied to a reference apply to the referenced object, not to the reference. This is true for the sizeof operator. Thus, for any type T, sizeof(T &) == sizeof(T). In other words, you can't take the size of a reference. This has several ramifications.You cannot declare arrays of references, such as
T &r[N];because the compiler can't determine the size of each element. Similarly, you cannot declare pointers to references, like
T &*p;because pointer arithmetic, like array subscripting, relies on the size of the type to which the pointer points. In addition, you cannot declare references to references, nor references to bit-fields.You cannot declare both f(X) and f(X &) in the same scope. Although C++ lets you overload functions, the argument lists must be sufficiently different to allow the compiler to unambiguously match a function call with a function declaration. For example, if you try to declare both
void f(int); void f(int &);in the same scope, the compiler can't tell if the function call in
int k; ... f(k);should pass k by value or by reference. Most C++ compilers will not wait for the call to f to issue a diagnostic; they will reject the second function declaration as indistinct from the first.
Readings
[1] Stroustrup, Bjarne, The C++ Programming Language, Addison-Wesley, Reading, MA, 1986.[2] Ellis, Margaret A. and Bjarne Stroustrup, The Annotated C++ Reference Manual, Addison-Wesley, Reading, MA, 1990.