Courses/CS 2124/Lab Manual/Pointers and Dynamic Memory
Introduction
This lab introduces the use of pointers, and dynamic memory with classes.
Pointers and Pointer Arithmetic
Thus far we have used memory to store integers, floating-point values, characters, etc. Each time a variable is declared in a program, memory is reserved. The computer keeps track of the variable’s memory location so the program does not have to. However, it is possible for a program to determine the memory address of a variable. It is also possible to store a memory address. A pointer variable, typically referred to as a pointer, is a variable than can hold a memory address. A variable that stores a memory address is called a pointer because it points to another variable stored in memory.
Declaring a Pointer
To define a pointer, use an asterisk (*
) in the declaration to specify the variable will be a pointer to the specified data type. The asterisk in this context is the pointer type modifier.
Create a program pointers_inlab.cpp containing a main()
funciton and add the following code:
int values[5] = {325, 879, 120, 459, 735};
int* valuePtr = values; // valuePtr is a pointer to values.
Recall that the name of an array references the memory address of the first element of the array. The second statement then would store the memory address of the first value in the variable valuePtr
.
The name of a pointer is an identifier and must follow the rules for defining an identifier. Some programmers place the letters ptr
, a common abbreviation for pointer, at the end of the variable name, others prefix a pointer with p_
or ptr_
, and there are those who do nothing to distinguish a pointer by its name. The naming of a pointer variable is a matter of programming style.
Once a pointer has been declared and initialized, it can be used to access the data to which it points. In order to access this value, the dereference operator, *
, must be used to prefix the name of the pointer. From the code above, valuePtr
holds the address of the first element of the values
array; therefore, *valuePtr
will access the value 325
. Add the following to main()
and execute the program.
cout << "valuePtr: " << valuePtr << endl;
cout << "*valuePtr: " << *valuePtr << endl << endl;
The output generated should look something like the following.
0xbfad43e8
325
Where 0xbfad43e8
is a memory address displayed in hexadecimal.
Obtaining the Memory Address of a Variable
The address operator, &
, is used to determine the memory address of a variable that is not an array. Add code necessary to use fixed
and setprecision
, then add the following to main()
and run the program to confirm the use of the address operator.
double payRate = 10.25;
double* ratePtr = &payRate;
cout << fixed << setprecision(2);
cout << "address of payRate: " << ratePtr << endl;
cout << "value stored in payRate: " << *ratePtr << endl << endl;
Using a Pointer to Alter Data
Just as the dereference operator is used to retrieve data, so too is it used to store data. Add the following to function main()
and run the program.
*ratePtr = 12.50;
cout << "altered value of payRate: " << payRate << endl << endl;
The pointer, ratePtr
, is used to change the value stored in payRate
. The output of payRate
confirms the change.
Using a Pointer in an Expression
As previously shown, the value pointed to by a pointer variable can be retrieved by dereferencing the pointer. In the above code, the retrieved value was simply displayed to the screen; however, it can also be used in an expression. Add the following to main()
and execute the program.
double grossPay;
grossPay = payRate * 40;
cout << "grossPay calculated with payRate: " << grossPay << endl;
grossPay = *ratePtr * 40;
cout << "grossPay calculated using ratePtr: " << grossPay << endl << endl;
Pointer Arithmetic
It is possible to add to or subtract from a memory address stored in a pointer. These actions can be accomplished using the addition operator,+
, subtraction operator, -
and the increment and decrement operators, ++
and --
. This is helpful when accessing arrays via pointers.
The increment operator has been used to add the value 1
to a variable. When used with a pointer, the increment operator adds the size of the data referenced to the memory address stored in the pointer, effectively moving the pointer to the next data value. Building on the code from above, add the following to main()
and execute the program.
cout << "values[0]: " << values[0] << endl;
cout << "*valuePtr: " << *valuePtr << endl;
valuePtr++;
cout << "values[1]: " << values[1] << endl;
cout << "*valuePtr after increment: " << *valuePtr << endl << endl;
We initialized valuePtr
to the memory address of the first element of the array values
. Incrementing valuePtr
instructs the computer to add four bytes, the size of an integer, to the memory addressed stored in valuePtr
. After the increment, valuePtr
now points to the second element of the array.
The addition operator, +
, works in a similar fashion. Adding an integer value to a pointer calculates a new pointer that is offset by the amount specified by the integer, in the “forward” direction (if you view “forward” as “further into an array”).
The subtraction and decrement operators perform in a similar manner, only moving logically “backward” in memory.
![]() |
FOR IN-LAB CREDIT: Demonstrate your progress when you reach this point. |
Pointers as Parameters
As with any other variable, pointers may be passed to functions as parameters. They can be useful to allow a function to observe (read or modify) a value in the scope of the calling function. This is similar to how we use reference parameters.
A pointer parameter is also useful for passing arrays to functions since a pointer to an array may be viewed as the array itself. Add the following to the program.
void displayArray(int* array, int size)
{
cout << "Array values: \n";
for (int i = 0; i < size; ++i){
cout << array[i] << endl;
}
cout << endl;
}
Add the following to function main()
and execute the program.
displayArray(values, 5);
One final array notation option exists for the displayArray
function. While an array name’s value cannot change, a pointer’s value can change. When an array is declared, the name of the array holds the memory address of the first element. If this value could be changed, there would be no way to retrieve the original memory address and data would be lost. The name of an array behaves like a constant pointer — the pointer itself cannot change. Here, we use a non-const
pointer to reference items in the array… There is no danger in changing the pointer’s value (or memory address) in the function as the name of the array, in the calling function, still holds the original memory address. Replace the previous pointer notation version of displayArray
with the following pointer notation version and execute the program.
void displayArray(const int* const array, int size)
{
const int* const end = array + size; // mark the end of the array
cout << "Array values: \n";
for (const int* current = array; current != end; current++){ // "moves" current toward end
cout << *current << endl;
}
cout << endl;
}
In the parameter declaration const int* const array
, the first const
qualifier says that the values pointed to by array
cannot be changed. The second const
qualifier says that the address stored in array
cannot be changed. The local end
marker is similarly qualified. The local current
variable only needs to promise not to change the values, so it only has one const
qualifier.
Dynamic Memory Allocation
Sometimes we need to reserve a specific amount of memory at runtime (during our program’s execution). One reason is that we might be creating new objects in response to interactions with a user. Another is that we may decide to create a new array whose size was determined by a calculation performed at runtime.
The new
Operator
The new
operator allows the program to dynamically reserve memory at runtime. The new
operator has one operand on its right which specifies the type of object to allocate — the type of object determines the size, or amount of memory that is allocated. The new
operator returns the memory address of the allocated memory.
pointerName = new object_to_allocate;
Where object_to_allocate
is a data type and pointerName
has been previously declared as a pointer of that type.
Once allocated, the memory can be accessed via the pointer as discussed previously. It is important to note that memory allocated in this way is not initialized unless it is an object-type that is automatically initialized. Be sure to initialize the memory before accessing it in an expression.
The delete
Operator
After memory has been allocated using new
, the memory is accessible until it is released using the delete
operator. Releasing the memory with delete
allows it to be used for other purposes either later in the program or by another process running concurrently on the computer.
delete pointerName;
Add the following to main()
and execute the program.
double* price = new double;
*price = 88.25;
cout << "address of price: " << price << endl;
cout << "value of price: " << *price << endl << endl;
delete price; // de-allocates the memory (the OS will mark it as "available")
price = nullptr; // null the pointer so that it cannot be dereferenced again.
delete
must never be used to release any memory that was not obtained via new
. The address nullptr
is reserved so that any attempt to dereference a pointer to that address will fail. You should always set an owning pointer to nullptr
after freeing the memory that it was managing, since the address of that (now deallocated) memory is still stored in the pointer; leaving it intact opens you to the possibility of accidentally making use of a storage location that no longer belongs to your program.
new[]
and delete[]
Use the []
operator in conjunction with new
to dynamically allocate an array.
arrayName = new data_type[size_of_array];
Where arrayName
has been declared a pointer to the specified data type. This instruction will allocate size_of_array
many elements of data_type
. As with any array, the elements will occupy consecutive memory addresses. As with any locally declared variable, the memory is not initialized.
Use the []
operator in conjunction with delete
to release any memory allocated with the new[]
operator.
delete [] arrayName;
Add the following to the program.
void getData (int* const array, int size)
{
for (int i = 0; i < size; ++i){
cout << "Enter value " << (i + 1) << ": ";
cin >> array[i];
}
}
Add the following to function main()
and execute the program.
int sizeOfDynArray;
cout << "How many values would you like to enter? ";
cin >> sizeOfDynArray;
int* dynArray = new int[sizeOfDynArray];
getData(dynArray, sizeOfDynArray);
displayArray(dynArray, sizeOfDynArray);
delete [] dynArray;
dynArray = nullptr; // null the pointer for safety.
With this code, the programmer is no longer forced to decided memory usage prior to the program’s execution. Here, the user specifies the number of elements that will be needed; that amount of memory and only that amount of memory is reserved.
Memory allocated using new
remains available until it is released. delete
need not be used within the function using new
. To demonstrate, modify the getData
function as follows.
int* getData (int& sizeOfDynArray)
{
cout << "How many more values would you like to enter? ";
cin >> sizeOfDynArray;
int* dynamicArray = new int[sizeOfDynArray];
for (int i = 0; i < sizeOfDynArray; ++i){
cout << "Enter value " << (i + 1) << ": ";
cin >> dynamicArray[i];
}
return dynamicArray;
}
Then modify main()
as follows and execute the program.
int sizeOfDynArray;
int* dynArray = getData(sizeOfDynArray);
displayArray(dynArray, sizeOfDynArray);
delete [] dynArray;
dynArray = nullptr; // null the pointer for safety.
Notice the allocation of memory has been moved to the getData
function while the release of memory remains in function main()
. Memory for locally declared variables is reserved at the beginning of a function and then released at the end of that same function. Memory allocated via new
is not automatically released at the end of a function nor is it required that delete
be used to release it before the end of the function. In the above code, dynamicArray
is a pointer variable local to the getData
function; the pointer dynamicArray
will be released at the end of function getData
. If the address stored in dynamicArray
is not returned at the end of this function, access to the memory would be lost but the physical memory would still be allocated to the program — this is referred to as a memory leak.
![]() |
FOR IN-LAB CREDIT: Demonstrate your progress when you reach this point. |
RAII and Destructors
Object-Oriented C++ offers a tool to help protect against memory leaks. If we create objects to represent dynamic data structures (like a dynamic array), the object itself can be responsible for its own initialization, memory allocation, and eventually memory deallocation. The lifetime of the dynamic resources can then be bound to the lifetime of the object itself. The term used for this idea of managing dynamic resources within a class in such a way that external users need not be aware of it is a part of the C++ design pattern known as RAII (Resource Allocation Is Initialization).
Add a new header file DynamicArray.h and a corresponding DynamicArray.cpp implementation file. We will start with the following simple class definition in the header file:
class DynamicArray{
public:
DynamicArray(int size);
int size() const { return _size; }
int& at(int i);
~DynamicArray();
private:
int* _array = nullptr;
int _size = 0;
};
In your implementation file, start by implementing the constructor as follows:
- The parameter
size
contains the number of elements the user wants to store. If the value ofsize
is greater than zero, allocate the appropriately sized integer array dynamically using thenew
operator and store the result in the attribute_array
. (If the value ofsize
is not positive, do nothing and allow the default “empty” array state to remain.) - Create a temporary
cout
statement in the constructor to indicate that the array has been created and what size it is.
Check that your code will compile (no need to run it yet though). Fix any errors you receive.
At this point your object would allocate memory when it is instantiated, and the memory is owned by the attribute _array
. Thus, it is the responsibility of the object to deallocate the array memory when we are finished using it. When would this be? Well, an obvious choice is when the object itself is being destroyed (by going out of scope or being deleted).
C++ allows us to define a special kind of method called a destructor that will execute automatically whenever the object’s lifetime is over. The prototype for the destructor is the same as a default constructor, except that it begins with the “tilde” symbol (~
). In our case, the prototype is:
~DynamicArray();
Now, implement the destructor for DynamicArray
in the implementation file. The destructor’s responsibility is to perform any necessary shutdown actions on behalf of the object. In our case, since the object owns some dynamic memory, it is the destructor’s responsibility to make sure that memory is released:
- Release the dynamic memory owned by the
_array
attribute by using thedelete []
operator. - Create a temporary
cout
statement in the destructor to indicate that the array’s memory has been freed.
Now, check that your code will compile. When it does, add code to your main program to attempt to create an array (you choose the size anywhere between 10 and 10000). Compile the program and run it. You should observe the statements generated by both the constructor and the destructor. If you do not, fix the issue before moving on.
Finally, implement the at()
method so that it performs these actions:
- The parameter
i
represents an index into the array that the user would like to access. Begin by checking to see ifi
is invalid, and if it is, throw the exceptionstd::out_of_range
with the following statement:
throw std::out_of_range{"Array index out of range."};
- Note: you will need to incude the
<stdexcept>
header file to use this exception object’s name.
- After the bounds-check (but not inside an
else
), return the array element at the specified index.
Add code to your main program to test the at()
method. Notice that the method’s return value is by-reference, which means that it returns a value that can be written to as well as read. So try using the at()
method to both read a value from the array and to write a value into it. As a hint, a loop is shown below that will initialize the array to contain 100 in all elements, assuming the DynamicArray
object is named my_dynamic_array
:
for(int i = 0; i < my_dynamic_array.size(); ++i){
my_dynamic_array.at(i) = 100;
}
Be sure to compile and test your program thoroughly before submitting.
WARNING: DynamicArray
Can’t be Safely Copied!
Unlike the ArrayWrapper
classes you have worked with before, DynamicArray
is not well-behaved with “by-value” semantics.
The reason is that the internal structure is very different. The logical object for both kinds of array class is “an array”, but the physical objects are different. Classes containing automatic (i.e. “plain old”) C++ arrays completely contain the storage associated with the array; thus, we say that the physical object and logical object match.
But, with classes like DynamicArray
, the class definition itself does not actually contain an array, even though we logically think of it as being “an array”. The class definition actually contains only a pointer. The array itself doesn’t exist in the physical object, and is only created at runtime — and even then it is in a different physical location in memory than the rest of the object.
See the figures below for a graphical representation of the difference:
For now, you should just be aware that any attempt to make a copy of a DynamicArray
object will result in an incomplete copy and an unintended “sharing” relationship where both objects will share the same underlying array storage. This happens because only the pointer itself is copied, not the memory containing the array that was dynamically allocated.
You will not attempt to fix this issue here, but you will tackle the subject in the homework assignment this week.
![]() |
FOR IN-LAB CREDIT: Demonstrate your progress when you reach this point. |
![]() | When you complete the assignment, zip all source code files and submit the archive as OOP_lab03_inlab.zip . |