Previous Section Main Menu Next Section

CHAPTER 9

DYNAMIC MEMORY AND POINTERS


Section III: Arrays and Pointers Section I: Memory Allocation and the Pointer Section II: Using Pointers Section IV: Strings

Section III: Arrays and Pointers

A. The Similarity of the Two

In chapter 7 you learned that arrays are automatically passed by reference. Now that we are working more closely with memory addresses, the meaning of pass by reference can become clearer. When a variable is passed by reference to a function, the address of the memory is passed to the function. In other words, the variable is treated a bit like a pointer.

In most cases, this is invisible, and the underlying mechanism is of little use to us, as long as we remember that variables passed by reference can have their contents changed upon returning from the function, while variables passed by value cannot have their values so changed. However, with arrays this is not true. Not only are array parameters automatically treated as reference parameters but arrays themselves can be treated exactly like pointers. The line of code:

int intArray[10];

is the same as:

int* intArray;
intArray = new int[10];

Both request memory for 10 integers.

Consider this more drastic example:


	typedef  int IntArray[20];

`	void main ()
	{  	int* ptrArray;
		ptrArray = new IntArray;
		for (int i = 0; i < 20; i++)
		{	ptrArray[i] = i * 10;
		}
	}
We start with a user defined type for an array of 20 integers. We then declare a pointer to an integer. Third, we ask for memory to be set aside for 20 integers ('new IntArray') and that the address for the beginning of this memory be stored in the pointer to an integer, 'ptrArray'. And, finally, in the 'for' loop we use the pointer as if it referred to an array!

This business of converting back and forth can be very confusing, so let's take our time with it. The key point is that C++ works with arrays by working with the address of the first byte of the array. A variable that represents an array, therefore, can be considered a pointer to the first byte of the array. Likewise, the name for an array element is the same as a pointer to the memory containing that specific array element. For example, in the code:

'AnArray[0]' is equivalent to a pointer to the first element of the array and 'AnArray[1]' is equivalent to a pointer to the second element of the array. The reverse is also true. In the code inside 'main' above, 'ptrArray' is declared to be a pointer to an integer. But, once the 'new' statement is executed, it is also equivalent to a pointer to the first byte of an array of integers. As such, it is equivalent in meaning to 'AnArray'. And, once 'ptrArray' is seen to be equivalent in meaning to 'AnArray', it makes sense that 'ptrArray[0]' be equivalent to 'AnArray[0] and 'ptrArray[1]' be equivalent to 'AnArray[1], etc. Thus, the 'for' loop code makes sense. (Well, maybe it does not make sense yet, but keep with it and it will! You are urged to read this paragraph again if you didn't follow its logic the first time.)

When we say 'equivalent' here, we mean that the two statements have the same semantics, not that they refer to the same memory. In fact, as written, 'AnArray' refers to a different set of memory locations than 'ptrArray' because one requests memory at compile time and the other requests a different block of memory at run-time. But, consider the following:

As the picture below shows, 'AnArray1' and 'AnArray2' both refer to the same memory. 'AnArray1' is the symbolic (variable) name for the memory of the array itself, while 'AnArray2' is the symbolic (variable) name for a memory location that points to the memory of the array.


Figure 9.9

To demonstrate this, experiment with the code in the file, ch9prog1.cpp part of which is show below:


	typedef  int IntArray[20];

	void main () 
	{
		IntArray array1;
		int* array2;

		array1[0] = 111;
		array2 = array1;
		array2[0] = 222;
		cout << endl << array1[0] ;
		*array2 = 333;
		cout << endl << array1[0];
		cout << endl << "An Address: " << array2;
		cout << endl << "The contents of that address: "  << *array2;
	}

When you run this, you will find that when we output 'array1[0]' the first time, we get the value placed in 'array2[0]' and when we output it the second time we get the value put in '*array2' ! If 'array1[0]', 'array2[0]' and '*array2' did not all refer to the same memory, how could placing a value in '*array2', for example, affect 'array1[0]'?

The last two outputs are there to demonstrate something a bit different. The line with the output string that begins with, "An Address ...", is actually showing the address of the array. That makes sense because 'AnArray2' is the symbolic name, not for a memory location containing an integer, but for a memory location containing an address of a memory location that contains an integer. The next line uses the dereferencing operator (*) to actually access the contents of the memory pointed to by 'AnArray2'. Note that this is the same value (333) that was just placed in this location and output a few lines up.  

B. Pointer Arithmetic
There is one more little trick to all this. Consider this addition to the for loop code above (also found in the file "ch9prog2.cpp")


	typedef  int IntArray[20];

`	void main ()
	{  	int* ptrArray;
		ptrArray = new IntArray;
		for (int i = 0; i < 20; i++)
		{	ptrArray[i] = i * 10;
		}
	// Here is the new part
		int* ptr;
		ptr = ptrArray;
		for (i = 0; i < 20; i++)
		{	cout << *ptr << endl;
			ptr++;
		}
	}
First, the code initializes the array as we have already discussed. When this initialization is completed (but before the 'new' part), the array looks as follows:

Figure 9.10

Then, in the "new part", the code creates a second pointer to an integer and has that integer also point to the array. Figure 9.11 shows this. (The values under each box in the array number the array elements.)

Figure 9.11

This is followed by a 'for' loop that obviously is to output something. The first time into the loop 'ptr' points to the first element of the array, '*ptr' means retrieve the contents of that memory location, and so '0' is output. The following line, "ptr++", increments something by 1. The key is what gets incremented. Since 'ptr' represents a memory location containing an address and the '++' operator increments the contents of a memory location, it must be the address in 'ptr' that is being incremented.

Suppose 'ptr' contained the address 30,456. If this address was really incremented by 1, 'ptr' would be pointing to address 30,457. But, if memory location 30,456 holds an integer, as the declarations say it must, and, if an integer takes two bytes, as we have suggested it might, then address 30,457 is in the middle of an integer. Then, the next time through the loop, what would it mean when we tried to output the contents of the middle of an integer?

Actually, the computer would come to a halt for reasons based on hardware design. The situation would be even worse if 'ptr' pointed to a double. It would seem then that either C++ should not allow code like 'ptr++' or it should mean something else. In fact, it is valid code and therefore has a slightly different meaning. The '++' here means, "Increment the pointer the equivalent of one integer", which, if an integer takes up two bytes, would mean that the address becomes 30,456. If 'ptr' was declared to be a pointer to a double and doubles required 4 bytes, the address would increment by four bytes. If 'ptr' was declared to be a pointer to a contract and contracts took 10 bytes, then the address would be incremented by 10.

Thus, the second time through the loop, 'ptr' points to the second element in the array (Figure 9.12) and '10' is output. Then, at the bottom of the loop and each time through the loop after that, 'ptr' is incremented to point to the next integer in the array. The result is that all 20 elements of the array are output. This is the equivalent of the code we have seen in earlier chapters to output an array by incrementing the array index.


Figure 9.12

You might ask why the code creates a second variable 'ptr' to 'walk through' the array elements. Note that the only way to access the array is via the two pointers. Now imagine that 'ptr' is gone and we increment 'ptrArray' to get to the 2nd and following elements of the array. After one increment (and without 'ptr'), here is how memory would look:


Take a moment to see if you recognize the problem.

***************************************

 

Did you notice that once 'ptrArray' has been incremented, there is no longer any way to access the first element of the array! Actually, we could execute the line:

ptrArray--;

but we would have to be very careful to decrement as many times as we incremented or we would either go back too many memory locations or not enough. The safest way to handle this is to always leave a pointer pointing to the beginning of the array and use a second pointer to walk through the array.

C. Memory Leaks
Pointer based code can be very dangerous. It is possible to completely lose access to some data needed in a program or, as we will see later, to leave a pointer pointing to memory that it should not have access to. We just saw an example of the first problem and here is a second example.

Consider the following code and pictures

A picture of memory at this point::


Figure 9.14

Now consider the line of code

d2 = d1;

and the picture that goes with it:


Figure 9.15

As you can see, the memory location with the value 24 is no longer accessible. In fact, given that we have no idea what address it had to begin with and that it no longer has any connection with 'd1', there is NO way that a program with this code can again access this value! The moral is:

BE CAREFUL!

D. Another Look at Arrays as Parameters
Back in chapter 7, you learned how to declare a function parameter that could be used to receive or return an array. At that time, we used C++'s 'typedef' and the ability to create user-defined types . Since C++ essentially treats array variable names as pointers to memory, there is a second, commonly used way to declare functions to handle array parameters.

One example we studied in chapter 7 involved an array of 10,000 integers representing an inventory. The code included the function 'OutputInventory' with the declaration:

void OutputInventory(InventoryArray items);

Now that we know that:

  1. there is a close connection between arrays and pointers;
  2. when a parameter is passed by reference, what is passed is the address of the actual parameter;
  3. arrays are automatically passed by reference;

it is easy to see why the following function declaration is valid:

void OutputInventory(int* items);

In place of the user-defined type name, ' InventoryArray', the code simply says that the function will receive a pointer to an integer (the address of an integer).This is what we have been doing all along since, when an array is passed as a parameter, what is actually passed is the address of the beginning of the array. As a second example, if a function is to receive an array of contracts, the function declaration might look as follows:

void SomeFunctionName(Contract* contracts);

Again, a call to this function would be expected to pass the address of a memory location holding a contract. In our case, since we are planning on passing an array of contracts, the address of the first contract in the array would be passed. Note, however, that one cannot tell from the declaration whether what is being passed is a pointer to one contract or to the first of many contracts.

Note also that, if the address of the first element of an array is being passed, one still cannot tell how big the array is. Thus, this form of parameter declaration is considered more powerful, one might say "more forgiving" or "more dangerous". It is often the case that a declaration like this is written with a second parameter to indicate the size of the array being passed.

As we saw in Part A above, a pointer to what really is an array of elements can be manipulated using either pointer or array notion. Therefore, in the case of ' OutputInventory', we could write the function definition either as:


	void OutputInventory(InventoryArray items)
	{
		for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
		{	
			cout << "The inventory amount for item " << itemCode << " is: " 
		        	        <<  items[itemCode] << endl;
		}
	}
which is exactly how we wrote it in chapter 7. Or, we could write:

	void OutputInventory(int* items)
	{
		for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
		{	
			cout << "The inventory amount for item " << itemCode << " is: " 
		        	        <<  *items<< endl;
			items++;
		}
	}
Notice how in both these cases we did not include a second parameter to indicate the size of the array. Instead, we replied on the existence of a global constant. Some programmers would criticize this code and insist on the need for a second parameter. Here is how we might write such a function definition:

	void OutputInventory(int* items int size)
	{
		for (int itemCode = 0; itemCode < size;  itemCode++)
		{	
			cout << "The inventory amount for item " << itemCode << " is: " 
		        	        <<  *items<< endl;
			items++;
		}
	}

As a reminder, there also is a third way one could declare an array parameter. Still using 'OutputInventory' as an example, one could write:

void OutputInventory(int items[]);

This says that an array of integers of some size is being passed and, thus, this declaration is equivalent to the first line of the definition above. This approach does emphasize a point that should be stressed again. When an array is passed, the function has no direct way of knowing how big the array is. As written, this declaration will probably rely on a global constant being available. As we noted, the other way to handle this would be to pass a second integer parameter to provide the number of elements in the array.

Many people prefer this second approach because it makes the size of the array clear. Whatever you do, be sure you carefully handle this situation. Suppose the size of the 'items' array was actually 1000 integers, but you thought it was still 10,000 integers, and you did not use the MAX_ITEMS constant. If you wrote the following code:


	for (int itemCode = 0; itemCode < 10000;  itemCode++)
	{	
		items[itemCode] = 0;
		// OR: *items = 0 ; items++;
	}
you would be placing zeroes into 9,000 memory locations that were not part of the array. Who knows what damage you could be creating. In fact, a smart programmer once took advantage of the fact that the Unix operating system was writing values into the elements of arrays without checking for what are referred to as array bounds and he was able to break into what were thought to be secure systems!

Topics Covered in the "Essentials of C++"

Arrays and Pointers
Pointer Arithmetic

Top of Section Main Menu Next Section