Lecture 7 - Dynamic Storage Allocation in C++

Storage Classes

Storage is allocated for variables in different ways depending on the storage class of the variable. The storage classes are:
  1. Automatic: Storage is allocated when the scope containing the variable definition is entered and released when execution leaves the scope. This is the normal storage class for function parameters and (unless declared otherwise) local variables.
  2. Static: Storage is allocated when the program begins execution and remains allocated until the program terminates. Applies to global data defined outside all functions and to variables declared static inside functions.
  3. Dynamic: Storage is allocated and released during execution under control of the program. (New!)

Dynamic Allocation

Dynamically allocated variables are anonymous - they do not have names (identifiers). They are created by the new operator, which returns a pointer to newly allocated data of the specified type. Dynamic data is deleted (and the storage occupied by it is reclaimed) by the delete operator.
int *ip = new int;    // ip points to a new integer created by new
int *iq;

*ip = 17;
iq = new int;
*iq = *ip + 1;

delete ip;	//  get rid of the integer pointed to by ip.
delete iq;
The basic forms of new and delete work on all types except arrays. When new is used for an object, the constructor is automatically called. Likewise, when delete is used, the object's destructor is called.
struct pair {  char a, b; } ;

pair *pr;

pr = new pair;

(*pr).b = 'y';		// Set value of field of struct pointed to by br
pr->a = 'x';		// pr->a is an abbreviation for (*pr).a

delete pr;

A null pointer is one that is guaranteed not to point to allocated data. It can be written as 0 or NULL; both mean the same thing.

Avoid mixing the C library dynamic allocation routines malloc and free with the new and delete operations (unless you really know what you are doing). A well-written C++ program should use new and delete.

Pitfalls

Pointers and dynamic allocation are enormously useful tools, but they are also prone to subtle errors. Here are some of the more common ones.
  1. Dereferencing a pointer before initialization:
    int *ip;
    *ip = 3;		// clobbers some random part of memory
    
    A pointer must point to something before it can be dereferenced. Initialize with either ip = &somevar; or ip = new sometype;

  2. Dereferencing a pointer after delete (a dangling pointer). Examples:
    ip = new int;		 or			ip = new int;
    *ip = 3;					iq = ip;
    delete ip;					delete ip;
    cout << *ip;					*iq = 3;
    
    A useful technique is to initialize or set a pointer to NULL (0) if it doesn't point anywhere. Attempts to dereference 0 are less likely to slip by unnoticed. Or use assert to verify p != 0 before dereferencing p.

  3. Memory leaks (memory that cannot be referenced but still consumes space).
    pair *p, *q;
    p = new pair;
    q = new pair;
    p = q;
    
    In C++, dynamically allocated storage is not reclaimed automatically as it is in languages like Java that provide automatic garbage collection.

  4. Deleting a pointer that was never allocated or has already been deleted (allocated storage can only be returned to the free pool once).
    delete p;			or			p = q;
    ...							delete p;
    delete p;						delete q;
    

Dynamic Allocation of Arrays

Variants of the basic new and delete operators are used to allocate storage for dynamic arrays. The number of array elements to be allocated is a parameter to new. Brackets are used with delete.
int k, sz;	// sz == # of items
double *dp;	// ptr to array of sz items

cout << "How much data? ";
cin >> sz;

dp = new double[sz];

for (k=0; k> dp[k];
...
// process data
...
delete [ ] dp;
WARNING: Strange and terrible things will happen if you attempt to free storage using the basic form of delete on storage allocated by new[ ] or use delete[ ] on storage allocated by the basic new operator.

Dynamic Allocation of 2-D Arrays

The new operation works for 2-D arrays, but it is not as flexible as one would like. The sizes of all but the first dimension must be specified with constants.

A much more useful way to implement a 2-D array is to allocate an of pointers to the rows in the 2-D array, and then allocate each row individually. For a 2-D arrray with nr rows, the data structure looks like this.

2-D Array Allocation

Here's the code to allocate a 2-D array with nr rows and nc columns, and allocate and initialize the row pointers.
double **a;		// data array as a 2-D array

// Allocate array of pointers to each row
a = new double*[nr];

// Allocate and initialize each row
for (int i = 0; i < nr; i++ ) {
	a[i] = new double[nc];
	for (int j = 0; j < nc; j++) { 
		a[i][j] = 0;
	}
} 
Storage occupied by the array is released in the opposite manner in which it was allocated. DO NOT just delete a.
for (int i = 0l i < nr; i++ ) {
	delete [] a[i];
}
delete [] a;

Classes with pointers to dynamic memory

Let's see what happens when we modify the dictionary from assignment #3 so that there isn't a limit to the number of words we add. We'll do this by dynmically allocating the arrays to hold the words and replacing them when they aren't big enough.

class dict {
   int numwords;	// Number of words in dictionary
   int sz;		// size of arrays		
   int* count;		// pointers to dynamically allocated arrays
   string* words;
   
 public:
   dict();		// constructor
   void add(string);	
  
   // New -  Needed this time	
  ~dict();		                // destructor	
   dict ( const dict& );		// copy constuctor
   dict & operator= ( const dict& );	// assignment operator
};
  	
First we should change the add function to give us more space if we run out.
void dict::add( string word ) {
if the word is already there { 
 ....  (do like before) ...
} else { 

  // See if we are out of space 
  if (numwords == sz) {
    
     // Create new arrays for the data, twice as big as before.
     sz = sz*2;
     string* tempw = new string[sz];
     int * tempc = new int[sz];

     // Copy all of the old data into the new arrays
     for (int k=0; k < numwords; k++) {
       tempw[i] = words[i];
       tempc[i] = counts[i];
     }

    // deallocate the space for the old ones
    delete [] words;
    delete [] counts;

    // set the pointers to point to the new arrays.
    words = tempw;
    counts = tempc;
  }

  // Now add the word 
  ...
  ...

}

Because we've changed the add function, we need to modify the constructor (and write a destructor!)

// The add function expects that words and counts point to some
// allocated memory. So we should give it space for one word to 
// begin with.

dict::dict () {
   numwords = 0;
   sz = 1;

   words = new string[sz];
   counts = new int[sz];

}


// Now, in order not to leak memory, the destructor deallocates our arrays.

dict::~dict() {
   delete [] words;
   delete [] counts;	
}

In order to be complete, we need two more functions:
   dict ( const dict& );		// copy constuctor
   dict & operator= ( const dict& );	// assignment operator
I didn't really cover these well in class, so we'll go over them in the next lecture.