As we’ve seen, a variable definition consists of a base type and a list of declarators. Each declarator can relate its variable to the base type differently from the other declarators in the same definition. Thus, a single definition might define variables of different types:
//
i is an int; p is a pointer to int; r is a reference to intint i = 1024, *p = &i, &r = i;
Many programmers are confused by the interaction between the base type and the type modification that may be part of a declarator.
It is a common misconception to think that the type modifier (*
or &
) applies to all the variables defined in a single statement. Part of the problem arises because we can put whitespace between the type modifier and the name being declared:
int* p; // legal but might be misleading
We say that this definition might be misleading because it suggests that int*
is the type of each variable declared in that statement. Despite appearances, the base type of this declaration is int
, not int*
. The *
modifies the type of p
. It says nothing about any other objects that might be declared in the same statement:
int* p1, p2; // p1 is a pointer to int; p2 is an int
There are two common styles used to define multiple variables with pointer or reference type. The first places the type modifier adjacent to the identifier:
int *p1, *p2; // both p1 and p2 are pointers to int
This style emphasizes that the variable has the indicated compound type.
The second places the type modifier with the type but defines only one variable per statement:
int* p1; // p1 is a pointer to int
int* p2; // p2 is a pointer to int
This style emphasizes that the declaration defines a compound type.
There is no single right way to define pointers or references. The important thing is to choose a style and use it consistently.
In this book we use the first style and place the *
(or the &
) with the variable name.
In general, there are no limits to how many type modifiers can be applied to a declarator. When there is more than one modifier, they combine in ways that are logical but not always obvious. As one example, consider a pointer. A pointer is an object in memory, so like any object it has an address. Therefore, we can store the address of a pointer in another pointer.
We indicate each pointer level by its own *
. That is, we write **
for a pointer to a pointer, ***
for a pointer to a pointer to a pointer, and so on:
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = π // ppi points to a pointer to an int
Here pi
is a pointer to an int
and ppi
is a pointer to a pointer to an int
. We might represent these objects as
Just as dereferencing a pointer to an int
yields an int
, dereferencing a pointer to a pointer yields a pointer. To access the underlying object, we must dereference the original pointer twice:
cout << "The value of ival
"
<< "direct value: " << ival << "
"
<< "indirect value: " << *pi << "
"
<< "doubly indirect value: " << **ppi
<< endl;
This program prints the value of ival
three different ways: first, directly; then, through the pointer to int
in pi
; and finally, by dereferencing ppi
twice to get to the underlying value in ival
.
A reference is not an object. Hence, we may not have a pointer to a reference. However, because a pointer is an object, we can define a reference to a pointer:
int i = 42;
int *p; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i to 0
The easiest way to understand the type of r
is to read the definition right to left. The symbol closest to the name of the variable (in this case the &
in &r
) is the one that has the most immediate effect on the variable’s type. Thus, we know that r
is a reference. The rest of the declarator determines the type to which r
refers. The next symbol, *
in this case, says that the type r
refers to is a pointer type. Finally, the base type of the declaration says that r
is a reference to a pointer to an int
.
It can be easier to understand complicated pointer or reference declarations if you read them from right to left.