Am avut 371712 vizite de la lansarea siteului.




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:

  1. A reference must be initialized when it is created. (Pointers can be initialized at any time.)
  2. 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.)
  3. 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.

Here’s an example:

//: 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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)