Basics
Pointers are a means of referring to stored values by their location in memory (memory address) rather than by a specific variable name for the storage location.
We can look up the memory address of a variable using the & symbol, e.g.
int x = 3; cout << "The memory address of x is " << (&x); cout << " and the value stored there is " << x << endl;
Pointer variables are variables that can store a memory address,
and are declared by specifying what kind of data we expect to
find stored at that memory address. For instance, in the definition
below iptr can hold the memory address of a int.
int* iptr;
We can make it point at (hold the memory address of) our earlier
variable, x as follows:
iptr = &x;
Once a pointer variable holds a memory address, we can use the * symbol to look up what is stored there, e.g. the code below prints out the address of x (as stored in iptr) and the value of x (found through the pointer into memory)
cout << "The address of x is " << (iptr); cout << " and the current value of x is " << (*iptr) << endl;
NULL pointers
Pointers can refer to any valid memory address, 1..SizeOfMemory, but it is also handy to have a value we can use to indicate that a pointer currently is not set to a valid address.
The default value used in C/C++ is NULL (aka 0).
We will often initialize pointers to NULL, or set them to NULL when we no longer want them to refer to any specific location, which allows us to add error checking capabilities to our code, e.g.
int* p = NULL; ... ... ... if (p == NULL) { cout << "p is not pointing anywhere" << endl; } else { cout << "The int p is pointing at is " << (*p) << endl; }
Important note: if we try to use the * operator to access the contents of memory through a pointer, and that pointer is currently NULL, the program will crash, giving the Segmentation violation error message.
Dynamic allocation and deallocation (new, delete)
We often use pointers to allow us to create and access memory space for new storage locations while the program is executing.
This is referred to as dynamic allocation.
This is often done when we are not sure if/when/how many of a data type we will need until the program is already running, but don't want to waste storage space by allocating items unnecessarily.
To request a new item we use the new operator, telling it what kind of data item we want. The operator finds and allocates space (someplace in memory) and returns the address of the space it found. We store this in an appropriate pointer so that we can use the resulting item. If new fails to find enough space for the item then it simply returns NULL instead.
// allocate a float and make fp point to it float* fp; fp = new float; if (fp == NULL) { cout << "Allocation failed" << endl; } else { (*fp) = 1.2; // use the new space. } // allocate an array of 20 floats and make arr point to it float *arr; arr = new float[20]; if (arr == NULL) { cout << "Allocation failed" << endl; } else { ... use the array normally ... }The newly allocated storage is available for use until the program ends or until you explicitly free the storage using the delete operator:
We should always deallocate such storage once we are completely finished with it, but not until we are completely finished with it.
Pointers as parameters
Passing pointers as parameters to functions allows the function to access (and even alter) the contents of the memory pointed at - i.e. allows us to emulate pass-by-reference. (In fact, this is how pass-by-reference is actually done 'under the hood'.)
// sets the contents of memory to 0 for whichever // int variable iptr currently points at void setToZero(int* iptr) { if (iptr != NULL) { (*iptr) = 0; } } int main() { int x = 10; setToZero(&x); // passes address of x to the function, // which sets contents of x to zero int y = 10; int* p = y; // p points to y setToZero(p); // sets contents of y to zero }
As with any parameter, we can pass pointers using pass-by-reference - thus allowing the function to change where the pointer points. E.g.
// create an array of floats of the specified size, // and make the array pointer point to it // return true if successful, false otherwise bool allocatearray(float* &arr, int size) { arr = new float[size]; if (arr == NULL) { return false; } return true; }
Pointers to structs
We can create pointers to structs, but must be careful in the syntax for accessing the struct fields afterwards, e.g.
// defining a struct type struct MyStructType { int someDataField; float anotherDataField; }; int main() { // creating an instance of a struct MyStructType s; // creating a pointer to variable s MyStructType* ptr; ptr = &s; // accessing the fields through the pointer (*ptr).someDataField = 1; (*ptr).anotherDataField = 1.0;Note that the (*ptr) lets us access struct s, and the .someDataField allows us to get at a specific field in the struct.
Because this notation can get somewhat cumbersome (especially when we get into more complex data definitions that include pointers to structs inside other structs) there is an alternative notation using pointerVariable->fieldName e.g.
ptr->someDataField = 1; ptr->anotherDataField = 1.0;The two notations are equivalent, and ONLY applicable when we are working with pointers to structs.
Pointers and arrays
In C++, an array variable is treated as a pointer
with a fixed (constant) value. For example, arr
below is really a constant-valued pointer to the first
storage location 0 in the array.
float arr[10];
As a result, we can use array names and pointers almost interchangably (the key practical difference being that we can change the value of a pointer variable)
float arr[10]; float *ptr; ptr = arr; // make ptr point to the same place as arr does, // i.e. at array element 0 ptr[5] = 1.2; // effectively the same as saying arr[5] = 1.2; ptr = &(arr[0]); // make ptr point at the location of array element 0, // which is exactly the same as saying ptr = arr; !!!This has some interesting implications in terms of parameter passing:
void f1(float* ptr, int size); void f2(float arr[], int size); int main() { float array[25]; // array of 25 floats float* p = array; // pointer for the same array // all the following are totally acceptable f1(p, 25); f2(p, 25); f1(array, 25); f2(array, 25); }Note that, since we can be passed a null pointer, this is something we should test inside f1 and f2 before trying to use the arrays we were expecting.