Pointer Arithmetic
Overview
Given the pointer declaration
int *ip
- What would the (sensible) meaning of
ip + 1 be?
- incrementing by 1 byte?
- incrementing by 4 (the size of an integer) bytes?
- When would
ip + 1 make sense? ip -1?
The Details — Scaled Pointer Arithmetic
Given the pointer T *p, p is incremented by the specified increment multiplied (scaled) by the size of the type (T)
being pointed to.
char *cp: cp + 1 -> add 1 byte to cp
int *ip: ip + 1 -> add 4 bytes to ip
double *dp:dp + 1 -> add 8 bytes to dp
Valid Arithmetic Operations on Pointers
The legal pointer arithmetic operations are those that 'make sense':
pointer + integer — moves pointer upwards by (scaled) value of integer
pointer - integer — moves pointer downwards by (scaled) value of integer
- (in)equality of pointers — two pointers (not) pointing to the same location
pointer - pointer — determines (scaled) distance between pointers
ip1 - ip2
- When would this be meaningful?
- pointer comparison —
<, <=, etc
ip1 < ip2 — ip1 has a lower address in memory than ip2
- Again, when is this meaningful?
Arrays
Declaration and Initialization
- Declared with a syntax similar to Java, except they are array objects, not merely reference variables; i.e., space for the array is alocated
int arr[10];
- Declared as a local variable, they are allocated on the stack, like any other variable
- Arrays must be declared with a size
int nums[10];
double averages[14];
string names[100];
- Note the placement of the subscript (after the variable name)
- As with Java, they are 0-based
- An array may be initializes with an initalization list:
int a[5] = {1, 2, 3, 4, 5};
int b[] = {1, 2, 3};
- If the size is omitted, the number of elements in the initialization list is used to determine the size of the array
- there are specific rules for what happens is the explicit size does not match the number of elements in the intialization list
The Subscript Operator — The Array as Pointer
Given the array declaration:
int a[10];
- the identifier
a (i.e., the name of the array) is viewed as a pointer to the first element of the array
- the notation
a[i] refers to the elements i elements from the beginning of the array.
a[i] ≡ *(a + i)
- i.e., subscripting in C++ is defined via the corresponding pointer expression; subscript is sometimes called a
'secondary notation'
Passing an Array to a Function
- The contiguous area of the array is not what is passed, rather its address.
- This makes sense, the array is not the full extant of storage; but a pointer to the first element
- This has several major ramifications
- while the address of the array is usually passed by-value (and thus won't change in the function),
the content are being 'passed' by indirection and thus, changes made to the elements of the
array persist upon leaving the function
- In the function header, the array is declared without a size (the elements are not passed, merely the starting address)
bool contains(int arr[], int n, int val);
- This mean the function can handle any size array (of the proper type, of course)
- Array parameters can also be declared as pointers (which is what they are)
bool contains(int *arr, int n, int val);
- Unless the array contains some sort of trailer/sentinel value, the number of elements in the array is passed as a separate
argument
bool contains(int arr[], int n, int val);
- Compelling reason to introduce an vector/array class.
- Within the function, the array is processed as usual (usually in conjunction with the associated length parameter):
bool contains(int arr[], int n, int val) {
for (int i = 0; i < n; i++)
if (arr[i] == val) return true;
return false;
}
A Closer Look at Pointers and Arrays
i
Pointers vs. Arrays
- Declaring an array, however, is not the same as declaring a pointer to the corresponding element type
int *ip;
- Only a pointer (typically 4 or 8 bytes) is allocated)
- Not pointing at anything yet
- Can assign to
ip
int a[10]
- Space is allocated for 10 integers
- No space is allocated for the pointer — it's a constant (almost a liter) and the compiler keeps track of the
address of the beginning of the array and associates it with
a
a a points to the first element
- cannot assign to
a (no Lvalue).
a is not a pointer variable but rather a fixed name for the location of the first element
- When you code the following:
int a[] = {1, 2, 3, 4};
you are providing initial values to the elements of the array; you are not assigning an array to a
A Final Look at Pointers and Arrays
Given:
int *ip, a[10]
all of the following are legal:
a[5]
a[20]
a[-1]
5[a]
ip[5]
ip+1
a+4
a-1
*(a+2)
*(ip+1)
Recursion and Arrays
Given
int a[10]
i.e., a is an array of 10 elements, what is a+1?
The sizeof Operator
- The sizeof operator is a compile-time value; i.e., the compiler replaces the sizeof operation with
the number of bytes allocated to the operand.
cout << sizeof(int);
- The operand to
sizeof can be a type, variable, or expression.
- There are rules as to when the operands needs to be enclosed in ()'s. In general, simply use the parens.
sizeof was more hevily used in C, where dynamic allocation of memory was a function whose argument was the number of bytes to allocate
- In C++,
new takes care of that calculation, using the type that is specified
sizeof for an array returns the number of bytes allocated to the array
Dynamic Memory
Overview
- Variable names are static in the sense that they must be chosen during program editing
- New variable names cannot be introduced after editing, for example, at run-time
- One of the uses of pointers is that they allow us to keep track of memory that we will request at runtime
- The result of our request will be the address of the new memory, which we can then store in a pointer.
- With this ability, we can allocate an arbitrary amount of memory (up to the limit of the machine's memory size) during execution,
allowing for much more flexibility than previously.
- We are also not restricted to the fixed (i.e., static) number of variable names specified in the program source. As long
as we can find somewhere to save (and subsequently retrieve) all the pointers to the dynamically allocated locations, we
can have as many as we want (where we store those pointers and how we organize and manipulate them is a major portion
of the topics covered in a data structures course).
- This is also the basic idea behind linked representations (such as linked lists) … each node keeps track of the next, through its
next
field
The new Operator
The delete Operator
- Memory allocated using the
new operator is completely under the programmer's control,; and thus it is up to the
programmer to decide when the memory should be freed.
- this is achieved using the
delete operator which accepts a pointer to the memory to be freed:
delete ip;
delete [] dp;
- Note: if an array was allocated (using the
[] notation) as seen above for dp,
a corresponding notation should be used when using the delete operator on that memory.
Dynamically-Allocated Arrays
Aliases
The indirection provided by pointers introduces a new issue — that of the alias
Memory Leaks (Garbage)
In the code that resized the array, had we not deleted the old storage, we could very well have run out of memory eventually.
This is because we reassigned the arr pointer to point to a different area of storage; leaving us with no way to get
to the old area. This was not a problem in itself, because we had copied the information out of it, but it meant that we now had memory
that we could no longer use, nor get to. This situation is called a memory leak and the memory itself (which is
now useless since it can't be reached) is called garbage.
int *ip = new int[100];
ip = new int[10];
- Preventing the occurrence of garbage is one of the primary purposes of having a destructor in a class.
- Assigning a pointer away from a storage location does not automatically create garbage as long as there is an alias (i.e., another
pointer) to the location.
Dangling References/Pointers
The other side of the coin from garbage are dangling references; this is when storage IS freed up, but the programmer
still has a pointer to the freed up storage and uses it as if the storage was still available.
int *ip = new int[10];
...
delete [] ip;
...
ip[3] = 1; // uh, oh... ip's storage was freed; we have no business using ip
- Following a dangling reference is exteremely dangerous, the memory may have been reallocated and reassigned new values. In the above example,
assigning 1 to ip[3] may very well be corrupting data being used in another portion of the program (that obtained that storage in a subsequent
call to
new).
- What we need is either:
- to be very careful not to access storage through a pointer whose storage has been deleted (and has not been assigned new storage
after that), or
- have some value that can be assigned to the pointer AFTER the storage has been deleted and which indicates the pointer is currently
unused (see next section).
NULL, nullptr
The predefined value NULL (obtained via a #include of cstdlib) is a value that can be assigned to
any pointer and represents an invalid pointer.
- This value is assigned to a pointer to represent that the pointer is current not pointing at anything
- The program can test the value of a pointer for
NULL to determine whether to 'follow' the pointer.
NULL is actually defined as being 0; there is a difference of opinion whether to use NULL or 0.
- The fact that it is actually the integer value 0 created problems when used in the context of pointers
- C++ 11 introduced the
nullptr keyword to be used in place of NULL
- It is of a pointer type that is compatible with all pointer types and only pointer types.
References
- A reference is an alias to an object.
- References are declared using
&, and are initialized by supplying the original name for the object.
int i = 12;
int &ir = i;
- A reference is permanent — once assigned a reference cannot be reassigned.
- Access to the value is performed transparently
ir++; // Adds 1 to i
- In essence, reference is a pointer without all the notation and machinery
- This transparency means that the reference loses its individual identity (i.e., there's no way to manipulate the reference itself after it's created;
all 'references' to the reference refer to what it is 'pointing' to.
- Primary uses of a reference are to:
- permit call- and return-by-reference (see below)
- permit clean operator overloading
A Pattern for a Growable Array
- The trick to implementing a growable array is to use a pointer rather than an array (which must be delcared with a fixed size).
- Using a pointer allows you to call
new during runtime, when additional memory is needed.
- Furthemore, the level of indirection provided by the pointer allows access to the memeory though the pointer. I.e.,
even if the pointer is re-assigned to refer to a larger chunk of mememory, you are still going through the same
pointer.
- The basic idea is to maintain two separate size variables, and a pointer to the current allocation representing the array:
- A physical size, usually called the capacity. This corresponds to the number of elements allocated.
- a logical size, usually simply called the size representing the number of elements
actually assigned values by the calling application.
- The size is always less than or equal to the capacity. A size of 0 means the array is effectively empty; if the
size equals the capacity, the array is full.
- Inserting elements into the array incresaes the size; when the size equals the capacity, the array must be resized. This involves:
- Increasing the capacity by some value (say, 2 times the current capacity)
- Allocating new storage whose size is that of the new capacity
- Copying the elements from the old storage to the new storage
- Freeing the old storage
- Assigning the pointer to the new storage to the array's pointer
- Here is a function that ensures there is room for at least one more element in the array.
// checkCapacity accepts an array pointer, and a size and capacity; if necessary, it resizes the array
void checkCapacity(int *&arr, int size, int &capacity) {
if (size == capacity) { // full, need to grow/reallocate
capacity *= 2;
int *tmp = new int[capacity];
for (int i = 0; i < size; i++)
tmp[i] = arr[i];
delete [] arr;
arr = tmp;
}
}
- Notice the type of the
arr parm — it's declared as a reference since the pointer may change
(if the array needs to be resized).
- This is an example of a pointer being passed because it's a pointer (and not to simulate by-reference by passing 'by-pointer').
- Here is sample usage:
const int DEFAULT_CAPACITY = 10;
int capacity = DEFAULT_CAPACITY, size = 0;
int arr = new int[CAPACITY];
…
// when we now want to add an element to the array
checkCapacity(arr, size, capacity);
// when we come back, we're guaranteed there's sufficient room (for one more element)
Here is the more typical scenario … the function appears as a member function of a class that performs automatic capacity mamnagement (typically
a vector).
class vector {
public:
vector(int capacity=100) : capacity(capacity), size(0), arr(new int[capacity]) {}
…
private:
void check_Capacity() {
if (size == capacity) { // full, need to grow/reallocate
capacity *= 2;
int *tmp = new int[capacity];
for (int i = 0; i < size; i++)
tmp[i] = arr[i];
delete [] arr;
arr = tmp;
}
}
int *arr;
int size, capacity;
};
- Notice the paramaters of the previous version are no longer present; they are now encapsulated as data members of the class
- The memory management is now totally handled by the class itself