By Julienne Walker
License: Public Domain
Most newcomers to programming have trouble with the
concept of pointers. This is almost entirely due to the reputation that pointers
have for being difficult, and they are approached with a feeling of apprehension.
That feeling is completely unfounded, as pointers are really very simple. However,
pointers are also very powerful, and the potential for abuse and very difficult
to find errors exists. It is this potential that has earned pointers the bad reputation
that they have. In this tutorial, I will attempt to describe pointers in as much
of their entirety as reasonable.
Memory addressing (abstract)
Memory in a computer system is basically just an array
of addresses. Each address typically represents a single byte (8 bits, also called
an octet, on many systems). This is called byte-addressable memory, and each address
has an offset, or index, to represent its location in the physical memory space.
The total addressable memory is determined by the address bus size of the of the
processor, which has a limit of 2n bytes.
Put simply, memory can be viewed as an array of unsigned
char, where unsigned char represents a byte. On the newer Pentium processors (Pro,
II, III, and IV), the address bus supports 36 bits, so the array could be represented
in C as:
1 #define ADDRESS_BUS_SIZE 36 /* Pentium 4: 36-bit address bus */
2 unsigned char memory[1 << ADDRESS_BUS_SIZE];
Naturally, this code is unlikely to actually run,
but if you know how to use arrays, it gives you an idea of how addresses in memory
work. Now, this is only part of the concept of memory. Even though most systems
are byte-addressable, it makes sense for the processor to move as much data arround
as possible. This is done by the data bus, and the size of the data bus is where
the names 8-bit system, 16-bit system, 32-bit system, 64-bit system, etc.. come
from. When the data bus is 8 bits wide, it can transfer 8 bits in a single memory
operation. When the data bus is 32 bits wide (as is most common at the time of writing),
at most, 32 bits can be moved in a single memory operation.
This is a good thing because now larger values can
be loaded from memory in the same amount of time that it takes to load a byte on
an 8-bit data bus. However, it introduces the problem of alignment. While some systems
go out of their way to guarantee that a byte-addressable system can load a multi-byte
value from any arbitrary address, many do not, and even those that do have performance
penalties for accessing a byte with an unexpected alignment. Consider this example
where memory is divided into two byte integers:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define ADDRESS_BUS_SIZE 20 /* 2^20 address bus */
5 unsigned char memory[1 << ADDRESS_BUS_SIZE];
6
7 int main ( void )
8 {
9 int i;
10 unsigned short val = 12345;
11
12 for ( i = 0x12340; i < 0x1234A; i += 2 ) {
13 memcpy ( &memory[i], &val, sizeof val );
14 ++val;
15 }
16
17 val = 0;
18
19 for ( i = 0x12340; i < 0x1234A; i += 2 ) {
20 memcpy ( &val, &memory[i], sizeof val );
21 printf ( "%hu\n", val );
22 }
23
24 return 0;
25 }
This naturally assumes that unsigned short is two
bytes, but at the time of writing, this is a fairly safe assumption. Looking at
the memory as two byte pairs, it is fairly easy to see that problems can occur when
odd addresses are accessed directly. While such an operation may be legal depending
on the processor, it may take extra instructions to disambiguate the request from
the more common even-address operation:
Byte1 Byte2
-----------------
0x12340 | 0x12341 /* Contains 12345 */
0x12342 | 0x12343 /* Contains 12346 */
0x12344 | 0x12345 /* Contains 12347 */
0x12346 | 0x12347 /* Contains 12348 */
0x12348 | 0x12349 /* Contains 12349 */
Let us say that a program wants to read an unsigned
short from address 0x12343. If the data bus always places even
addresses in Byte1, and odd addresses in Byte2,
a request for a starting byte at the odd address would require the processor to
first read 0x12343 into Byte2, then 0x12344
into Byte1. Because this is the inverse of what is wanted, the
processor must then swap the addresses (or the contents of the addresses). As expected,
this takes more time than a memory request from an even address.
This is what people mean when they talk about alignment.
It's not directly relevant to a discussion of pointers, but an understanding of
how memory works simplifies pointer arithmetic.
Concept of pointers (abstract)
Pointers are variables that hold an address. That's
it! The secret behind the infamous pointer is nothing more than “variables
that hold an address”. Given the collective dread that programmers seem to
attribute to the complexity of pointers, the reality is somewhat anticlimactic.
When we speak of a pointer, we really mean a variable
that holds an address. When we talk about a pointer “pointing to” something,
we mean whatever object resides at that address. To “dereference” a
pointer is to retrieve the value of the object at the address.
Because a pointer is a variable, there are actually
four parts to it. The direct value of a pointer is an address. The indirect value
of a pointer is the value at that address. The direct address of a pointer is the
address of itself, and the indirect address of a pointer is the same as the value
of the pointer. So a pointer has an address, and a pointer holds an address. Therefore,
a pointer can point to another pointer. The logic may seem circular, but it makes
sense when you think about it for a bit.
What are pointers?
A pointer is simply a variable that refers to another
object in memory. In other words, a pointer is a variable that contains a memory
address, much like an integer is a variable that contains a whole number. Once this
single fact is understood, the entire concept becomes much more manageable. However,
because a pointer contains a memory address, it actually has two values,
unlike the integer. The first value is the address itself; this is the direct
value of a pointer. The second value is the contents of the address; this is the
indirect value of a pointer. Assume that we have a pointer p (represented
as an index) that contains an address 0x12340. Using the array
example, it might look like this:
1 p = 0x12340;
2 memory[p] = 212;
Notice that the immediate value of p
is the address, 0x12340, but through p we can
assign 212 to the integer at that address. The act of accessing
memory through a pointer is called indirection (accessing the indirect value), or
dereferencing. At first glance, this entire concept may seem overly complicated
for no gain. If you have access to the original variable, what need is there for
a pointer to its address? Well, sometimes you do not have access to the original
variable except through a pointer. For example, in C, arguments are passed
to a function by value, meaning that a copy of the variable's value is made and
assigned to a new variable. Under this scheme, there is no way to change the value
of the original variable.
However, if you pass a pointer to the variable by
value, a copy of the address is made instead of the value of the variable, and you
can use indirection to access the original variable's contents. This functionality
is very consistent with pointers. If you need to change the address of a pointer
when you do not have access to the original pointer, a pointer to that pointer can
be used:
1 p1 = 0x12340;
2 memory[p1] = 0x12342;
3 memory[memory[p1]] = 212;
This example is somewhat convoluted, but the key to
understanding it is to notice that p1 is a pointer, but so is
memory[p1]. This is an example of double indirection, accessing memory
through two pointers. The first pointer, p1, refers to the second
pointer, memory[p1], which then refers to the address of an integer
where 212 can be assigned. Single and double indirection are the
most common operations, though occasionally you might see more levels of indirection.
Fortunately, the concept is scalable and consistent to however many levels of indirection
you might need.
Uses of pointers (abstract)
The myriad ways that pointers can be used is overwhelming
at first. However, they can be broken down into a few broad categories of common
use:
1) Creating pass-by-reference semantics
In languages such as C, where arguments to a function
are passed only by value, a copy of the value is made and assigned to a new variable.
Under such a scheme, it is impossible to change the value of the original object
from within a function because the function body only sees a copy of it. For example
(using a fictional programming language):
function increment: x
begin
x := x + 1
end
program
begin
i := 0
while i < 10 do
increment ( i )
print i
loop
end
This will print 0 infinitely (or
until some outside power stops the program). The reason is because i
is not actually passed to increment, its value is. The value is then copied into
x. Inside increment, x is incremented
by one and then promptly destroyed when the function returns. Back in the main program,
i still has the value 0 because a completely separate
variable was incremented. Removing all of the hidden framework, the example would
look like this:
program
begin
i := 0
while i < 10 do
x := i
x := x + 1
print i
loop
end
The error is more obvious with this example. To fix
the problem in a language that only supports pass-by-value semantics, you must pass
the address of the object by value. A copy of the address is made, but because the
address is still that of the original object, it can be dereferenced and the original
value can be changed. The example could be corrected like so:
function increment: pointer x
begin
deref x := deref x + 1
end
program
begin
i := 0
while i < 10 do
increment ( addressof i )
print i
loop
end
2) Walking through memory
With a pointer to an address, it makes sense that
you might want to move forward to the next address or backward to the previous address.
Using pointer arithmetic, this is possible, and often useful. Most of the time,
this technique is used to walk across the elements of an array.
3) Referring to dynamic memory
Because dynamic memory is not bound to a variable,
there is no way of accessing it without some form of reference to the memory. This
is an ideal use for pointers. Simply assign the beginning address of the memory
block to a pointer, and use dereferencing to access that memory. This technique
is used very often in both C and C++ for such tasks as creating arrays that grow
and shrink on demand.
4) Creating linked data structures
While arrays are simple to use, they are not well
suited to operations that require insertions or removals from anywhere but the end.
This is because to make a hole for insertions, or to remove a hole for removals,
a potentially expensive shifting of elements is required. Pointers can be used to
link together non-contiguous blocks of memory so that they can be treated as if
they were all adjacent.
Declaration and syntax
To declare a pointer, simply place an asterisk between
the type and identifier of a variable. Any number of levels of indirection can be
created by adding more asterisks:
1 int *i; /* Pointer to int */
2 double *d; /* Pointer to double */
3
4 char **p; /* Pointer to pointer to char */
Multiple pointers can be declared on the same line,
but care must be taken to use the asterisk for every variable:
1 int *p, *q;
If the asterisk is omitted, then the variable without
an asterisk is not a pointer:
1 int *p, q; /* p is a pointer, but q is not! */
A pointer can have two levels of constness. The first
level is for the object being pointed to, and in such a case, the keyword const
will need to be inserted on the left side of the asterisk. As usual, const can be
on either side of the type:
1 const int *i; /* Pointer to const int */
2 int const *j; /* Pointer to const int */
When a pointer points to a const type, the pointer
can be changed, but the value pointed to cannot. To create a const pointer, place
the const keyword on the right side of the asterisk, but before the identifier:
1 int * const i; /* Const pointer to non-const int */
These two levels can be combined to create a const
pointer to const:
1 const int * const i; /* Const pointer to const int */
The two operators needed for working with a pointer
are the indirection operator, *, and the address-of operator,
&. The address-of operator is a unary operator that returns the address
of an object that has an address. It basically “makes a pointer” out
of an object by evaluating to an address that can be assigned to a pointer:
1 int i;
2 int *p;
3
4 p = &i; /* p now "points to" i */
The indirection operator does just that; it dereferences
a pointer. Provided the pointer has been assigned an address, indirection will return
the expected value:
1 int i = 123;
2 int *p = &i;
3
4 printf ( "%d\n", *p ); /* Prints 123 */
A pointer in C and C++ can have one of three values.
First, a pointer can point to the address of an object. This is the most common
value. Second, a pointer can point to nothing. This is the so called, “null”
pointer. Last, a pointer can point to a random address. This is the initial state
of a local pointer, and is the cause of many problems for beginners. If a pointer
points to a random address, such as by declaring the pointer but not intializing
it, and then dereferences it, the behavior of the program will be unpredictable.
Therefore, it is critical that a pointer be initialized before it is used.
Here is a complete example that shows these operations
in action:
1 #include <stdio.h>
2
3 int main ( void )
4 {
5 int i = 123;
6 int j = 321;
7 int *p = &i;
8 int *q;
9
10 printf ( "i is %d\n", i );
11 printf ( "*p is %d\n\n", *p );
12
13 q = p; /* q points to p's value, which is i */
14
15 printf ( "i is %d\n", i );
16 printf ( "*p is %d\n", *p );
17 printf ( "*q is %d\n\n", *q );
18
19 /* Increment i three times */
20 ++*p;
21 ++*q;
22 ++i;
23
24 printf ( "i is %d\n", i );
25 printf ( "*p is %d\n", *p );
26 printf ( "*q is %d\n\n", *q );
27
28 /* Reassign p */
29 p = &j;
30
31 printf ( "i is %d\n", i );
32 printf ( "*p is %d\n", *p );
33 printf ( "*q is %d\n\n", *q );
34
35 return 0;
36 }
Types of pointers
Astute readers may have noticed that despite the variable
being a pointer, it still has a type. Every pointer in C and C++ must have a type.
The type of the pointer determines what operations are allowed, and how those operations
are carried out.
Null pointers
The first type is actually a value, the null pointer
that was mentioned earlier. A null pointer is created by assigning the integer literal
0 to the pointer. This does not actually cause the pointer to point
to the address 0x0, despite the common misconception that the bit pattern of a null
pointer is all bits zero. It is considered good style to initialize all pointers
to either an object's address, or null when first declared:
1 int i;
2
3 int *p = &i; /* Points to i */
4 float *q = 0; /* Null pointer */
For your convenience, several headers define a macro
called NULL that correctly defines a null pointer value. The most
common headers for this macro are stdio.h, stdlib.h,
and stddef.h. (the C++ headers would be cstdio,
cstdlib, and cstddef). Using NULL,
it is well understood that a null pointer is intended:
1 #include <stddef.h>
2
3 float *q = NULL;
As expected, a null pointer cannot be dereferenced
or bad things will happen. Unpredictable bad things. But always bad things. No good
can come from dereferencing a null pointer just as no good can come from dereferencing
an uninitialized pointer.
Pointers to basic types
A pointer can point to any of the built-in “simple”
types, such as int, char, double,
float, long, and short. Pointers
differentiate between these types because any movement of the pointer will take
into account the size of the object being pointed to, and scale the movement accordingly.
So if an int is four bytes, moving a pointer to int forward in memory by two will
actually move the pointer by eight bytes instead of two. Likewise, a pointer to
char would move forward by two bytes because the size of a
char is always 1.
Pointers to void
In order to fulfill the obvious need of a generic
pointer, the pointer to void was introduced. A pointer to
void cannot be dereferenced, but it makes for a convenient (in C) way
to move around pointers that could be of any type. As an example, the standard library
function malloc returns a block of suitably aligned memory in the
form of a pointer. However, because malloc cannot know what type
of pointer is required, a pointer to void is returned instead.
Pointers to void are generally not
used as much in C++ because other, less awkward solutions exist. In C, however,
the pointer to void is the simplest way to create and maintain
a heterogeneous data structure. Pointers to void are move convenient
in C because a cast not required to convert to and from a pointer to void.
For example:
1 int *p = malloc ( 100 * sizeof *p ); /* Allocate 100 integers */
In C++, this line of code would need to include a
cast to int*:
1 int *p = static_cast<int*> ( malloc ( 100 * sizeof *p ) );
Pointers to user-defined types
A pointer to structure or class creates a new problem.
Because user-defined data types allow for member access using the .
operator, and the member access operator has higher precedence than the indirection
operator, the only way to access a member of a user-defined type through a pointer
is to surround the indirection with parentheses and then apply the member access
operator to the result:
1 struct s {
2 int i;
3 };
4
5 struct s t;
6 struct s *p = &t;
7
8 (*p).i = 123;
Fortunately, the designers of C realized how awkward
this is and added a special member access operator for pointers to user-defined
types. The so called “arrow” operator is this addition:
1 struct s {
2 int i;
3 };
4
5 struct s t;
6 struct s *p = &t;
7
8 p->i = 123;
Pointers to pointers
As mentioned several times before, a pointer can point
to another pointer because a pointer is just another variable with an address. The
syntax is consistent and intuitive:
1 int i = 123;
2 int *p = &i;
3 int **pp = &p;
4
5 printf ( "%d\n", **pp );
Pointers to functions
Many people are surprised to learn that functions
have an address. Therefore, it is possible to have a pointer to a function. This
is incredibly useful for providing callback functions where a given function performs
an operation that the client code defines. A standard example is qsort
in the standard C library header stdlib.h:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #define length(x) ( sizeof ( x ) / sizeof *( x ) )
5
6 int compare ( const void *a, const void *b )
7 {
8 const int *ia = a;
9 const int *ib = b;
10
11 if ( *ia < *ib )
12 return -1;
13 else if ( *ia > *ib )
14 return +1;
15 else
16 return 0;
17 }
18
19 int main ( void )
20 {
21 int a[] = {9,8,7,6,5,4,3,2,1,0};
22 int i;
23
24 for ( i = 0; i < length ( a ); i++ )
25 printf ( "%-2d", a[i] );
26 printf ( "\n" );
27
28 qsort ( a, length ( a ), sizeof a[0], compare );
29
30 for ( i = 0; i < length ( a ); i++ )
31 printf ( "%-2d", a[i] );
32 printf ( "\n" );
33
34 return 0;
35 }
Notice that in the call to qsort,
we are passing the name of a function. The address-of operator is not required because
the compiler can deduce whether or not the function is being called, and if a function
is not being called then the only other valid operation that can be performed on
it is taking its address. Passing a function to handle some application specific
detail is a very useful trick. Because qsort only works with pointers
to void, it cannot know what types are actually being sorted. Therefore,
it uses a callback comparison function that handles this need.
A pointer to function is declared just like a regular
function except an asterisk precedes the function name, and both the name and asterisk
must be surrounded with parentheses:
1 void (*fp) ( int );
In this example, fp is a variable.
It is a pointer to a function that takes an integer argument and returns no value.
Without the parentheses, around *fp, it would be a declaration
of the function fp that takes an integer argument and returns a
pointer to void. This is a big difference, so take care in placing
the parentheses when declaring a pointer to function.
Pointers to arrays
A pointer can point to an array, but the syntax is
awkward enough that this feature is not often used. To create a pointer to an array,
simply declare an array and then, just as with a pointer to a function, add an asterisk
and surround both the name and the asterisk with parentheses:
1 int (*pa)[10];
This example declares a pointer to an array of ten
integers. The awkwardness comes from trying to subscript a pointer to an array,
where the same problem as with pointers to user-defined types rears its head again.
Unfortunately, this time there is no operator to save the day:
1 int a[5] = {1,2,3,4,5};
2 int (*pa)[5] = &a;
3
4 printf ( "%d\n", (*pa)[2] ); /* Prints 3 */
Pointers to members (C++ only)
A pointer to member is a way to indirectly access
a member through an object or pointer to object. In other words, you can use a pointer
to member to access a member only knowing its type. The syntax is esoteric, at best:
1 #include <iostream>
2
3 struct s {
4 int i;
5 void f() { std::cout<<"i is "<< i <<'\n'; }
6 };
7
8 int main()
9 {
10 s a, b;
11
12 int s::*pi = &s::i;
13 void (s::*pf)() = &s::f;
14
15 a.*pi = 10;
16 b.*pi = 20;
17
18 (a.*pf)();
19 (b.*pf)();
20 }
A more realistic use is printing the contents of a
map. Since the value of a map element is a
pair, you might want to print a columnar list with the first value on
each column of a row and the second value on the same column of the next row. A
clever programmer would factor this out into a single function using a pointer to
member:
1 #include <iomanip>
2 #include <iostream>
3 #include <map>
4 #include <utility>
5
6 void print ( std::map<int, int>& table, int std::pair<int, int>::*item )
7 {
8 std::map<int, int>::iterator it = table.begin();
9
10 while ( it != table.end() ) {
11 std::cout<< std::left << std::setw ( 3 ) << ( (std::pair<int, int>)*it ).*item;
12 ++it;
13 }
14 }
15
16 int main()
17 {
18 std::map<int, int> table;
19
20 for ( int i = 0; i < 10; i++ )
21 table[i] = i * 10;
22
23 print ( table, &std::pair<int, int>::first );
24 std::cout<<'\n';
25 print ( table, &std::pair<int, int>::second );
26 std::cout<<'\n';
27 }
Now instead of maintaining two loops, or two functions,
or something worse, the programmer only needs to control the formatting from a single
loop. However, this example is filled with complicated features, and it is somewhat
brittle. In my experience, pointers to members are not used often. I have yet to
see them used in any production code.
Pointer arithmetic
Arithmetic on pointers is very simple, you can add
an integer to a pointer, subtract an integer from a pointer, and subtract a pointer
from a pointer. These operations are only valid on arrays, or blocks of contiguous
memory owned by the program. Anything else would be accessing memory outside of
the program's address space and would then be subject to unpredictable behavior.
Adding an integer to a pointer causes the pointer
to move “forward”, toward the end of the array. You can add any integral
value to a pointer as long as it does not exceed the boundaries of the array:
1 #include <stdio.h>
2
3 int main ( void )
4 {
5 int a[] = {1,2,3,4,5};
6 int *p;
7
8 for ( p = a; p != a + 5; p++ )
9 printf ( "%d\n", *p );
10
11 return 0;
12 }
This example introduces three important concepts.
The first, that the address-of operator is not required to assign the address of
an array to a pointer, will be discussed later. The second concept is that of a
past-the-end address. C and C++ guarantee that every array allocates an address
past the end that can be used for comparison. Note that you can compare with the
address, but the value of the past-the-end element is off limits in all cases. The
last important concept is that pointers can be compared with the relational operators,
but the details will be provided later. The next example does the same thing as
the previous example except it increments the pointer by two instead of one:
1 #include <stdio.h>
2
3 int main ( void )
4 {
5 int a[] = {1,2,3,4,5};
6 int *p = a, *q = a + 5;
7
8 printf ( "%td\n", p - q );
9 printf ( "%td\n", q - p );
10
11 return 0;
12 }
Notice that an even number of elements is used because
incrementing the pointer by two would exceed the boundaries of the array. The comparison
with the past-the-end element would never be made, and an infinite loop would be
created as a result.
Subtraction of an integer from a pointer works exactly
the same as addition. The only difference is that there is no past-the-end element
for the front of an array. Use caution when walking backward in an array with a
pointer. The interesting part of subtraction comes when a pointer is subtracted
from a pointer. Both pointers must point to elements within the same array. The
result of subtraction is the distance between the two pointers. The result can be
either negative or positive depending on the relationship of the pointers:
1 #include <stdio.h>
2
3 int main ( void )
4 {
5 int a[] = {1,2,3,4,5};
6 int *p = a, *q = a + 5;
7
8 printf ( "%td\n", p - q );
9 printf ( "%td\n", q - p );
10
11 return 0;
12 }
The result type of pointer subtraction is ptrdiff_t,
a typedef for a signed integral value. Note that most C compilers at the time of
writing will not support the type specifier for printf that correctly prints a
ptrdiff_t value (it was added in the C99 standard). On those compilers,
a hack to force the value to the largest possible signed integer is required:
1 #include <stdio.h>
2
3 int main ( void )
4 {
5 int a[] = {1,2,3,4,5};
6 int *p = a, *q = a + 5;
7
8 printf ( "%ld\n", (long)( p - q ) );
9 printf ( "%ld\n", (long)( q - p ) );
10
11 return 0;
12 }
Pointer comparisons
A pointer comparison only compares the direct value
of two pointers. Pointers can only be compared if they point to the same array;
otherwise, the relative position of the pointers is irrelevant. However, tests for
equality using == and != are always legal, because
a pointer either points to the same location in memory, or it does not.
What are references? (C++ only)
A reference in C++ is another name for an object.
Because it's a synonym, it has to have something to be a synonym for, therefore
a reference must always refer to an object. At first glance, it might seem like
references are a cleaner version of pointers, where you do not have to use the address-of
operator or dereference the resulting reference, but in reality there are subtle
differences.
Uses of references (abstract)
References were designed almost entirely to support
operator overloading. Therefore, the best use of them is to replace pointers in
achieving pass-by-reference semantics. The general recommendation for C++ is to
use references when you can, and pointers when you must. One of the most important
issues with references is that once bound to an object, it cannot be rebound to
refer to another object. There is also no such thing as a null reference, so if
either of those features are needed, a pointer must be used instead of a reference.
Declaration and syntax
A reference is, confusingly, declared much like a
pointer, except using an ampersand instead of an asterisk. This is confusing because
the ampersand is also used as the address-of operator with pointers. A reference
must be initialized, therefore there is no such thing as an uninitialized reference:
1 #include <iostream>
2
3 int main()
4 {
5 int i = 123;
6 int& s = i;
7
8 std::cout<< s <<'\n';
9 }
Because a reference is just another name for the object
it refers to, there are no operations on references, per se. This makes using references
very simple because you can just treat them like the type they refer to.
Types of references
As can be expected, because a reference to T must
act like a T, it is required that a reference have a type. The good news is that
the syntax for declaring references is nearly identical to the syntax for declaring
pointers.
References to basic types
The syntax for a reference to a built-in simple type
should be obvious:
1 int i;
2 double d;
3 char c;
4 int& r = i;
5 double& s = d;
6 char& t = c;
References to user-defined types
Unlike with pointers, references to user-defined type
can use the member access operator and do not need a special operator, as pointers
need the arrow operator:
1 struct s {
2 int i;
3 };
4
5 struct s t;
6 struct s& r = t;
7
8 r.i = 123;
References to pointers
Because the type of a reference must be on the left
side of the ampersand in a reference declaration, and the asterisk of a pointer
is a part of the type, the declaration for a reference to a pointer might look a
little funny:
1 char *p;
2 char*& r = p;
This is logical, but not immediately obvious at first
glance. It helps to read the declaration from right to left. When you do that, it
makes more sense: r is a reference to a pointer to char.
References to pointers eliminate many needs for pointers to pointers. In fact, many
C++ programmers have little or no experience with pointers to pointers, much to
the surprise of C programmers, who use them regularly. Pointers to references are
not legal, nor are references to references (at the time of writing, though this
is the topic of a defect report and will likely be changed in the next revision
of the C++ standard).
References to functions
While references to functions are legal, they are
almost never used because they have no benefit over pointers to functions. To declare
a reference to a function, simply take the declaration of a pointer to a function
and change the asterisk to an ampersand.
References to arrays
A reference to an array is far more useful than a
pointer to an array. The syntax is, like references to functions, a pointer to an
array with the asterisk replaced by an ampersand:
1 #include <iostream>
2
3 void print ( int (&ra)[5] )
4 {
5 for ( int i = 0; i < sizeof ra / sizeof ra[0]; i++ )
6 std::cout<< ra[i] <<'\n';
7 }
8
9 int main()
10 {
11 int a[] = {1,2,3,4,5};
12
13 print ( a );
14 }
The biggest benefit to using a reference to an array
over a pointer to an array is that the sizeof trick for determining
the size of an array works as expected.
Using pointers
Theory and syntax is all well and good, but all of
the clever ways for using pointers in real code can be a shock to newcomers. In
this part of the tutorial, I will cover some of the more common ways of using pointers,
as well as common pitfalls and solutions to those pitfalls. Many of these solutions
can be found in FAQ documents in one form or another as the answer to a common question.
Pointers and functions
As described several times before, one common use
for pointers is to simulate pass-by-reference semantics when the only choice is
pass-by-value. No tutorial on pointers would be complete without the canonical swap
example. Assume that you want to swap two integers. A first attempt might look like
this:
1 #include <stdio.h>
2
3 void swap ( int a, int b )
4 {
5 int save = a;
6 a = b;
7 b = save;
8 }
9
10 int main ( void )
11 {
12 int a = 10;
13 int b = 20;
14
15 printf ( "a: %d, b: %d\n", a, b );
16 swap ( a, b );
17 printf ( "a: %d, b: %d\n", a, b );
18
19 return 0;
20 }
Of course, that does not work because arguments are
passed by value in C. The solution is to pass pointers to int,
so that the address can be dereferenced and the actual value modified instead of
a copy:
1 #include <stdio.h>
2
3 void swap ( int *a, int *b )
4 {
5 int save = *a;
6 *a = *b;
7 *b = save;
8 }
9
10 int main ( void )
11 {
12 int a = 10;
13 int b = 20;
14
15 printf ( "a: %d, b: %d\n", a, b );
16 swap ( &a, &b );
17 printf ( "a: %d, b: %d\n", a, b );
18
19 return 0;
20 }
This works as expected, though the code is no longer
as clean. Being able to change the value of an object from within a separate function
is the first reason for using pointers to pass an object to a function. The same
rule applies for pointers. Assume that you want to pass a pointer to a function
to allocate memory to it. The following does not work:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void alloc ( int *p )
5 {
6 p = mall