Session 9: Memory Management

The issues

Programs manipulate data, which must be stored somewhere.

Common storage modes

(This is different from scope, which is a compile-time attribute of identifiers.)

static
exists for the duration of program execution.
local (or stack-based)
exists from entry of a block or function until its exit.
free (or dynamic, or heap-based)
explicitly created, and either
temporary
for intermediate values in expressions.

Static storage in C++

Variables may be initialized when defined:
        int i;
        int *p;
        int area = 500;
        double side = sqrt(area);
        double *ptr = &side;

Implicit initialization of static variables

Static variables that are not explicitly initialized are implicitly initialized to 0 converted to the type.

        int i;
        bool b;
        double x;
        char *p;
is equivalent to
        int i = 0;
        bool b = false;
        double x = 0.0;
        char *p = 0;     // null pointer

Evaluation

Static storage is

simple
No extra effort from the programmer.
safe
Storage is guaranteed.
inflexible
Must determine limits at compile-time.
wasteful
We often allocate more than needed. Also, the storage is held for the entire execution, even if it is not being used.

Local storage in C++

        int f(int start, int size) {
                int total = 0;
                int tmp;
                for (int i = start; i < size; i++) { ... }
        }

Evaluation

Local storage is

efficient
The implementation merely adjusts a stack pointer.
often suitable
If the data is being used in a block-structured way.
not enough
What if we wish to construct some data in a function and return it to the caller?

Free storage in C++

Class types:

        Point *p;      // uninitialized pointer
        p = new Point; // default constructor
        p = new Point(1,3);
        cout << p->x << ' ' << p->y << '\n';
        delete p;
and similarly for primitive types.

Dynamically allocated arrays in C++

A pointer can also address a dynamically allocated array:

        int *arr;
        arr = new int[n];
        for (int i = 0; i < n; i++)
                arr[i] = f(i) + 3;
        delete[] arr;
Note the special syntax for deletion syntax, which is required because C++ doesn't distinguish a pointer to an int from a pointer to an array of ints.

Destructors

A class C may include a destructor ~C(), to release any resources (including storage) used by the object.

    class C {
        Date *today;
        int *arr;
    public:
        C() { today = new Date(); arr = new int[50]; }

        virtual ~C() { delete today; delete[] arr; }
    };
Destructors are called in the opposite order to constructors.

Why virtual?

Suppose B is a derived class of A. Then in

        A *p;
        p = new B;
        ...
        delete p;
the destructor ~B() will not be called unless A's destructor is virtual.

So why aren't destructors virtual by default? Because that would be a little less efficient.

Construction and destruction

  Storage allocated, initialized by a constructor The destructor is called, storage reclaimed
static object when the program starts when the program terminates
local object when the declaration is executed on exit from the function or block
free object when new is called when delete is called
subobject when the containing object is created when the containing object is destroyed

Example: a simple string class

class String {
        int len;
        char *chars;

public:
        String(const char *s) :
                    len(strlen(s)), chars(new char[len]) {
                for (int i = 0; i < len; i++)
                        chars[i] = s[i];
        }

        // more to come later ...
};

Default constructor

We also have a default constructor making an empty string:

        class String {
                int len;
                char *chars;

        public:
                String() : len(0), chars(new char[0]) {}

                // ...
        };

Destructor

The constructors allocate dynamic storage, so the destructor must delete it:

        class String {
                int len;
                char *chars;

        public:
                // ...

                virtual ~String() { delete[] chars; }

                // ...
        };

Initialization of objects

A problem

Here are some initializations:

        {
                String empty;
                String s1("blah blah");
                String s2(s1);  // initialized from s1
                String s3 = s1; // initialized from s1
        } // all four strings are destroyed here

Solution: define a copy constructor

We define a copy constructor to copy the character array:

        String(const String &s) :
                        len(s.len),
                        chars(new char[len]) {
                for (int i = 0; i < len; i++)
                        chars[i] = s.chars[i];
        }
This copying (deep copy) is typical: with explicit deallocation, it is generally unsafe to share. In this case, Java is more efficient.

Assignment

More problems

Consider

        {
                String s1("blah blah");
                String s2("do be do");
                s1 = s2;        // assignment
        } // the two strings are destroyed here

Solution: define an assignment operator

We define an assignment operator inside the String class:

    String & operator= (const String &s) {
            if (&s != this) {   // don't copy onto self
                    delete[] chars;
                    len = s.len;
                    chars = new char[len];
                    for (int i = 0; i < len; i++)
                            chars[i] = s.chars[i];
            }
            return *this;
    }

The this pointer

In C++,

An alternative: forbid copying

If we define a private copy constructor and assignment operator,
    class String {
    private:
        String (const String &s) {}

        String & operator= (const String &s) {
            return *this;
        }
        ...
The compiler will not generate them, but the programmer will not be able to use these ones. Any attempt to copy strings will result in a compile-time error.

Summary

For each class, the compiler will automatically generate the following member functions, unless the programmer supplies them:

copy constructor:
memberwise copy
assignment operator:
memberwise assignment
destructor:
do nothing (subobjects are destroyed automatically)
If no constructor is supplied, the compiler will generate a default constructor: memberwise default initialization.

If these defaults are not what we want, these functions must be defined.

Summary, continued

Summary, concluded

Next week

Reading: Savitch section 11.1, Stroustrup chapter 9.