| Section III: Arrays and Pointers | Section I: Memory Allocation and the Pointer | Section II: Using Pointers | Section IV: Strings |
|
Table of Contents
Learning C++:
An Index of Entry Points
2. The A reference document on the basic elements of C++.
3. The Patterns
|
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:
is the same as:
Both request memory for 10 integers. Consider this more drastic example: 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:
IntArray AnArray; '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:
IntArray AnArray1; AnArray2 = AnArray1; 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.
To demonstrate this, experiment with the code in the file, ch9prog1.cpp part of which is show below: 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
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.
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:
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 Consider the following code and pictures
d1 = new double; A picture of memory at this point::
Now consider the line of code
and the picture that goes with it:
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:
D. Another Look at Arrays as 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:
Now that we know that:
it is easy to see why the following function declaration is valid:
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:
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:
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: Topics Covered in the "Essentials of C++" |