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