Courses/CS 2124/Lab Manual/Chapter 2

From A-State Computer Science Wiki
Jump to: navigation, search

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.


Labcheckpoint.png 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.


Labcheckpoint.png 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 of size is greater than zero, allocate the appropriately sized integer array dynamically using the new operator and store the result in the attribute _array. (If the value of size 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 the delete [] 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 if i is invalid, and if it is, throw the exception std::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.
Information Icon.png

In a pure Structured Programming paradigm, return statements should always be at the “top-level” in their corresponding function (i.e. Don’t put your return statements inside any control structures; the compiler must see that every possible path through a function results in a return being executed.)

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:

`ArrayWrapper`

`DynamicArray`

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.


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate your progress when you reach this point.


Labsubmitsinglefile.pngWhen you complete the assignment, zip all source code files and submit the archive as OOP_lab03_inlab.zip.