DYNAMIC MEMORY ISSUES IN C++

Whenever the implementation of a class involves dynamically allocated memory, the class should explicitly define:

Delete

(See Main & Savitch, pp. 145-147; Wang 100-101)

The C++ keyword delete is used to release dynamically allocated memory so that it may be reused.

Dynamic variables (memory allocated by newshould be deleted when they will no longer be used. Deleted dynamic variables should NOT be reused unless that have been reallocated with another call tonew.
int *i = new int
*i = 10;
   // some uses of i (and *i)
   ...
   // i no longer needed
delete i;          // OK


int x = 2;
int *y = &x;       // y gets the address of x
delete y;          // BAD - y NOT dynamically allocated


int *i = new int
   // some uses of i (and *i)
delete i;
*i = 10;           // VERY BAD - i has been deleted


int *i = new int
   // some uses of i (and *i)
delete i;
i = new int;
*i = 10;           // OK *i has been reallocated

Delete invokes the destructor

From StringList::remove() we have delete location. location is a pointer to a ListItem so, destructor for ListItem is invoked. In turn, since ListItem has member String val, the destructor for String is invoked.

Default destructors call destructors of member objects, but do NOT delete pointers to objects. Thus, we need to write destructors that explicitly call delete.

Destructors

(See Main & Savitch, pp. 166-169; Wang 183-185)

The destructor for a class is a member function that is automatically activated in three cases:

  1. for a local object that goes out of scope:
    { 
      StringList L;
         // some uses of L
    } // L.~StringList() automatically called
        
  2. for a global object when the program terminates
  3. when a pointer to an object is deleted:
    Foo *f = new Foo;
         // some uses of f (and *f)
    delete f;  // f.~Foo() automatically called
        

An object's destructor always invokes the destructors of its member objects (not for member variables that are pointers to objects) regardless of whether or not such calls are made explicitly in the destructor. For example, suppose we have:

class Foo {
public:
  ~Foo();
  // ...
};

class Bar {
public:
  Foo f;
  Foo *p;
  ~Bar(){delete p;}
};

int main()
{
  Bar b; 
  b.p = new Foo;

  return 1;
}
Upon exit from the main function, the destructor for b (b.~Bar()) is automatically invoked. The explicit definition of the destructor calls delete p which then invokes Foo's destructor for the member object *p. The destructor then automatically calls the destructor for the member object f, even though no explicit call is made within the ~Bar code we have written.

Copy constructors and assignment overloading

(See Main & Savitch, pp. 169-172, 177-178; Wang 249-256)

Assignments are from values to variables. If x and y are variables then x = y means the variable x is assigned the value of variable y. Immediately following the assignment the values of variables x and y are identical.

In the case of non-pointers, such as

int x = 3;
int y = 5;
x = y;
things are `well behaved'. Now consider:
int *p = new int(1);
int *q = new int(2);
p = q; 
The value of q (NOT *q) is copied to p. That means that the pointer is copied. This means that at a later assignment of the form *p = 3 the value 3 is copied to only one place (*p), but (*q) is the same, so both *p an d *q change. So, with pointer assigned associated dynamic variables become the same.

Default assignment for objects

class Foo{
public:
  int i;
  int *p;
}

Foo f,g;

f.i = 6;
f.p = new int(4);

g.i = 1;
g.p = new int(2);

g = f
Assignment occurs at each member variable so it's as if there were these two assignments:
g.i = f.i;   // non-pointer assignment
g.p = f.p;   // pointer assignment
This is a shallow copy. It is called shallow because on the surface it appear that there are now two separate copies of 4 (*(f.p) and *(g.p)), but in fact there is only one copy since *(f.p) and *(g.p) are the same dynamic variable. There are two big problems with this: We can overcome this problem with overloading the assignment operator as follows:
void Foo::operator=(const Foo& source)
{
  // do nothing if the assignment is of form x = x
  if (&source != this) { 
    i = source.i;
    delete p;   // free old *p memory
    p = new int // allocate new *p memory
    *p = *(source.p);
  }
}
In general, our prescription for assignment for objects that use dynamic memory is:
  1. Check for self assignment
  2. Release dynamically allocated memory that the assigned to object was using
  3. Allocate new memory for x to use that matches y's storage needs
  4. Perform deep copy

Sometimes we want/need to create a new object that is identical to an existing object. This operation is called the copy constructor. It's behavior is nearly identical to the assignment except that since this is the first allocation of dynamic memory for the new object then no memory need be released at this time.

Copy constructors need to be written explicitly in exactly the situation when assignment needs to be written explicitly: when an object uses dynamically allocated memory. For example:


Foo::Foo(const Foo& source) 
// copy constructor for class Foo
{
  i = source.i;
  p = new int;
  *p = *(source.p);
}

Foo x(y); // create a new Foo x, initialized as a copy of y
Copying of objects occurs in three cases:
  1. One object initlaized by another - explicit copy construction:
    String s = t;
    StringList L(M);
        
  2. Function returns an object by value:
    String StringList::current();
  3. Object passed by value. Suppose we had:
    void StringList::insert(String s);
    Then at L.insert(s) a copy of s is made