Inapoi
Inainte
Cuprins
11: References & the Copy-Constructor
References are like constant
pointers that are automatically dereferenced by the compiler.
Although references
also exist in Pascal, the C++ version was taken from the
Algol language. They are essential in C++ to support the syntax of operator
overloading (see Chapter 12),
but they are also a general convenience to control the way arguments are passed
into and out of functions.
This chapter will first look briefly at
the differences between pointers in C and C++, then
introduce references. But the bulk of the chapter will delve into a rather
confusing issue for the new C++ programmer: the
copy-constructor, a special
constructor (requiring references) that makes a new object from an existing
object of the same type. The copy-constructor is used by the compiler to pass
and return objects by value
into and out of
functions.
Finally, the somewhat obscure C++
pointer-to-member feature is
illuminated.
Pointers in C++
The most important difference between
pointers in C and those in C++ is that
C++ is a
more strongly typed language. This stands out where
void*
is concerned. C doesn’t let you casually assign a pointer of one type to
another, but it does allow you to accomplish this through a void*.
Thus,
bird* b;
rock* r;
void* v;
v = r;
b = v;
Because this “feature” of C
allows you to quietly treat any type like any other type, it leaves a big
hole in the type system. C++ doesn’t allow this;
the compiler gives you an error message, and if you really want to treat one
type as another, you must make it explicit, both to the compiler and to the
reader, using a cast. (Chapter 3 introduced C++’s improved
“explicit” casting
syntax.)
References in C++
A reference
(&) is like a constant pointer that is
automatically dereferenced. It is usually used for function argument lists
and function return
values. But you can also make a
free-standing reference. For
example,
//: C11:FreeStandingReferences.cpp
#include <iostream>
using namespace std;
// Ordinary free-standing reference:
int y;
int& r = y;
// When a reference is created, it must
// be initialized to a live object.
// However, you can also say:
const int& q = 12; // (1)
// References are tied to someone else's storage:
int x = 0; // (2)
int& a = x; // (3)
int main() {
cout << "x = " << x << ", a = " << a << endl;
a++;
cout << "x = " << x << ", a = " << a << endl;
} ///:~
In line (1), the compiler allocates a
piece of storage, initializes it with the value 12, and ties the reference to
that piece of storage. The point is that any reference must be tied to someone
else’s piece of storage. When you access a reference, you’re
accessing that storage. Thus, if you write lines like (2) and (3), then
incrementing a is actually incrementing x, as is shown in
main( ). Again, the easiest way to think about a reference is as a
fancy pointer. One advantage of this “pointer” is that you never
have to wonder whether it’s been initialized (the compiler enforces it)
and how to dereference it (the compiler does it).
There are certain rules when using
references:
- A reference must be
initialized when it is created. (Pointers can be initialized at any
time.)
- Once a
reference is initialized to an object, it cannot be changed to refer to another
object. (Pointers can be pointed to another object at any
time.)
- You cannot
have NULL references. You must always be able to assume that a reference is
connected to a legitimate piece of
storage.
References in functions
The most common place you’ll see
references is as function arguments and return values. When a reference is used
as a function argument, any modification to the
reference inside the function will cause changes to the argument
outside the function. Of course, you could do the same thing by passing a
pointer, but a reference has much cleaner syntax. (You can think of a reference
as nothing more than a syntax convenience, if you want.)
If you return a
reference from a function, you must take the same care as if you return a
pointer from a function. Whatever the reference is connected to shouldn’t
go away when the function returns, otherwise you’ll be referring to
unknown memory.
//: C11:Reference.cpp
// Simple C++ references
int* f(int* x) {
(*x)++;
return x; // Safe, x is outside this scope
}
int& g(int& x) {
x++; // Same effect as in f()
return x; // Safe, outside this scope
}
int& h() {
int q;
//! return q; // Error
static int x;
return x; // Safe, x lives outside this scope
}
int main() {
int a = 0;
f(&a); // Ugly (but explicit)
g(a); // Clean (but hidden)
} ///:~
The call to f( )
doesn’t have the convenience and cleanliness of using references, but
it’s clear that an address is being passed. In the call to
g( ), an address is being passed (via a reference), but you
don’t see it.
const references
The reference argument in
Reference.cpp works only when the argument is a non-const object.
If it is a const object, the function g( ) will not accept
the argument, which is actually a good thing, because the function does
modify the outside argument. If you know the function will respect the
constness of an object, making the argument a
const reference will allow the function to be
used in all situations. This means that, for built-in types, the function will
not modify the argument, and for user-defined types, the function will call only
const member functions, and won’t modify any public data
members.
The use of const references in
function arguments is especially important because your function may receive a
temporary
object. This might have been
created as a return value of another function or explicitly by the user of your
function. Temporary objects are always const, so if you don’t use a
const reference, that argument won’t be accepted by the compiler.
As a very simple example,
//: C11:ConstReferenceArguments.cpp
// Passing references as const
void f(int&) {}
void g(const int&) {}
int main() {
//! f(1); // Error
g(1);
} ///:~
The call to f(1) causes a
compile-time error because the compiler must first create a reference. It does
so by allocating storage for an int, initializing it to one and producing
the address to bind to the reference. The storage must be a const
because changing it would make no sense – you can never get your hands on
it again. With all temporary objects you must make the same assumption: that
they’re inaccessible. It’s valuable for the compiler to tell you
when you’re changing such data because the result would be lost
information.
Pointer references
In C, if you want to modify the
contents of the pointer rather than what it points to, your function
declaration looks like:
void f(int**);
and you’d have to take the address
of the pointer when passing it in:
int i = 47;
int* ip = &i;
f(&ip);
With references in C++, the syntax is
cleaner. The function argument becomes a reference to a pointer, and you no
longer have to take the address of that pointer. Thus,
//: C11:ReferenceToPointer.cpp
#include <iostream>
using namespace std;
void increment(int*& i) { i++; }
int main() {
int* i = 0;
cout << "i = " << i << endl;
increment(i);
cout << "i = " << i << endl;
} ///:~
By running this program, you’ll
prove to yourself that the pointer is incremented, not what it points
to.
Argument-passing
guidelines
Your normal habit when passing an
argument to a function should be to pass by const reference. Although at
first this may seem like only an efficiency
concern (and you normally
don’t want to concern yourself with efficiency tuning while you’re
designing and assembling your program), there’s more at stake: as
you’ll see in the remainder of the chapter, a copy-constructor is required
to pass an object by value, and this isn’t always
available.
The efficiency savings can be substantial
for such a simple habit: to pass an argument by value requires a constructor and
destructor call, but if you’re not going to modify the argument then
passing by const reference only needs an address pushed on the
stack.
In fact, virtually the only time passing
an address isn’t preferable is when you’re going to do such
damage to an object that passing by value is the only safe approach (rather than
modifying the outside object, something the caller doesn’t usually
expect). This is the subject of the next
section.
 |
|