Monday, January 17, 2011

Introduction to pointers

Pointers are one of the most powerful and confusing aspects of the C language. A pointer is a variable that holds the address of another variable. To declare a pointer, we use an asterisk between the data type and the variable name:
1int *pnPtr; // a pointer to an integer value
2double *pdPtr; // a pointer to a double value
3 
4int* pnPtr2; // also valid syntax
5int * pnPtr3; // also valid syntax
Note that an asterisk placed between the data type and the variable name means the variable is being declared as a pointer. In this context, the asterisk is not a multiplication. It does not matter if the asterisk is placed next to the data type, the variable name, or in the middle — different programmers prefer different styles, and one is not inherently better than the other.
Since pointers only hold addresses, when we assign a value to a pointer, the value has to be an address. To get the address of a variable, we can use the address-of operator (&):
1int nValue = 5;
2int *pnPtr = &nValue; // assign address of nValue to pnPtr
Conceptually, you can think of the above snippet like this:

It is also easy to see using code:
1int nValue = 5;
2int *pnPtr = &nValue; // assign address of nValue to pnPtr
3 
4cout << &nValue << endl; // print the address of variable nValue
5cout << pnPtr << endl; // print the address that pnPtr is holding
On the author’s machine, this printed:
0012FF7C
0012FF7C
The type of the pointer has to match the type of the variable being pointed to:
1int nValue = 5;
2double dValue = 7.0;
3 
4int *pnPtr = &nValue; // ok
5double *pdPtr = &dValue; // ok
6pnPtr = &dValue; // wrong -- int pointer can not point to double value
7pdPtr = &nValue; // wrong -- double pointer can not point to int value
Dereferencing pointers
The other operator that is commonly used with pointers is the dereference operator (*). A dereferenced pointer evaluates to the contents of the address it is pointing to.
1int nValue = 5;
2cout << &nValue; // prints address of nValue
3cout << nValue; // prints contents of nValue
4 
5int *pnPtr = &nValue; // pnPtr points to nValue
6cout << pnPtr; // prints address held in pnPtr, which is &nValue
7cout << *pnPtr; // prints contents pointed to by pnPtr, which is contents of nValue
The above program prints:
0012FF7C
5
0012FF7C
5
In other words, when pnPtr is assigned to &nValue:
pnPtr is the same as &nValue
*pnPtr is the same as nValue
Because *pnPtr is the same as nValue, you can assign values to it just as if it were nValue! The following program prints 7:
1int nValue = 5;
2int *pnPtr = &nValue; // pnPtr points to nValue
3 
4*pnPtr = 7; // *pnPtr is the same as nValue, which is assigned 7
5cout << nValue; // prints 7
Pointers can also be assigned and reassigned:
01int nValue1 = 5;
02int nValue2 = 7;
03 
04int *pnPtr;
05 
06pnPtr = &nValue1; // pnPtr points to nValue1
07cout << *pnPtr; // prints 5
08 
09pnPtr = &nValue2; // pnPtr now points to nValue2
10cout << *pnPtr; // prints 7
The null pointer
Sometimes it is useful to make our pointers point to nothing. This is called a null pointer. We assign a pointer a null value by setting it to address 0:
1int *pnPtr;
2pnPtr = 0; // assign address 0 to pnPtr
or shorthand:
1int *pnPtr = 0;  // assign address 0 to pnPtr
Note that in the last example, the * is not a dereference operator. It is a pointer declaration. Thus we are assigning address 0 to pnPtr, not the value 0 to the variable that pnPtr points to.
C (but not C++) also defines a special preprocessor define called NULL that evaluates to 0. Even though this is not technically part of C++, it’s usage is common enough that it will work in every C++ compiler:
1int *pnPtr = NULL; // assign address 0 to pnPtr
Because null pointers point to 0, they can be used inside conditionals:
1if (pnPtr)
2    cout << "pnPtr is pointing to an integer.";
3else
4    cout << "pnPtr is a null pointer.";
Null pointers are mostly used with dynamic memory allocation, which we will talk about in a few lessons.
The size of pointers
The size of a pointer is dependent upon the architecture of the computer — a 32-bit computer uses 32-bit memory addresses — consequently, a pointer on a 32-bit machine is 32 bits (4 bytes). On a 64-bit machine, a pointer would be 64 bits (8 bytes). Note that this is true regardless of what is being pointed to:
01char *pchValue; // chars are 1 byte
02int *pnValue; // ints are usually 4 bytes
03struct Something
04{
05    int nX, nY, nZ;
06};
07Something *psValue; // Something is probably 12 bytes
08 
09cout << sizeof(pchValue) << endl; // prints 4
10cout << sizeof(pnValue) << endl; // prints 4
11cout << sizeof(psValue) << endl; // prints 4
As you can see, the size of the pointer is always the same. This is because a pointer is just a memory address, and the number of bits needed to access a memory address on a given machine is always constant.
Quiz
1) What values does this program print? Assume a short is 2 bytes, and a 32-bit machine
01short nValue = 7; // &nValue = 0012FF60
02short nOtherValue = 3; // &nOtherValue = 0012FF54
03short *pnPtr = &nValue;
04 
05cout << &nValue << endl;
06cout << nValue << endl;
07cout << pnPtr << endl;
08cout << *pnPtr << endl;
09cout << endl;
10 
11*pnPtr = 9;
12 
13cout << &nValue << endl;
14cout << nValue << endl;
15cout << pnPtr << endl;
16cout << *pnPtr << endl;
17cout << endl;
18 
19pnPtr = &nOtherValue;
20 
21cout << &nOtherValue << endl;
22cout << nOtherValue << endl;
23cout << pnPtr << endl;
24cout << *pnPtr << endl;
25cout << endl;
26 
27cout << sizeof(pnPtr) << endl;
28cout << sizeof(*pnPtr) << endl;

No comments: