Chapter 5. Advanced Memory Handling

Table of Contents

Storage types in C++
Constant Data
Automatic Life
Dynamic Life
Static Life
Local Static Variables
Array Elemenst
Life of Object Attributes
Union Member
Temporary
New and Delete
Operators
Overloading the New and Delete operators
Extra Parameters for New and Delete
Auto_ptr class
Usage of Auto_ptr class
The Definition of Auto_ptr
Problems with Auto_ptr
Memory Handling Strategies
Auto_ptr as Class Member
Arrays
Arrays are not Polymorphic
Arrays and Auto_ptr
Array Delete Adapter
Objects with Special Storage Restrictions
Objects only in the Heap
Objects not in the Heap

Memory-models in C++. New and delete operators. Constructors, exceptions and memory-leaks. The auto_ptr - and when it does not help. Write your own smart pointer. Arrays are not polymorphic! C++ object internals.

Storage types in C++

In C++ objects are mapped into the memory based on their storage types. Different storage types means different life rules.

Constant Data

Values are known at compile-time:


const char *hello1 = "Hello world";
      char *hello2 = "Other hello";
      
// hello1[1] = 'a'; // syntax error
hello2[1] = 'a';    // could cause runtime error

char *s = const_cast<char *>(hello1);   // dangerous
s[3] = 'x';         // could be runtime error !

This kind of data stored outside of the program read-write area. Therefore write attempt is undefined.

There is difference between a string literal and an initialized character array.


#include <iostream>

int main()
{
    // declaration of three arrays in the user data area
    // read and write permissions for the elements:
    char t1[] = {'H','e','l','l','o','\0'};
    char t2[] = "Hello";
    char t3[] = "Hello";

    // declaration of two pointers in the user data area
    // read and write permissions for the pointers
    // ...and...
    // allocation of the "Hello" literal (possibly) read-only
    char  *s1 = "Hello";    // s1 points to 'H'
    char  *s2 = "Hello";    // ... and s2 likely points to the same place  

    void  *v1 = t1, *v2 = t2, *v3 = t3, *v4 = s1, *v5 = s2;
    std::cout <<v1<<'\t'<<v2<<'\t'<<v3<<'\t'<<v4<<'\t'<<v5<<std::endl;
    // the result (v1, v2 v3 are different, v4 and v5 could be the same):
    0xbffff460   0xbffff450    0xbffff440   0x8048844   0x8048844

    // assignment to array elements:
    *t1 = 'x'; *t2 = 'q'; *ct = 'y';

    // modifying string literal: could be segmentation error:
    *s1 = 'w'; *s2 = 'z';

    return 0;
}

Automatic Life

Objects local to a block (and not declared static) has automatic life. Such objects are created in the stack. The stack is safe in a multithreaded environment. Objects created when the declaration is encountered and destroyed when controll leaves the declaration block.


void f()
{
    int i = 2;  // life starts here with initialization
    ....
}               // life finished here

Dynamic Life

Objects with dynamic life is created in the free store. The lifetime starts with the evaluation of a new expression (not the new operator). The life ends at the delete expression.


char *p = new char[1024];   // life starts here
...
delete [] p;                // life finished here

There are two kind of allocation: one for single objects and one for arrays. The delete operation must be corresponding with the type of the new operation.


char *p1 = new char[1024];
char *p2 = new char[1024];

delete [] p1;
delete [] p1;   // could be runtime error: p1 deleted twice
delete    p2;   // could be runtime error: p2 points to array

The free store is often referred as the heap. This is not correct: free memory (new) and heap (malloc) is not neccessary the same.


char *p = malloc(1024); // allocates in "heap"
...
free(p);

Static Life

Global variables, namespace variables, and static datamembers have static life. Static life starts at the beginning of the program, and ends at the end of the program.


date d(2003,3,13);  // life of "d", "i" starts here
static int i;
int main()
{                       // initialization/constr call happens here
    ...
}                       // destr call happens here

The order of creation is well-defined inside a compilation unit, but no defined order between source-files.

Local Static Variables

Local statics are declared inside a function as local variables, but with the static keyword. The life starts (and the initialization happens) when the declaration first time encountered and ends when the program is finishing.


int main()
{
    while (... )
        if ( ... ) {
            static int j = 6;   // initialization happens here
        }
}                               // destr call happens here

Array Elemenst

Array elemenst are created with the array itself in order of indeces. Array elements destroyed when the array itself is deleted.

Built-in arrays by the standard have size known by the compiler, aka constant expression. Some compiler, like GNU G++ is submissive: accept variable size arrays with warnings. In C9X variable size arrays are accepted.


#include <vector>

struct X { X(int i) { x = i; }; int x; };
struct Y {                      int y; };

int main()
{
    const int n = 4;
          int k = 4;

 // X x1[n];    // no default constructor
    X x2[n] = { 1, 2, 3, 4};
    Y y1[n];
    Y y2[k];    // k is non-const: error by standard

 // X *xp = new X[k];   // no default constructor
    Y *yp = new Y[k];

 // std::vector<X> xv(10);  // no default constructor
    std::vector<Y> yv(10);

    delete [] yp;
}

Life of Object Attributes

(Non-static) data members are created when their holder object is created. If they have constructor, then their constructor will be woven into the container object constructor. The subobjects will be initialized by their constructor. However built-in types have no constructor, so they must be explicitelly initialized.

Union Member

A member of a union has two constraints:

  • Member must not have constructor or destructor

  • The union must not have static field.

Temporary

Created under the evaluation of an expression and destroyed when the full expression has been evaluated.


void f( string &s1, string &s2, string &s3)
{
    const char *cs = (s1+s2).c_str();
    cout << cs;     // Bad!!

    if ( strlen(cs = (s2+s3).c_str()) < 8 && cs[0] == 'a' ) // Ok
        cout << cs;     // Bad!!
}

The correct way:


void f( string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    string s = s2 + s3;

    if ( s.length() < 8 && s[0] == 'a' )
        cout << s;
}

When we assign a name to a temporary, the scope of the name will define the life of the temporary:


void f( string &s1, string &s2, string &s3)
{
    cout << s1 + s2;
    const string &s = s2 + s3;

    if ( s.length() < 8 && s[0] == 'a' )
        cout << s;  // Ok
} // s1+s2 destroyes here: when the const ref goes out of scope