PointersReferencesAndArrays

From Cppwiki

Jump to: navigation, search
Thanks to Stefan Hoehne for some of his pointers (pun intended).
Thanks to David A. Caabeiro for making sure that I noted that an array is not exactly a const pointer and for bringing up the topic of string literals.
Jey suggested that I add a bit about function templates and references to arrays. If you say so, Jey... :)

Contents

Terminology note

Note: If you're not already familiar with object-oriented programming, you might want to skip this section.

Object-oriented programming (OOP) usually describes an object as an instance of a class. On the other hand, I use the Standard C++ definition of "object" as a region of storage, regardless of whether it's a class instance, an int, a pointer, or any other type:

int i;            // "i" is an int object
int *q = new int; // the object "q" points to an unnamed int object
const int c_i;    // "c_i" is a const int object

OOP terminology only covers classes and objects, not ints and pointers and so on. Outside of OOP, many people use the term variable for these things, but there's some ambiguity there:

int *q = new int; // q is obviously a variable, but what about "new int"?
const int c_i;    // c_i is a variable, but it's not variable!

Pointers

A pointer is used for representing a memory address. Similar to a postal address, a memory address refers to the location where an object resides in the computer's memory. If a pointer contains the address of an object, then it is said to point to that object, and it can be used to access and modify that object. Also, a pointer may be made to point to different objects at different times while your program is running.

To declare a pointer, use the * operator. Variable declarations are read right-to-left, resulting in the following syntax:

int *p;     // p is a pointer (*) to int

Note that * is said to bind to the right, meaning that it should be associated with whatever appears to its immediate right. This can produce somewhat unexpected results:

int *p, q; // * binds to p: p is pointer to int, but q is int

For this reason, it is often recommended to declare only one variable per line, in order to reduce confusion:

int *p; // p is clearly a pointer-to-int
int q;  // q is clearly an int

The expression &i is read as "the address of i" and results in a pointer to i that you can store into a pointer variable:

int i;      // i is an int
p = &i;     // store the address of i into p

Once a pointer has been declared and made to point to an object, you can then use the * operator to dereference the pointer. That is, if p points to an object, then the expression *p results in that object. In our example, "that object" happens to be our variable i:

*p = 12345; // store 12345 into the int at address p, which is our int "i"
cout << i;  // output the value of i, which is 12345

If you declare a pointer but do not give it a value, it contains an unspecified value. Also, if a pointer points to an object that ceases to exist for some reason, it becomes a dangling pointer. Both of these conditions result in invalid pointers.

You may not dereference an invalid pointer, nor copy its value, nor even inspect its value. To do so would evoke undefined behavior in your program. That is, your program may work as expected, or it may do weird things, or (if you're lucky) it may crash.

int *invalidptr;       // invalidptr contains an unspecified value
*invalidptr = 12345;   // ERROR: never dereference an invalid pointer
int *p = invalidptr;   // ERROR: never copy an invalid pointer
if (p == invalidptr) { // ERROR: never inspect the value of an invalid pointer
}

If you end up with an invalid pointer, you should probably make it point to something else. If you have nothing else to which to make it point, then it should be made a null pointer. To do this, assign to it the value 0 or the standard identifier NULL (defined in <cstddef> in C++ or <stddef.h> in C). Null pointers can be copied, and their values can be inspected to determine if they're null:

int *nullptr = 0; // nullptr contains the null value
*nullptr = 12345; // ERROR: never dereference a null pointer
int *p = nullptr; // OK: you may copy a null pointer
if (p != 0) {     // OK: you may inspect the value of a null pointer
}

References

A reference is an alias to another variable, called its referent. An alias, of course, is just another name for something. For instance, the American writer Samuel Langhorne Clemens is better known by his alias, Mark Twain:

Person SamuelLanghorneClemens;
Person &MarkTwain = SamuelLanghorneClemens; // just another name

Once a reference has been initialized, it is completely indistinguishable from its referent. If you try to access or modify the value of a reference, you will actually be accessing or modifying the value of its referent. Here is a rewrite of the example in the Pointers section, using references:

int i;        // i is an int and can contain an integer value
int &ref = i; // ref is another name for i
ref = 12345;  // assign 12345 to i (also known as ref)
cout << i;    // output the integer stored in i (which is 12345)

As an aside, a reference is often described as a relative of the pointer. This is true, to an extent, since both are ways to refer indirectly to another variable. However, this is really just incidental to the concept of a reference. In particular, note that a pointer can initially point to one object and later be reseated, or made to point to another object. On the other hand, a reference cannot be reseated -- once it has been initialized, again, it is completely indistinguishable from its referent. Also, there is no such thing as a "null reference" -- all references must be initialized with a valid object.

Arrays

Let's start with this:

int array[3];

Here, array is an int[3], or an array of three ints. The expression array[i] is used to represent the ith element in the array, where i is an offset from the beginning of the array. In this case, array[0] would be the first element and array[2] would be the last. Note that the index of the last element is one less than the number of elements -- array[3] is actually outside of the array, and it may in fact refer to another variable entirely. In other words, never access array[3] in a three-element array. :-)


An array is said to decay to a pointer to its first element (i.e., array[0]) when necessary, as when it is assigned to a pointer variable or passed to a function which takes a pointer as a parameter. Also, a pointer can be dereferenced using array notation: *array is equivalent to array[0], and *(array + i) is equivalent to array[i]. Because of this, arrays and pointers are generally very similar and often (though not always) are even interchangeable. For instance, although a C++ program must specify the sizes of array variables, you can use a pointer and create an array based on input:

cout << "Enter a number: ";
int n;
if (cin >> n) {
  int *pArray = new int[n];                  // create n ints
  for (int i = 0; i < n; ++i) pArray[i] = 0; // set n ints to 0
  delete[] pArray;                           // destroy n ints
}

Here are some things you can (and cannot) do with int array[3];:

array = new int[10];     // wrong: array cannot be changed
array[i] = 12345;        // store 12345 into array[i]
*(array + i) = 12345;    // same
int *p1 = array;         // store &array[0] into p1
int *p2 = array + i;     // store &array[i] into p2
int (*pa1)[3] = &array;  // int(*)[3] means "pointer to int[3]"
int (&ra1)[3] = array;   // int(&)[3] means "reference to int[3]"
int *pa2[3] = &array;    // wrong: pa2 is (int *)[3] (array of 3 pointers-to-int)
int &ra2[3] = array;     // wrong: ra2 is (int &)[3] (invalid C++)

One might almost say that an array is a const pointer to its first element, except for the following:

  • sizeof(int[3]) is the size of three ints, but sizeof(int * const) is just the size of a pointer, even if it points to three ints.
  • You cannot use a int * const where an int[3] is absolutely required:
int * const pInt = array; // pointer to first element of array
int (*pArray)[3] = &pInt; // wrong: pInt is not an int[3] (even though array is)
int (&rArray)[3] = pInt;  // wrong: same reason
  • When C++ must choose between treating an array as an array or treating it as a pointer, it will always treat it as an array. For instance, the address of an array is a pointer to an array, not a pointer to a pointer:
int * const *ppInt1 = &pInt;  // "&pInt" is a pointer to an int * const
int * const *ppInt2 = &array; // wrong: "&array" is a pointer to an array
int (*ppInt3)[3] = &array;    // correct, ppInt3 is a pointer to an array

Of course, you can do the following:

int * const &rpInt = array;   // rpInt refers to array as an int * const

The difference is that array decays to int * const, but &array does not decay to int * const *; rather, it is strictly int(*)[3], or a pointer to an int[3].

Special case: string literals

String literals are arrays of const char. As an example, the string literal "hello" is a const char[6] -- there are five characters in the string itself, plus a null terminator (the character whose value is 0, often written '\0') which C string functions like strcpy() use to determine the end of the string.

Passing arrays to functions

Here's a case where an array is a pointer:

You cannot pass an array by value in C++ (generally speaking). A function declaration which seems to take an array by value is actually taking a const pointer. This means that the following are all identical as far as C++ is concerned:

void takes_array (int array[]);
void takes_array (int array[12345]);  // *never* use this
void takes_array (int * const array);

Notice the second version: It has a size specified, and yet it is equivalent to the other two, which do not. Passing an array pointer by value loses any array dimension information originally associated with the array. Make sure not to get bitten by this one, which actually compiles under standard compilers:

void takes_10_ints (int a10[10]);
int main () {
  int a5[5];
  takes_10_ints(a5); // oops: a5 doesn't have 10 elements!
}

You should always enforce array sizes. Unfortunately, you need to use some other mechanism to do this.

The pointer-length method

The canonical way to pass an array to a function takes a pointer to a block of elements, like takes_array() above, as well as the number of elements in that block. Inside the function, just use array notation on the pointer. For instance:

void f (int p[], int n) {
  for (int i = 0; i < n; ++i) p[i] = 0; // set all elements to 0
}

Let's say that you are given the following variable declarations:

// "extern" means "defined elsewhere"
extern const int N;     // dimension of one array
extern const int M;     // dimension of another array (N != M)
extern int i;           // index of an element
extern int n;           // number of elements
int arrN[N];            // array of N ints
int arrM[M];            // array of M ints
int *ptrN = new int[N]; // pointer to array of N ints

You can then use f() in the following ways:

f(arrN, N);           // all elements of arrN
f(arrN, n);           // first n elements of arrN
f(arrN + (N - n), n); // last n elements of arrN
f(arrN + i, n);       // n elements of arrN, starting at index i
f(arrM, M);           // also works on arrays of any size
f(ptrN, N);           // also works on dynamic arrays

The pointer/reference-to-array method

Alternatively, you can take a pointer/reference to an array, but this is rather limiting. Here's one which takes a pointer to an array of N ints:

void g (int (*p)[N]) {
  for (int i = 0; i < N; ++i) (*p)[i] = 0; // set all elements to 0
}

And here's what you can (and cannot) do with it:

g(&arrN); // all elements of arrN
g(&arrM); // wrong: g() only works on int[N], not int[M]
g(&ptrN); // wrong: g() only works on int[N], not int *

There's no measurable speed or size increase, and you lose a lot in terms of code reuse -- the implementations of f() and g() are almost identical, but f() is obviously more flexible than g(). Thus, I'd suggest that you go with f() rather than g() unless you have a particularly good reason not to do so.

Caveat: a generic reference-to-array method

There is one construct where passing a pointer/reference to an array can be useful. C++ features an extremely powerful mechanism called templates which allow programmers to write functions that may work on a variety of data types. When this is combined with the reference-to-array method, it can be used to operate on arrays of all data types and of all sizes, provided that the sizes are known at compile time:

template <typename T, int Elements>
void h (T (&a) [Elements]) {
  for (int i = 0; i < Elements; ++i) a[i] = T(); // reset all elements
}

This function can now be called as follows:

h(arrN); // all elements of arrN
h(arrM); // all elements of arrM
h(ptrN); // wrong: h() only works on arrays, not pointers

Once again, though, this only works when you have an actual array, not just a pointer into an array. With only a pointer, you have little choice but to use the pointer-length method to pass the array.

Footnotes

  1. If you must, you can emulate passing an array by value by encapsulating it in a struct or class:
struct Foo { int array[10]; };
void takesFoo (Foo f);
int main () { Foo foo; takesFoo(foo); }
Personal tools