Monday, October 5, 2009

Pointers explained

Many students have been having trouble with pointers, so I am going to attempt to explain them in a very simplistic version. I will assume you, the student, have had minimal exposure to pointers. The examples presented here are applicable to C++ but to use this in C call the malloc() and free() functions instead of new and delete. The dereferencing will be the same for both.

A small review: Pointers live on what is called the heap or free space. This is memory separate from the program stack where general variables are called for example see the following code.

1 int i = 7; //i lives on the program stack
2 int* p = new int; //create a pointer to an int and allocate enough space on the
//heap to hold an int

3 int *p = new int; //same as above
4 int * p = new int; //same as above

We can see that the location of the '*' does not matter when declaring a pointer. This will hold true for accessing pointers as well.

So what is a pointer?
A pointer, unlike a variable, is actually an address. While not all that technically accurate, for now it safe for us to think of pointers as an int that holds the memory address of some object.

Above, when we declared the pointer p, all we did was allocate, or set aside, enough memory for an int, and get the address of that memory and put it to p. Assuming a 32 bit machine that uses 4 bytes for an int,(most Unix, Linux, and Windows systems use this) p holds the address of the first byte, and since the compiler knows that p is an int pointer, it will know to grab the first byte and the next three, four total, to get the full value of the int. If it was a double pointer the complier would grab the allotted number of bytes for a double(generally 8).

How do I set a value to a pointer?
When declaring a pointer you can also define the pointer.(if you are unclear as to the difference between declarations and definitions click here) See the following code example:

1 int* p = new int(3); //create a pointer to an int and allocate enough space on
//
the heap to hold an int and places the int 3 inside this space
2 int *p = new int(3); //same as above
3 int * p = new int(3); //same as above

Here when we created a the pointer p we set the value held in the heap to be 3. THIS DOES NOT MEAN THAT p==3. p is still a pointer meaning that p is the address to an int in the heap which has the value of 3. If we want the value of p we must dereference the pointer.

See the following code example:

1 int* p = new int(3); //create a pointer to an int and allocate enough space on
2 //the heap to hold an int and places the int 3 inside this space
3
4 cout << "The address of p = " << p << endl; //prints: The address of p = xxxxxxxxx
5 //xxxxxxxxx is the address held by p, this will most likely be a huge number over 10^8.
6
7 cout << "The value of p = " << *p << endl; //prints: The value of p = 3
8 //*p derefences p meaning that it will print the value held at the address p which we set to be equal to 3 when we created p.

We see that on line 7 to print the 3 we had to "dereference" the pointer p. This is done with the * operator. When a dereference is done the following pseudo code will be executed:

1. Go to address held by p
2. Get the int held at p by accessing the four byte(assuming machine uses 4 byte ints)
3. Return this int

How do pointer arrays work?
The memory in computer is in its rawest form one massive array and therefore it easily transfers to pointer arrays. Lets look at some example code:

1 char *array = new char[3] //allocate a sequential sector big enough for three chars and put the address of the first char to the pointer array.
2
3 array[0] = 'a'; //assign the first char in the array to be 'a'
4 array[1] = 'b'; //assign the second char in the array to be 'b'
5 array[2] = 'c'; //assign the third char in the array to be 'c'
6
7 cout << "Third element in array = " << array[2];
//
Prints: "Second element in array = c"
8
9 cout << "Third element in array = "<< *(array + 2*sizeof(char)); //Prints same as above. See explanation for why this is true.

It is important to note that the [] operator will actually dereference array. This is what allows us to write array[2] = 'c' such as in line 5. Before we go into more detail about this lets take a look at line 9 and most specically the part of the statement *(array + 2*sizeof(char)). Lets break this down:

-sizeof(char) returns the size of a char(typically 1 byte)
-2*sizeof(char) will be the amount of memory held by two chars.
-remember that array is a pointer to the first element of the array.
-(array + 2*sizeof(char)) will return the address two characters away from the begining of the array. This is simply array[2] because is the third position of the array not just two characters away from the first? (if this does not make sense try drawing out the array)
-*(array + 2*sizeof(char)) will return the value held at array[2]. Remember that (array + 2*sizeof(char)) is the address of the third element so dereferencing that will simply give us the value held at the third index of the array.

When we use the statement array[2] the compiler will actually follow the procedure outlined above. The compiler of course will be optimized and to increase readability in your code it is highly advised to use the [] notation instead of incrementing your pointer each time. This will also help reduce errors in the code.

What is the & operator and how does it relate to pointers?
The & operator will actually return the address of the object it is applied to. We say we are "dereferencing" a pointer but the & "references" an object so it does the revers of a dereference. see the following code:

1 int i = 7;
2 int *ptr = new int(4);
3
4 cout << "value of i = " << i << " address of i = " << &i;
5 //prints: "value of i = 7 address of i = xxxxxxxx where xxxxxxxx is the address of i in the stack(most likely above 10^8)
6
7 cout << "value of ptr = " << *ptr << " address of ptr = " << ptr << " which is the same as " << &(*ptr);
8 //prints: value of ptr = 4 address of ptr = xxxxxxxx which is the same as xxxxxxxx
9 //xxxxxxxx is the address held by ptr(most likely above 10^8)

While the first cout statement is pretty strateforward lets look at the second one. Printing the address of ptr the first way has been explained but why is the same as the second verison? Lets break it down.

-ptr is a pointer that points to an address holding the value 4
-(*ptr) will deference the pointer and return the value 4
-&(*ptr) will find the address of the value 4 returned by (*ptr) which is of course the address held by ptr.

What does the -> operator do on function calls?
The -> operator will dereference the pointer then call the function on the object held at that location. See the following code:

1 string *s = new string("pointers rock");
2
3 cout << "length of s = " << (*s).size();
4 //prints: "length of s = 13
5
6 cout << "length of s = " << s->size();
7 //prints: "length of s = 13

We see that both statements do exactly the same thing. The -> operator is a shortcut that simply dereferences the pointer then calls the object on the returned pointer.

What is a NULL pointer?
A null pointer is a pointer that points to nothing. You cannot dereference a NULL pointer because, again, the pointer doesn't point to anything. Null pointers are sometimes unavoidable such as in Linked Lists but if the problem can be solved without their use, this method is desired. Lets look at the following code

1 int *i = NULL;//creates an int pointer, i, that doesn't point to anything
2 int *i = 0;//different same syntax but means the same as above.
3
4 char *c = NULL; //creates a char pointer, c, that doesn't point to anything
5 char *c = 0;//once again different syntax but it means the same thing
6
7 cout << *c; //ERROR: no value pointed to by c so no value to print

We can see that the word NULL or the number 0 can be used to assign a pointer to null. On line 7 when we try to output the value at c we get an error. This will most likely come in the form of a segmentation fault at run time. The error comes from the fact that there is nothing that c points to so there is nothing to dereference and print. In general you should almost never dereference a null pointer.

What is a void pointer?
A void pointer is a pointer that points to a location in memory but the compiler doesn't know its type. Recall that when the compiler prints an int it knows to get n=sizeof(int) bytes to construct the int. Therefore you cannot print a void pointer because there is no way of dereferencing it to an object.

Pointer arithmetic is still very valid for void pointers. You can increment and decrement that pointer just as one would move around an int pointer or a char pointer. Lets look at a code example:

1 int *i = new int[3]; //create an array of ints and populate the array
2 i[0] = 10;
3 i[1] = 20;
4 i[2] = 30;
5
6 void* v; //v is a pointer that can point to any object in the heap.
7
8 v = i; //set the address held by v to the address of the first element in i
9 v + 2*sizeof(int); //v now equals the address of the third element in i
10
11 cout << "The third element of i = " << (int)v; //cast v to an int before a print
12 //Prints: "The third element of i = 30

As explained earlier, i is simply an pointer to an array of ints in the heap. On line 6 we create a void pointer v. On line 6 the address of v is assigned to be the address of i. Remember that a void pointer can point to any type so i could have been a char, double, long, or any other type, even user defined types. Now that v points to the first element of i, when v is moved two ints to the right on line 9 it now will point to the same location as v[2]. Finally on line 11 we want to print the value held at the pointer v. To do this we must cast v to be an int pointer. This tells the compiler to grab n=sizeof(int) bytes and construct an int from these bytes. Note that this cast only lasts for this statment. If we were to use v again in another statement it would still be a void pointer.

So now you should have a generally solid feel on pointers. For further information please see this site. It has good information on pointers.

No comments:

Post a Comment