Storage

Aims

To enable students to:

  1. handle pointers

  2. use memory allocation routines

  3. understand storage classes

Handle pointers

To date variables have been viewed as a convenient way of naming a particular location in memory. When referring to a variable access is made to its contents, for example if an int variable is created called iTemperature, the process of assigning it the value 25 results in the number 25 ending up in some memory location - we are not interested in which location, just that the action has occurred.

In programming it is frequently useful to divorce code from specific memory locations. For example we may have an array of characters representing a persons name and wish to write code that will generate a number from it by ORing each character together. This could be written using array access operators [] or pointers. A pointer is a special variable whose contents is the address of another variable (including objects), and are declared using the operator *.

Given an array of characters in memory, defined by:

char szName [7];

and represented in memory as:

Address   Memory contents   Variable name
0 'F' szName [0]
1 'r' szName [1]
2 'e' szName [2]
3 'd' szName [3]
4 'd' szName [4]
5 'y' szName [5]
6 '\0' szName [6]

 

A pointer to a character can be declared that points to the second element of the array using the following syntax:

char *ptrcChar = &szName [1];

Note the use of the & to obtain the address of the variable szName[1], in this case location 1.

It is easy to modify the variable pointed to by ptrcChar as follows:

*ptrcChar = szName [4];

Note the use of * to de-reference the pointer ptrcChar. This permits access to the memory location pointed to by the pointer (ptrcChar) - in this example the character stored at element four of array szName ('d') is copied to element one, leading to the following memory representation:

Address   Memory contents   Variable name
0 'F' szName [0]
1 'r' szName [1]
2 'e' szName [2]
3 'd' szName [3]
4 'd' szName [4]
5 'y' szName [5]
6 '\0' szName [6]

 

As pointers are just another example of a variable it is possible to carry out arithmetic operations on them. When declaring a pointer the compiler is told what type of data object it points to, in this example a char. When applying arithmetic operators to a pointer the compiler works in units of whatever data type is pointed to. For example, ptrcChar points to a character, incrementing this variable by one results in ptrcChar containing its current value incremented by the size of a char (1 byte) - i.e. it now points to the next byte in memory. Note, there is no guarantee that what actually exists at this new location is valid data - it could be data of another type or part of the program code. The following lines makes ptrcChar point to element two of the array:

char *ptrcChar = &szName [1];

ptrcChar = ptrcChar + 1;

The example manipulation function can be written as:

void main ()
{      
    char szName [7] = "Freddy";       // Assign the string Freddy to the array. This      
                                      // form of assignment is only available when the     
                                      // array is created - it can not be used later.

    char *ptrcChar = &szName [0];     // Declare a pointer to a character and point it to      
                                      // the first element of the array szName     

    int iValue = 0;                   // An integer holding the magic value

    while (*ptrcChar)                 // End the loop when the NULL character is reached      
    {      
        iValue = iValue | *ptrcChar;  // Or current character value with the magic number

        ptrcChar ++;                  // Move on to the next character in the array      
    }  
}

Besides pointing to basic data types, pointers can be declared to point to any user defined type (including classes). The following code makes use of a pointer to an object of type Clocn;

#include "locn.h" // Include file were class prototype is defined

void vManipulatePosition (Clocn *pLocn)  
{   
    pLocn->set (10, 20); // Note use of the -> access operator as we are    
                         // dealing with a class, not a basic data type  
}

void main ()  
{
    Clocn position;                  // Declare an object of type Clocn;

    vManipulatePosition (&position); // Pass address of object to function.  
}

C++ defines a special pointer value called NULL - this evaluates to zero. It is guaranteed to be unequal to a pointer to any valid object, variable, etc. The value of NULL is frequently used to mark the end of a list of pointers in memory, indicate the failure of a function that returns a pointer, etc.

Use memory allocation routines

All the variables met so far have been defined at compilation time, and consequently exist on the function stack. The stack is an area of memory used by functions to store local variables, pass parameter values and store the address to return to once its execution is complete. When a function is called its parameters are pushed onto the top of the stack. As it begins execution the function places all its local variables, etc. onto the top of the stack. When it in turn calls another function the parameters for the new function together with the return address are placed on to the stack prior to the function call being made. The new function begins execution and pushes its own values onto the top of the stack. When it finishes execution the values it added, are popped off the top of the stack. The next function finishes execution and the values it pushed on to the stack are popped off, etc. A good analogy is a stack of plates in a canteen.

Variables declared using the mechanisms met so far are placed on the stack. The following code will result in the variable iAge being placed on the stack.

void main()
{  
    int iAge;  
}

In many machines stack space is limited. A second area of memory, referred to as the heap, is used to store large items of data or those that need to persist between function calls.

To make use of the heap C++ introduces the keywords new and delete. To add an integer to the heap the following code would be used:

void main ()
{
    int *ptriAge;
    iAge = new int;  
}

and to destroy it:

void main ()  
{ 
    int *ptriAge;
    ptriAge = new int;
    delete ptriAge;  
}

The new and delete operators both work with pointers, new passing back a pointer to the desired object within the heap and delete taking a pointer to an object within the heap to destroy.

The new and delete operators are not restricted to working with basic types. They can also handle any user defined types, for example an instance of the Clocn class can be declared as follows:

#include "locn.h" // Include file were class prototype is defined

void main ()
{
    Clocn *ptrPosition;               // Declare a pointer to an object of type Clocn;
    ptrPosition = new Clocn (10, 20); // Declare a new Clocn object with initial values    
                                      // of 10 and 20 on the heap storing its location
                                      // in ptrPosition

    delete ptrPosition; // Destroy the object pointed to by ptrPosition.  
}

If an array of objects is required the [] are employed, as follows:

#include "locn.h" // Include file were class prototype is defined

void main ()
{
    Clocn *ptrPosition;           // Declare a pointer to an object of type Clocn;
    ptrPosition = new Clocn [10]; // Declare ten new Clocn objects on the heap  
                                  // storing the location of the first in  
                                  // ptrPosition.

    delete [] ptrPosition;        // Destroy the ten objects on the heap pointed to  
                                  // by ptrPosition.  
}

Note, when creating arrays of objects on the heap it is imperative when deleting them to include the [], otherwise only the first object pointed to by the variable passed to delete will be destroyed.

Understand storage classes

The data storage class of a variable controls both its lifetime and visibility - i.e. from where in the program it can be seen and at what point in execution it is created and destroyed. Four data storage classes exist for variables that are not members of objects, auto, static, extern and const.

The storage class of a variable is given before its type, for example:

{
    auto int iAge;
    static float fHeight;
    const auto iDept = 10;

    ...

    float fShoeSize; // Note, because the storage type is not specified this variable  
                     // is assumed to be auto.  
}

By default variables are of auto type. This means that the variable has local scope to the current program block - it can not be seen by surrounding code blocks or other functions. When the program finishes executing the current block of code the variable is destroyed.

Variables defined as const can not have their value changed.

An external variable is declared outside a code block, i.e. outside of a function are available to any code within that source file and exist for the lifetime of the program. For example in the following code the variable iLongLife is available within the main function (and any others defined within that file) and exists while the program is executing.

int iLongLife;

void main ()
{ 
    iLongLife = 99;

    cout << iLongLife;  
}

To permit one source file to access an external variable defined by another source the extern keyword is used. For example, two source files first.cpp and second.cpp are written, one declaring an external variable and the other accessing it.

// first.cpp

int iValue; // Declare an external variable

void vModify (int iNewValue)
{
    iValue = iNewValue;  
}

--------------------------------- End of first file ---------------------------------

// second.cpp

extern int iValue; // Access an external variable declared elsewhere

void vRevert (int iOldValue)  
{  
    iValue = iOldValue;  
}

Both external variables and the extern keyword should be avoided if possible - they can lead to unpredictable program behaviour especially in large projects where source files beyond your control may change a variable.

Variables proceeded by the static keyword have what are termed static duration - they are allocated when the program begins and destroyed when the program ends, not when the code block they are defined in is entered and left. In this respect they are similar to external variables, nut unlike them they are only visible within the program block where their declaration is made. As an example in the following code the variable iSomeValue is created when the program starts and destroyed when it ends, not when the function vSomeFunction is entered and left.

void vSomeFunction ()  
{
    static int iSomeValue;
    iSomeValue = 27;  
}

All variables preceded by the static keyword are initialised to zero in the absence of some other specified value.


void vTesting ()
{
    static int iSomeValue; // Has the value of 0
    static int iAnotherValue = 33; // Has the value of 33  
}

External variables are obviously very useful - if they can be controlled. Within a single source files it is easy to police changes to a variables value. An external variable can be restricted to use within a single source file via the static keyword. External variables declared as static can not be accessed within other files via the extern keyword.

Member variables of classes can also use storage class modifiers, as in:

class CVehicle  
{
    public:
    auto char szRegistrationNumber [10];
    static int iVehicleCount;

    CVehicle ()
    {
        iVehicleCount++;  
    }

    ~CVehicle ()
    {
        iVehicleCount--;
    }

    void setRegistrationNumber (char *szRegistrationIn)
    {
        strncpy (szRegistrationNumber, szRegistrationIn);
    }  
}

In the class CVehicle two member variables are defined, one auto and the other static - as usual variables without a class modifier are assumed to be auto.

Each object of a class has its own set of auto variables, but any static variables are shared between all the objects.

The auto member variables of an object are created whenever it is instantiated, and deleted whenever the object is destroyed.

Because static members are not associated with a particular object, but instead are common to all, they are not created when an instance of an object is instantiated. Instead, the programmer must create the static member variables of a class before an object of its type is instantiated - as shown below:

#include "verhicle.h"

int CVehicle::iVehicleCount = 0; // Allocate an area in memory for the static member  
                                 // variable iVehicleCount

void main ()  
{
    CVehicle newVehicle;  
}

Note, because static variables are automatically assigned the value of 0 in the absence of any other value the line allocating memory space for iVehicleCount could be rewritten as:

int CVehicle::iVehicleCount;

The class Vehicle gives an example of a possible use for a static variable. In this example iVehicleCount is incremented by the constructor and decremented by the destructor - it acts as a count of the number of objects of type Vehicle that have been created.

At any point the public variable iVehicleCount of any object may be interrogated to find out how many instances of that class type exist. Because static variables are not tied to an individual object but instead exist independently in memory it is also possible to directly refer to CVehcile::iVehicleCount and obtain the same value. As an example:

#include "vehicle.h"

int CVehicle::iVehicleCount = 0;  

void main ()
{
    CVehicle vehicle1; // Increment CVehcile::iVehicleCount
    cout << vehicle1.iVehicleCount; // Produces the answer 1

    Vehicle vehicle2; // Increment Vehcile::iVehicleCount
    cout << CVehicle::iVehicleCount; // Produces the answer 2  
}

Download