PointersReferencesAndArrays
From Cppwiki
- 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
- 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); }