Content
Memory Models and Dynamic Memory Static Local Variables Automatic Storage Duration Address Space The new and delete Operators Memory Leaks Double Frees and Improper Deletes Dangling Pointers Dynamic Arrays <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 03/27/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
An object also has a storage duration that determines its lifetime. In this course, we consider three storage durations:
static: the lifetime is essentially the whole program
automatic (also called local): the lifetime is tied to a particular scope, such as a block of code
dynamic: the object is explicitly created and destroyed by the programmer
The first two durations are controlled by the compiler, and an object with static or automatic scope is associated with a variable.
Objects with dynamic storage are managed by the programmer, and they are not directly associated with variables.
The following variables refer to objects with static storage duration:
global variables
static member variables
static local variables
The lifetime of these objects is essentially the whole program, so they can be used at any point in time in the program.
We have already seen global and static member variables. A static local variable is a local variable declared with the static
keyword. Rather than living in the activation record for a function call, it is located in the same region of memory as other variables with static storage duration, and there is one copy of each static local variable in the entire program. The following is an example:
1 int count_calls() {
2 static int count = 0;
3 return ++count;
4 }
5
6 int main() {
7 cout << count_calls() << endl; // prints 1
8 cout << count_calls() << endl; // prints 2
9 cout << count_calls() << endl; // prints 3
10 cout << count_calls() << endl; // prints 4
11 }
The count
static local variable is initialized to 0, so its value is 0 the first time count_calls()
is called. The call increments count
and returns the new value of 1. The next time count_calls()
is called, count
has value 1.
It gets incremented again, and the call returns 2. And so on. Thus, all calls to count_calls()
use the same count
object, unlike a non-static local variable, where each call would get its own object.
1 void func(int x) {
2 for (int i = 2; i < 10; ++i) {
3 string s = " cats";
4 cout << i << s << endl;
5 } // end scope of i, s
6
7 int y = 10;
8
9 {
10 int z = -3;
11 cout << x << " " << y << " " << z << endl;
12 } // end scope of z
13
14 } // end scope of x, y
The variables fall into the following categories:
The scope of a parameter is the entire function body. Thus, the lifetime of the object associated with x
begins when the function is called and ends when the function returns.
The scope of a variable declared in a loop header is the entire loop. Thus, the lifetime of the object i
begins when the loop starts and ends when the loop exits.
The scope of a variable declared within a block is from its point of declaration to the end of the block. The lifetime of s
begins when its initialization runs and ends at the end of a loop iteration - a new object associated with s
is created in each iteration. The lifetime of y also begins when its initialization runs, and it ends when the function returns. Lastly, the lifetime of z
starts when its declaration is run and ends when the code exits the block containing z
's declaration.
The new
operator creates an object not in some activation record, but in an independent region of memory called the heap.
The stak and heap are two of the segments that make up a program's address space, which is the total memory associated with a running program. The following picture is a typital layout for an address space.
The text segment contains the actual machine instructions representing the program’s code, and it is often placed at the bottom of a program’s address space (Nothing is located at address 0, since that is the representation used for a null pointer).
Space for objects in static storage generally is located above the text segment, followed by the heap. The latter can grow or shrink as objects in dynamic memory are created and destroyed; in most implementations the heap grows upward as needed, into the empty space between the heap and stack.
The stack starts at the top of the address space, and it grows downward when a new activation record is created.
new
and delete
Operators¶The syntax of a new
expression consists of the new
keyword, followed by a type, followed by an optional initialization using parentheses or curly braces. The following are examples:
// default initialization
new int;
// value initialization
new int();
new int{};
// explicit initialization
new int(3);
new int{3};
If no initialization is provided, the newly created object is default initialized. For an atomic type, nothing is done, so the object’s value is whatever is already there in memory. For a class type, the default constructor is called.
Empty parentheses or curly braces result in value initialization. For an atomic type, the object is initialized to a zero value. For a class type, the default constructor is called.
Explicit initialization can be done by supplying values in parentheses or curly braces. For atomic types, the value is used to initialize the object. For C-style ADTs, curly braces must be used, and the values within are used to initialize the member variables. For C++ ADTs, both parentheses and curly braces invoke a constructor with the values within as arguments.
A new
expression does the following:
Allocates space for an object of the given type on the heap.
Initializes the object according to the given initialization expression.
Evaluates to the address of the newly created object.
The address is how we keep track of the new object; it is not associated with a variable name, so we have to store the address somewhere to be able to use the object.
1 int main() {
2 int *ptr = new int(3);
3 cout << *ptr << endl; // prints 3
4 ...
5 }
The newly created object’s lifetime is not tied to a particular scope. Rather, it begins when the new
expression is evaluated and ends when the delete
operator is applied to the object's address.
Here, the operand to delete
evaluates to the address value contained in ptr
. The delete
operator follows this address and destroys the object at that address.
The expression delete ptr;
does not kill the ptr
object – it is a local variable, and its lifetime ends when it goes out of scope. Rather, delete
follows the pointer to the object it is pointing to and kills the latter.
Try it out:
Memory leaks usually happen when part of your code allocates dynamic memory, but neglects to free up the space when it is done. You lose the address of a heap object, meaning it is inevitably leaked. The following is an example of memory leak.
1 void helper() {
2 int *ptr = new int(10);
3 *ptr = new int(20);
4 delete ptr;
5 }
6
7 int main() {
8 for (int i = 0; i < 1000000000; ++i) {
9 helper();
10 }
11 }
The helper()
function leaks memory. It allocates 2 int
s, but only cleans up 1.
When ptr
switches from the 10 to the 20, the 10 is orphaned.
If leaky code is used frequently, your program can run out of memory!
While we have to make sure we clean up all the memory that we create with new
by cleaning it up using delete
, we also have to watch out for a few potential errors:
Deleting an object twice usually results in a crash.
Deleting a non-heap object usually results in a crash.
If a null pointer is given to delete, nothing happens (i.e. it doesn't crash or do anything bad!). This is reasonable, if we consider that the behavior of delete could be specified as "destroy the object (if any) that this pointer points to" and that a null pointer indicates that a pointer "isn't pointing at anything right now".
Deleting an object allows its memory to potentially be reused for other objects.
As soon as an object's lifetime has ended, its data is no longer safe to access.
However, pointers to that dead object may still be hanging around. We call these dangling pointers.
The following code shows a dangling pointer:
1 void example() {
2 int x = 0;
3 int *ptr = new int(42);
4 delete ptr;
5 int *ptr1 = new int(3);
6 cout << *ptr << " "; // oops, we meant to type ptr1
7 delete ptr1;
8 }
On line 6, *ptr
is a dangling pointer because the object it points to was killed by delete
on line 4.
The following code shows another dangling pointer:
1 int * func() {
2 int x = 2;
3 return &x; // bad idea!
4 }
5
6 int main() {
7 int *ptr = func();
8 // ptr ends up pointing at the dead object left after x went out of scope
9 }
Because the function func()
is in another scope, so *ptr
becomes a dangling pointer.
The syntax for creating a dynamic array is the new
keyword, an element type, square brackets containing an integer expression, and an optional initializer list. The expression within the square brackets need not be a compile-time constant:
1 int main() {
2 cout << "How many elements? ";
3 int count;
4 cin >> count;
5 int *arr = new int[count];
6 for (int i = 0; i < count; ++i) {
7 arr[i] = i;
8 }
9 ...
10 }
A new
array expression does the following:
Allocates space for an array of the given number of elements on the heap.
Initializes the array elements according to the given initialization expression. If no initialization is provided, the elements are default initialized.
Evaluates to the address of the first element of the newly created array.
The lifetime of a dynamic array begins when it is created by a new
array expression. It ends when the array-deletion operator delete[]
is applied to the address of the array’s first element:
delete[] arr;
Though the type of arr is int *
, we cannot use the regular delete
operator on it; instead, we must inform the program with delete[]
that we intend to delete a dynamic array, not just a single object on its own.
Using the wrong deletion operator results in undefined behavior. It is also undefined behavior to apply delete[]
to anything other than the address of the first element of a dynamic array.
We cannot delete an individual element of an array. The lifetime of an array’s elements are tied to the lifetime of the array as a whole; they are born when the array as a whole is born and die when the array dies.