Previous | Table of Contents | Next |
This second call, which clears elements 23, 24, ...39 of the table, does something that would have been much more difficult without the correspondence between pointers and arrays.
Adding integers to pointers is straightforward, as is subtracting integers from pointers. If it is possible to add a negative integer to a pointer, it should also be possible to subtract a positive integer, and indeed it is. There are two other properties of pointer arithmetic that are also important, and which are less obvious.
One property is that it is possible to subtract two pointers and obtain an integer, provided that the pointers point to two elements of the same array. At the machine level, this subtraction involves address subtraction followed by division by the size of an element. The other property is more interesting, and is sometimes absolutely crucial: It is always possible to obtain a pointer one element beyond the last element of an array, provided that one does not actually try to use the element stored there. To see why this property is important, consider an alternative version of int_clear:
void int_clear(int* p, int n) { int* limit = p + n; while (p < limit) *p++ = 0; }
In principle, this version of int_clear should be faster than the previous one, because it changes only one variable each time through the loop instead of two. But in order to work, it must be able to form an address that points one element past the end of the array. For example, if we say
int table[100]; int_clear(table, 100);
the first thing int_clear does is to set limit to an address one element beyond the end of table. This technique is so widespread, and so important, that it is explicitly permitted, even though there is clearly not an actual element there.
7.2.6.5. Pointers and Dynamic Memory
Because we can use pointers almost as if they were arrays, the normal way to allocate dynamic memory returns a pointer to the initial element of that memory. For example, to allocate a dynamic array with n elements, we might say
int* dynamic_table = new int[n]; int_clear(dynamic_table, n);
We have not seen new int[n] before, but its behavior should be obvious: It allocates enough dynamic memory for an n-element array of integers and returns a pointer to the initial element. Once we have stored that pointer, here in dynamic_table, we can use it almost as if it were an array. For example, we can call int_clear to set the elements to 0, we can refer to dynamic_table[i], and so on.
When we are done with this memory, we can say
delete[] dynamic_table;
which returns the memory to the system. The [ ] indicate that we have asked to delete an array.
7.2.6.6. Pointers and Constants
If T is a type, we can have pointer to T, reference to T, and array of T. We can also have constant T, which is a type that is like T but whose objects are immutable during their lifetimes.
C++ uses the const modifier for that purpose. So, for example, we might say
const int N = 1000;
or, equivalently,
int const N = 1000;
with the idea that N might represent the size of some data structure, and we might want to ensure that N does not change during execution. Indeed, if a variable is declared const, and it is initialized with an expression that consists entirely of constants, then the compiler will treat the variable itself as a constant and allow its use as the size of an array. We can therefore say, for example,
int table[N];
Once we have the const modifier, it makes sense to think about how it might combine with other types. For example, if we write
const int *p;
we are saying, by analogy with use, that *p is a pointer to a const int, or, in other words, that p points to memory that we are not allowed to change. If, on the other hand, we wanted to define a pointer cp that was itself immutable, we might write
int x; int *const cp = &x;
Here, the sequence * const always defines a pointer that does not change. Such a pointer must be initialized when it is declared, because there will be no opportunity to do so later.
Among the most common uses of const is in conjunction with references. For example:
double x = 4.1; const double& y = x;
Now, y is another name for x, except that we are not allowed to use y to change the value of x:
x = 4.2; // y is now 4.2 y = 4.2; // error
One common use of references to const objects is as a way to avoid copying function arguments. Heres an example:
double abs(const double& x) { double result = x; if (x < 0) result = -result; return result; }
If a is a variable of type double and we call abs(a), there is no need to copy a into the abs function, because the function does it itself when it assigns x to result. The const double& parameter is a way of saying This parameter is a double, which need not be copied, because I promise not to change it.
We will see later that references to constants are absolutely essential as a tool for defining the semantics of abstract data types.
Previous | Table of Contents | Next |