Introduction: Learn to Grasp C++ Pointers

About: I have been an ethical gray-hat hacker and programmer for 10 years now.

A lot of people who work with C++ (namely beginning programmers) can't seem to grasp the concept of pointers. In this instructable, you will receive an in-depth look at what pointers are, how are they used, and where you can use them to make your life as a programmer easier.

Step 1: What Are Pointers?

Pointers are a special type of variable that contain the address of another variable. The address of a variable is where it is stored in the computer relative to physical or virtual memory.

Here is a first example of how to declare a pointer:

int* mypointer;

Let's break down the above example into its individual parts.

The "int" at the beginning of the declaration specifies what it returns when it is done executing. This can be whatever value type you want. In this example, the pointer returns an integer when it is done executing.

(NOTE: int = integer value, char = character value, bool = boolean value, void = no return value.)

The asterisk mark at the end of the return value specifies that we are declaring a pointer. And finally, the name of the pointer comes afterwards.

Before you can get a handle on pointers, you need to understand how computers address memory.

Step 2: How Computers Address Memory

The details of computer addressing on the Intel processor in your PC or Macintosh are quite complicated and much more involved than you need to worry about in this instructable. I will use a very simple memory model in this section.

Every piece of random access memory (RAM) has its own, unique address. For most computers, including Macintoshes and PCs, the smallest addressable piece of memory is a byte.

A byte is 8 bits and corresponds to a variable of type char.

An address in memory is exactly like an address of a house, or would be if the following conditions were true:

●Every house is numbered in order.
●There are no skipped or duplicated numbers.
●The city consists of one long street.

So, for example, the address of a particular byte of memory might be 0x1000. The next byte after that would be 0x1001. The byte before would be 0x0FFF.

I don't know why, but, by convention, memory addresses are always expressed in hexadecimal. Maybe its so that non-programmers will think that computer addressing is really complicated :-P.

Step 3: Declaring a Pointer

The example in the first section was a very basic look at how to declare a pointer.

A pointer variable that has not been otherwise intialized contains an unknown value. You can initialize a pointer variable with an address of a variable of the same type by using the ampersand (&) operator.

char cSomeChar = 'a';
char* pChar;
pChar = &someChar;

In this snippet, the variable cSomeChar has some address. For argument's sake, let's say that C++ assigned it the address 0x1000. (C++ also initialized that location with the character 'a'.) The variable pChar also has a location of its own, perhaps 0x1004. The value of expression &someChar is 0x1000, its type is char* (read "pointer to character"). So the assignment on the third line of the snippet example stores the value 0x1000 in the variable pChar.

Take a minute to really understand the three lines of C++ code. The first declaration says, "go out and find a 1-byte location, assign it the name cSomeChar, and initialize it to 'a'." In this example, C++ picked the location 0x1000.

The third line says, "assign the address of cSomeChar (0x1000) to the variable pChar."

"So what?" you say. Here comes the really cool part demonstrated in the following expression:

*pChar = 'b';

This line says, "store a 'b' at the char location pointed at by pChar." To execute the expression, C++ first retrieves the value stored in pChar (that would be 0x1000). It then stores the character 'b' at that location.

The * when used as a binary operator means "multiply"; when used as a unary operator, * means "find the thing pointed at by...". Similarly, & has a meaning as a binary operator (which I don't discuss), but as a unary operator, it means "take the address of...".

Step 4: Using Pointers

PASSING ARGUMENTS TO A FUNCTION

There are two ways to pass arguments to a function: either by value or by reference.

PASSING ARGUMENTS BY VALUE

Normally, arguments are passed to functions by value, meaning that it is the value of the variable that gets passed to the function and not the variable itself.

The implications of this become clear in the following snippet of code:

void fn (int nArg1, int nArg2)
{
//modify the values of the arguments
nArg1 = 10;
nArg2 = 20;
}

int main(int nNumberofArgs, char* pszArgs [])
{
//initiaize two variables and display their values
int nValue1 = 1;
int nValue2 = 2;

//now try and modify them by calling a function

fn (nValue1, nValue2);

//what is the value of nValue1 and nValue2 now?
cout << "nValue1 = " << nValue1 << endl;
cout << "nValue2 = " << nValue2 << endl;

system ("PAUSE")
return 0;

}

This program declares two variables, nValue1 and nValue2, initializes them to some known value, and then passes their values to a function fn (). This function changes the value of its arguments and simply returns.

Question: What is the value of nValue1 and nValue2 in main () after control returns from fn ()?

Answer: The value of nValue1 and nValue2 remain unchanged at 1 and 2, respectively.

To understand why, examine carefully how C++ handles memory in the call to fn (). C++ stores local variables (like nValue1 and nValue2) in a special area of memory known as the stack. Upon entry into a function, C++ figures out how much stack memory the function will require and then reserves that amount. Say, for argument's sake, that in this example, the stack memory carved out for main () starts at location 0x1000 and extends to 0x101F. In this case, nValue1 might be at location 0x1000 and nValue2 might be at location 0x1004.

(NOTE: IF YOU ARE USING CODE::BLOCKS, AN INT TAKES UP 4 BYTES.)

As part of making the call to fn (), C++ first stores the values of each argument on the stack starting at the rightmost argument and working its way to the left.

The last thing C++ stores as part of making the call is the return address so that the function knows where to return after it is complete.

For reasons that have to do more with the internal workings of the CPU, the stack "grows downward", meaning the memory used by fn () will have addresses smaller than 0x1000.

Remember that this is just a possible layout of memory. I don't know (or care) that any of these are in fact the actual addresses used by C++ in this or any other function call.

The function fn (int, int) contains two statements:

nArg1 = 10;
nArg2 = 20;

The main point of saying this is to demonstrate the fact that changing the values of nArg1 and nArg2 has no effect on the original variables back at nValue1 and nValue2.

PASSING ARGUMENTS BY REFERENCE

So what if I wanted the changes made by fn () to be permanent? I could do this by passing not the value of the variables but their address. This is demonstrated by the following snippet of code:

void fn (int* pnArg1, int* pnArg2)
{
//modify the value of the arguments
*pnArg1 = 10;
*pnArg2 = 20;
}

int main (int nNumberofArgs, char* pszArgs [])
{
//initialize two variables and display their values
int nValue1 = 1;
int nValue2 = 2;

fn (&nValue1, &nValue2);

system ("PAUSE")
return 0;
}

Notice first that the arguments to fn () are now declared not to be integers but pointers to integers. The call to fn (int*, int*) passes not the value of the variables nValue1 and nValue2 but theie addresses.

By using the pointers, the changes made to the two variables become permanent. Without using the pointers, the changes are only made to the values copied to fn ().

Step 5: Don't Know What Size You Are?

HEAPS OF MEMORY

Along with the stack, C++ creates another large pool of memory called the heap. The heap can be used by the programmer to free, or allocate, memory to be used for the program. A programmer can allocate any amount of memory off the heap using the keyword "new" as in the following example:

char* pArray = new char[256];

You can use the new keyword when you don't know how much memory your program will use.

A common problem that many programmers come across when allocating memory is something called a "buffer overflow". A buffer overflow occurs when an expression or function overwrites memory in the adjacent memory locations. This can happen because of fixed-length arrays. A common use of arrays is to hold strings. You can tack two strings together to create one concatenated string. If the amount of characters in the string is bigger than the size of the array, than you just caused a buffer overflow.

(WARNING: BUFFER OVERFLOWS ARE A COMMON EXPLOIT USED BY HACKERS TO EXECUTE ARBITRARY CODE ON A REMOTE SYSTEM.)

DON'T FORGET TO CLEAN UP AFTER YOURSELF

Allocating memory off of the heap is a neat feature, but it has one very big danger in C++: if you allocate memory off of the heap, you must remember to return it.

You return memory to the heap using the "delete" keyword as in the following:

char* pArray = new char[256];
// ... use the memory all you want...

//now return the memory block to the heap
delete[] pArray;
pArray = NULL;

(NOTE: Use delete to return a non-array and delete[] for an array.)

If you don't return heap memory when you are done with it, your program will slowly consume memory and eventually slow down more and more as the operating system tries to fulfill its apparently insatiable gluttony. Eventually, the program will come to a halt as the O/S can no longer satisfy its requests for memory.

Returning the same memory to the heap is not quite as bad. That causes your program to crash almost immediately. It is considered good programming practice to zero out a pointer once you have deleted the memory block that it points to for two very good reasons:

●Deleting a pointer that contains NULL has no effect.
●NULL is never a valid address. Trying to access memory at the NULL location will always cause your program to crash immediately, which will tip you off that there is a problem and make it a lot easier to find.

You don't have to delete memory if your program will exit soon - all heap memory is restored to the operating system when a program terminates. However, returning memory that you allocate off the heap is a good habit to get into.

Step 6: Buy a Book

Hope you enjoyed this instructable and learned a lot more about pointers. Besides just reading this instructable, you should buy a book on C++, as you can learn a lot more from that.