Part B - Foundations

Dynamic Memory

Describe the system memory under the operating system's control
Introduce the syntax for allocating and deallocating dynamic memory
Describe common issues that may arise with dynamic memory

"Avoid allocating and deallocating in different modules" (Sutter, Alexandrescu, 2005)

Memory | Allocation | Deallocation | Issues | Single Instances | Operator Review | Summary | Exercises


The system memory that an application uses may vary with the size of the user's problem.  C++ does not require programmers to specify all of the memory required at compile-time.  By designing an application so that it determines its memory requirements at run-time, we create a more flexible programming solution. 

This chapter describes the syntax for allocating and deallocating memory dynamically at run-time, including the operators for these tasks.  The chapter concludes with a summary of the operators used in these notes and their order of precedence in evaluating compound expressions.


System Memory

When a user initiates an application's execution, the operating system loads the executable code into RAM and transfers control to the entry point of the executable (the main() function).  Throughout execution, the application may request more memory from the operating system.  The system attempts to satisfy all such requests by reserving space in RAM.  Once the application terminates and returns control to the operating system, the system recovers all of the space that it reserved for the application. 

Static Memory

The memory that the operating system reserves for the application at load time is called static memory.  Static memory includes the space allocated for the program instructions, local variables and local objects.  The linker determines the amount of static memory that the application requires at link time. 

arrays in dynamic memory

The space reserved for the local variables and objects is shared amongst them whenever possible.  The executable allocates regions of memory for newly defined variables and objects over the memory that had been allocated for other variables and objects whose lifetime has ended.  The lifetime of each local variable and object extends from its definition to the closing brace of the code block within which it has been defined: 

 // lifetime of a local variable or an object

 for (int i = 0; i < 10; i++) {
     double x = 0;      // lifetime of x starts here
     // ...
 }                      // lifetime of x ends here

 for (int i = 0; i < 10; i++) {
     double y = 4;      // lifetime of y starts here
     // ...
 }                      // lifetime of y ends here

Note that the variable y may occupy the same physical memory location in RAM as variable x.  This system of organizing memory for the local variables and objects ensures that application uses RAM as efficiently as possible.

Dynamic Memory

The additional memory that the operating system reserves for the application during its execution is called dynamic memory

Dynamic memory is completely separate from the static memory that the operating system has reserved for the application at load time.  The operating system reserves dynamic memory at run-time and the application itself allocates and deallocates regions of it. 

To keep track of the dynamic memory currently allocated by the application, we store the address of each region of it in a pointer variable.  We allocate memory for the pointer variable in static memory and keep it alive for as long as we require access to that region of dynamic memory. 

Consider allocating dynamic memory for an array of n elements as shown below.  We store the address of the array in a pointer to it, p, in static memory.  We allocate the array itself dynamically and store the data in its elements sequentially in dynamic memory, starting at address p.

arrays in dynamic memory

Lifetime

The lifetime of any dynamic variable or object ends when the application explicitly deallocates the region of dynamic memory reserved for that variable or object.  If the application does not deallocate the dynamic memory reserved for a variable or object, its lifetime extends to the end of the application. 

<>p> Unlike variables and objects that have been allocated in static memory, those in dynamic memory do not go of out scope at the closing brace of the code block within which they were defined.  That is, we must manage the deallocation of dynamic variable and objects ourselves.


Dynamic Allocation

The keyword new followed by [n] allocates contiguous space outside static memory for an array of n elements and returns the address of the start of that array. 

A dynamic allocation statement takes the form

 pointer = new Type[size];

where Type is the primitive or compound type of the array's elements. 

For example, to allocate dynamic memory for an array of n Students, we write

 int n;                      // holds the number of students
 Student* student = nullptr; // will hold the address of the dynamic array 

 cout << "How many students? ";
 cin >> n;

 student = new Student[n]; // allocates space in dynamic memory

Initialization to nullptr ensures that student is not pointing to any valid address before the operating system allocates the dynamic memory.  Note that the size of the array is a run-time variable and not an integer constant or constant expression as required for a static array. 


Dynamic Deallocation

The keyword delete followed by [] and the address of the region of dynamic memory deallocates memory that has been allocated using new[]

A dynamic array deallocation takes the form

 delete [] pointer;

where pointer holds the address of the start of the dynamically allocated array. 

For example, to deallocate the memory allocated for the array of n Students above, we write

 delete [] student;
 student = nullptr;  // optional

The nullptr assignment ensures that student is no longer pointing to any valid address.  This optional assignment eliminates the possibility of deleting the original address more than once, which is a serious error.  Moreover, deleting the nullptr address has no effect and does not cause an error.

Omitting the brackets in a deallocation expression deallocate the first element of the array and leaves the other elements unreachable. 

Deallocation does not return dynamic memory to the operating system.  Deallocated dynamic memory remains available for subsequent re-allocations.  The operating system only reclaims dynamic memory once the application has terminated and has transferred control back to the system. 

Complete Example

Consider a simple program where the user enters as input the number of Students, the program allocates memory for that number of students, the user enters the data for each student, the program displays the data received and finally the program terminates:

 // Dynamic Memory Allocation
 // dynamic.cpp

 #include <iostream>
 using namespace std;

 struct Student {
     int no;
     char grade[14];
 };

 int main( ) {
     int n;
     Student* student = nullptr;

     cout << "Enter the number of students : ";
     cin >> n;
     student = new Student[n];

     for (int i = 0; i < n; i++) {
         cout << "Student Number: ";
         cin  >> student[i].no;
         cout << "Student Grades: ";
         cin  >> student[i].grade;
     }

     for (int i = 0; i < n; i++)
         cout << student[i].no << ": " << student[i].grade << endl; 

     delete [] student;
     student = nullptr;
 }

Memory Issues

Two important issues arise with dynamic memory allocation and deallocation:

  • memory leaks
  • insufficient memory

Memory Leak

A memory leak occurs when an application loses the address of dynamically allocated memory that has not deallocated.  This occurs if

  • a pointer to dynamic memory goes out of scope before the application has deallocated that memory 
  • a pointer to dynamic memory changes its value before the application has deallocated the memory starting at that value 

Memory leaks are difficult to find because they usually do not halt execution immediately.  We might only become aware of their existenceindirectly through gradually slower execution or incorrect results. 

Insufficient Memory

Many platforms have sufficient hardware and operating system software to support large allocations of dynamic memory.  On those platforms where memory is severly limited, a distinct possibility exists that the operating system might not provide the amount of dynamic memory requested by an application. 

One way to trap execution failures due to insufficient memory is to insert the argument (nothrow) after the new keyword.  new(nothrow) returns the nullptr address if a memory allocation failure occurs.  nothrow is defined in the <new> header file.

For example, we may write

 // Memory Allocation Failure
 // allocationFailure.cpp

 #include <new> // reuired for nothrow
 #include <iostream>
 using namespace std;

 struct Student {
     int no;
     char grade[14];
 };

 int main( ) {
     int n;
     Student* student = nullptr;

     cout << "Enter the number of students : ";
     cin >> n;
     student = new (nothrow) Student[n]; // for nullptr on failure
     if (student == nullptr)
         cout << "Memory Allocation Failed" << endl;
     else {
         for (int i = 0; i < n; i++) {
             cout << "Student Number: ";
             cin  >> student[i].no;
             cout << "Student Grades: ";
             cin  >> student[i].grade;
         }

         for (int i = 0; i < n; i++)
             cout << student[i].no << ": " << student[i].grade << endl; 

         delete [] student;
         student = nullptr;
     }
 }

Single Instances

We can allocate dynamic memory for single instances of a primitive or compound type.  The syntax for allocating and deallocating dynamic memory for single instances is similar to that for allocating and deallocating arrays. 

Allocation

The keyword new without the brackets allocates dynamic memory for a single variable or object of the specified type. 

An allocation statement takes the form

 pointer = new Type;

For example, to store one instance of a Student in dynamic memory, we write

 Student* harry = nullptr;   // a pointer in static memory
 harry = new Student;        // an instance of Student in dynamic memory 

 // we must deallocate harry later

single instance dynamic memory

Deallocation

The keyword delete without the brackets deallocates dynamic memory at the address specified. 

A deallocation statement takes the form

 delete pointer;

delete takes either a pointer that was returned by new.

For example, to deallocate the memory for harry that was allocated dynamically above, we write

 delete harry;
 harry = nullptr;  // good programming style 

Operator Review

The C++ operators that we have covered, including those in the C notes leading up to this set of notes, are listed in the table below.  The order of evaluation of these operators is important in compound expressions.  A compound expression consists of several sub-expressions where different orders of evaluation are possible.  That is, the results of evaluating a compound expression depend on the order in which we evalute its sub-expressions.  To ensure unique results for all compound expressions, the C++ language defines rules of precedence on the operators in any compound expression.  This order is from top to bottom in the table shown below.  The operators associate operands in each sub-expression from left to right, except as noted in the right column.

Operator Associate From
:: left to right
[ ] . -> ++ (postfix) -- (postfix) left to right
 ++ (prefix) -- (prefix) + - & ! (all unary) 
new new[] delete delete[] (type), type()
right to left
.* ->* left to right
* / % left to right
+ - left to right
>> << left to right
< <= > >= left to right
== != left to right
&& left to right
|| left to right
= += -= *= /= %= right to left
?: left to right
, left to right

The scope resolution operator :: has the highest precedence.  Its expression is always evaluated first. 

We can change the order of evaluation within any compound expression by enclosing a sub-expression in parentheses.  That is, we use (sub-expression) to evaluate sub-expression before applying the rules of precedence to the compound expression. 

For example,

   2 + 3   * 5 => 2 + 15 => 17
 ( 2 + 3 ) * 5 => 5 * 5  => 25

Summary

  • the memory available to an application consists of static memory and dynamic memory
  • static memory lasts the lifetime of the application
  • the linker determines the amount of static memory needed at link-time
  • the operating system provides dynamic memory to an application at run-time upon request
  • the keyword new [] allocates a contiguous region of dynamic memory and returns the address of the start that memory
  • we store the address of dynamic memory in static memory
  • delete [] deallocates continguous memory from the specified address
  • allocated space must be deallocated within the lifetime of the pointer that holds that address

Exercises




Previous Reading  Previous: Input and Output Examples Next: Classes   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License