15. Memory Management#

Programmers must develop code to appropriately manage a variety of different resources: memory, files, database queries, etc.

In Python, the interpreter handles much of the memory management for developers automatically. As everything is an object, variables are references to the underlying values and storage locations allocated on the heap. The Python Interpreter can detect when objects can no longer be referenced and release the memory used by those objects.

However, with C++, Programmers must be aware of the different ways variables may be placed into memory based on how and where the variables are declared.

The C++ standard defines these storage durations:

  • static: The storage for the object (aka, variable) is allocated when the program begins and deallocated when the program ends. Only one instance of the object exists. All objects declared at namespace scope (including global namespace) have this storage duration. Corresponds to the two data regions below.

  • automatic: The storage for the object is allocated at the beginning of the enclosing code block and deallocated at the end. All local objects have this storage duration except if a keyword such as static or extern specifies otherwise.

  • dynamic: The storage for the object is allocated and deallocated upon request by using dynamic memory allocation (e.g., new, delete).

  • thread local: Allocated when the thread begins and deallocated when the thread ends

In specific implementation environments, C++ divides a program’s memory into 5 regions:

  1. Text: The text(code) segment contains the executable instructions of a program. Typically, this is placed below the heap and stack memory errors to prevent overflow issues from overwriting the information contained within it. However, the segment may also be set as read-only memory to prevent any type of modification.

  2. Initialized Data: The initialized data segment contains the global and static variables that have been explicitly initialized with the code. Note: some consider this segment and the following to be the same.

  3. Uninitialized Data: This segment contains declared variables from the program source code, but have not been initialized. The operating system (program loader) will initialize data in this segment to zero before the program starts executing. BSS Details

  4. Stack: The stack segment contains the program stack. This structure grows downward in memory as function calls are made. With each function call, the compiler determines how much space is required to hold the variables defined within the function (i.e., those variables with _automatic _storage duration). The stack then grows by that space. If additional space is needed by internal blocks or arrays whose sizes are unknown until execution time (the size of arrays can be determined by variables), the stack frame can be further extended. Once a function exits, the local memory used by that function is no longer needed and is released - the stack shrinks in space. This is why we cannot return a locally declared array - the memory reference that pointed to that array is no longer valid. The command-line arguments are automatically placed into the stack as the main()function is called.

  5. Heap: The heap is where memory is dynamically allocated. With C, programmers can directly manipulate this memory space with functions such as malloc(), realloc(), and free(). In C++, we can create objects in this space with the new keyword. The compiler then handles allocating the space appropriately based upon the type of the object.

As mentioned, every variable in Python is reference and the underlying objects are allocated on the heap.

In C++ we different types of variables (value, reference, and pointer) as well as different locations where those variables may be placed.

15.1. Value Variables#

Value variables are probably the most commonly used variable type in C++. The following code demonstrate creating value variables for both normal types (int, float, char, etc) as well as for objects.

int i = 5;
char c = 'a';
Point p;
Point p2(4,6);

As the C++ compiler knows the exact size of these variables, the compiler can allocate space in the appropriate location. Within Python, the interpreter cannot determine the memory needed for an object due to the dynamic nature (e.g., adding additional properties to an object at runtime) of the language and, hence, must place objects on the heap. Additionally, the Python interpreter must utilize additional data structures to keep track of what’s exactly in an object.

For these variables declared within a function or code block, C++ will allocate memory on the stack. For global variables, the compiler allocates space on the data segment.

As C++ passes variables by value, a copy of the object is made when function calls are made using the class’s copy constructor. By default, C++ provides a copy constructor that makes a shallow copy of the object as any reference or pointers only have their value copied (and not the underlying object(s)/data. The pass by value also means that for larger objects as substantial amount of work must be done to copy those objects.

In Python, the assignment statement just copies the reference value to another variable.

In C++, the assignment operator actually uses the copy constructor to perform the work.

Point a(1,2);
Point b = a;

15.2. Reference Variables#

Reference variables are declared with an & after the type name. The variable provides a new name, but not a new object. The reference variable is an alias to existing object. Once the variable has been assigned, the “alias”/value is fixed. We cannot re-associate with another variable. Minus the inability to reassign, the variable type is most similar to Python’s model. Since the reference is fixed, we cannot assign nullptr or NULL to a reference variable. Additionally, we cannot have references to references, nor can we manipulate references as we can with pointers - “reference arithmetic” does not exist.

In the following code, we create the simple Point class and then demonstrate creating two reference variables c and d to p. Unlike pointers, the references are automatically dereferenced (i.e., we do not to need to explicitly use the * dereferencing operator).

 1//filename: pointer_reference.cpp
 2//complile: g++ -std=c++17 -o pointer_reference pointer_reference.cpp
 3//execute: ./pointer_reference
 4#include <iostream>
 5using namespace std;
 6
 7class Point {
 8private:
 9    double x, y;
10public:
11    Point(double initialX = 0.0, double initialY = 0.0) : x{initialX}, y{initialY} {}
12    double getX() const { return x; }
13    double getY() const { return y; }
14    void setX(double val) { x = val; }
15    void setY(double val) { y = val; }
16};
17
18int main(int argc, char *argv[]) {
19    Point p(5,2);
20    Point& c = p;
21    Point& d(p);
22    c.setX(1);
23
24    cout << "p: " << p.getX() << "," << p.getY() << endl;
25    cout << "c aka p: " << c.getX() << "," << c.getY() << endl;
26    cout << "d aka p: " << d.getX() << "," << d.getY() << endl;
27    
28    // Demonstrates that c and d are references / aliases to the original object
29    cout << "Memory address - p:" << &p << endl;
30    cout << "Memory address - c:" << &c << endl;
31    cout << "Memory address - d:" << &d << endl;
32}

Reference variables do not necessarily make sense for local variables as in the previous example. Confusing to have multiple names for the same object in the same context/scope.

Reference variables are important, though, for functions as the provide pass by reference semantics

The following example demonstrates uses reference variables for function parameters in the swap functions as well as in overriding the + operator when both operands are Point objects. Notice that we can use these reference parameters for both classes as well as the standard built-in data types.

 1//filename: pointer_ref_func.cpp
 2//complile: g++ -std=c++17 -o pointer_ref_func pointer_ref_func.cpp
 3//execute: ./pointer_ref_func
 4#include <iostream>
 5using namespace std;
 6
 7class Point {
 8private:
 9    double x, y;
10public:
11    Point(double initialX = 0.0, double initialY = 0.0) : x{initialX}, y{initialY} {}
12    double getX() const { return x; }
13    double getY() const { return y; }
14    void setX(double val) { x = val; }
15    void setY(double val) { y = val; }
16
17    Point operator+(const Point& other) const { 
18        return Point( x + other.x, y + other.y);
19    }
20    Point& operator+=(const Point& other){ 
21        this->x += other.getX();
22        this->y += other.getY();
23        return *this;
24    }
25};
26
27void swap(int& a, int& b) {
28    int temp = a;
29    a = b;
30    b = temp;
31}
32
33void swap(Point& a, Point&  b) {
34    Point temp = a;
35    a = b;
36    b = temp;
37}
38
39int main(int argc, char *argv[]) {
40    Point a(5,2);
41    Point b(-12, 7);
42
43    a = a + b;  // a = -7,9 now
44
45    swap(a,b);
46
47    cout << "a: " << a.getX() << "," << a.getY() << endl;
48    cout << "b: " << b.getX() << "," << b.getY() << endl;
49
50    int i = 42;
51    int j = 92;
52    swap(i,j);
53    cout << "i: " << i << endl;
54    cout << "j: " << j << endl;
55}

Reference variables as parameters provides two advantages: 1) passing large objects and 2) provide capability to mutate existing objects. We can add the const modify to avoid mutation of the passed object. The + method demonstrates this both in making the “other” point constant as well as the entire function. The const at the end of the method header informs other programmers that we are not mutating the original object, but rather creating a new object as the result. Anywhere you see const at the end of a function, you should read that method as being designated as an accessor in which the object it belongs to will remain unchanged when the method is invoked/called.

In the += method, we can’t make method const as the object itself needs to be mutated and returned.

_Note: _In a later docable page, we will see how to use templates to avoid writing multiple versions of the same function (e.g., swap) that just differ by their type.

15.3. Pointer Variables#

Pointer variables function the same as they do in C. With pointers, the address can be manipulated (pointer arithmetic) and assigned the nullptr value. nullptr is a keyword introduced in C++11 that represents a pointer that does not point to any memory location. Prior to C++, programmers would use the value NULL or 0.

15.4. Comparison between nullptr, NULL, and 0:#

  • nullptr:Type std::nullptr_t with implicit conversions to any pointer type.Cannot be converted to integral types, except for bool.Type-safe and preferred for representing a null pointer in modern C++.

  • NULL:Macro that typically represents the integer zero.May cause issues in function overloading and template specialization due to being an integer type, not a pointer type.

  • 0 (Zero):Integer that can be implicitly converted to any pointer type, representing a null pointer.Like NULL, possible ambiguity in function overloading and template specialization.

15.5. Dynamic Memory Management#

With value variables, the C++ compiler manages memory automatically. With the variable declaration within functions, the compiler automatically allocated space within that function’s stack frame. This allocation is possible due to static typing of C++. When the variable goes out of scope (e.g., the function returns), that stack frame is destroyed and hence any allocated space within that stack is implicitly released. This automatic management eases the burden both upon the programmer and the system itself (less work to manage memory).

However, this use of memory is pre-determined at compile when the programmer originally develops the code. Circumstances might exist when we need to more actively control the allocation of objects and when they are destroyed. For instance, when creating an object as the result of function that is then returned to be used elsewhere in the program or when we do not know the size or number of elements a particular data structure may contain.

In C, programmers directly allocated the needed space with malloc and a computed size. In C++, the dynamic allocation is performed using the keyword new along with the type to be created. As with C, we will use a pointer variable to store the address of the allocated memory.

Point *p;
p = new Point();

In the above code block, the first line allocates space to store the pointer p. Unless p is a global variable, this allocation occurs within the function’s stack frame. In the second line, the compiler creates code to allocate space for a Point object in the heap and then assigns that address to p.

As with any allocated resource (memory, files, network sockets, etc.), we need to use that resource, and, then, once we are finished with that resource, release that resource back to the system (free, close, etc.).

For dynamically allocated objects in C++, programmers use the delete keyword. Only dynamically created objects can be destroyed in this manner.

delete p;

15.6. Dynamic Memory Management for Arrays#

For better or worse, C++ treats built-in arrays about the same as C treats arrays. As with C, if we know the size of the array, we can declare array with that size.

However, if we need to dynamically allocate the array (e.g., so that it can be used as a result of a function, we can use the following pattern:

type *variableName = new type[numberOfElements];

Notice that it was not necessary the compute the memory size - the C++ takes care of this.

When this allocation occurs, the default constructor for the object is utilized.

To free the allocated memory, use

delete[] variableName;
variableName = nullptr;
 1//filename: point_dmm.cpp
 2//complile: g++ -std=c++17 -o point_dmm point_dmm.cpp
 3//execute: ./point_dmm
 4#include <iostream>
 5using namespace std;
 6
 7class Point {
 8private:
 9    double x = 0, y = 0;
10public:
11    Point(double initialX = 0.0, double initialY = 0.0) : x{initialX}, y{initialY} {}
12    double getX() const { return x; }
13    double getY() const { return y; }
14    void setX(double val) { x = val; }
15    void setY(double val) { y = val; }
16};
17
18int main(int argc, char *argv[]) {
19    size_t numPoints = 5;
20    Point *points = new Point[numPoints];
21    
22    for (size_t i; i < numPoints; i++) {
23        cout << i << ": " << points[i].getX() << "," << points[i].getY() << endl;
24    }
25
26    delete[] points;
27    points = nullptr;   // indicates that we can no longer access the array
28
29    return EXIT_SUCCESS;
30}

15.7. Dynamically Allocate Matrix#

Using matrices (two-dimensional arrays) is another common programming task. Matrices often represent spreadsheet-like information. They can also be used to represent positions on a game board such as chess or Battleship. For creating a two-dimensional array, we need to create an array of of pointers to a pointer. We first create the array of pointers for the rows, then for each row allocate another array to represent the columns within that row.

 1//filename: matrix.cpp
 2//complile: g++ -std=c++17 -o matrix matrix.cpp
 3//execute: ./matrix
 4#include <iostream>
 5#include <iomanip>
 6using namespace std;
 7
 8class Point {
 9private:
10    double x = 0.0, y = 0.0;
11public:
12    Point(double initialX = 0.0, double initialY = 0.0) : x{initialX}, y{initialY} {}
13    double getX() const { return x; }
14    double getY() const { return y; }
15    void setX(double val) { x = val; }
16    void setY(double val) { y = val; }
17};
18
19int main(int argc, char *argv[]) {
20    int rows = 3, columns = 5; 
21    
22    Point **matrix = new Point*[rows];  // allocate space for the row pointers
23 
24    for (int i = 0; i < rows; i++) {       // allocate space for the columns in each row
25        matrix[i] = new Point[columns];
26    }
27 
28    //Saves the current state of cout flags (e.g., precision)
29    std::ios cout_state(nullptr);
30    cout_state.copyfmt(std::cout);
31
32    cout << setprecision(2);  // precision remains set until changed
33    cout << fixed;
34    cout << "[";
35    for (int i = 0; i < rows; i++) {
36        if (i > 0) { cout << endl << " "; }
37        for (int j = 0; j < columns; j++) {
38            cout << "(" << setw(6) << matrix[i][j].getX() << ","  
39                        << setw(6) << matrix[i][j].getY() << ")";
40        }
41    }
42    cout << "]" << endl;
43    
44    // restore the state of cout
45    std::cout.copyfmt(cout_state);
46    
47    // free the allocated memory - just reverse the stesp
48    for (int i = 0; i < rows; i++) { // delete inner arrays (data for each row)
49        delete[] matrix[i];
50    }
51    delete[] matrix;
52    return 0;
53}

15.8. Memory Reallocation#

Unlike C, C++ does not have a function to reallocate memory. We’ll need to manually allocate new space, copy over the existing data, and then delete the previously allocated space.

 1//filename: realloc.cpp
 2//complile: g++ -std=c++17 -o realloc realloc.cpp
 3//execute: ./realloc
 4#include <iostream>
 5
 6int* realloc_int_array(int* oldArray, std::size_t oldSize, std::size_t newSize) {
 7    if (newSize == 0) {
 8        delete[] oldArray;
 9        return nullptr;
10    }
11    int* newArray = new int[newSize];
12    if(oldArray != nullptr) {
13        std::size_t copySize = (oldSize < newSize) ? oldSize : newSize;
14        std::copy(oldArray, oldArray + copySize, newArray);
15        delete[] oldArray;
16    }
17    return newArray;
18}
19
20int main() {
21    int* arr = new int[5];
22    for (int i = 0; i < 5; ++i) {
23        arr[i] = i;
24    }
25
26    arr = realloc_int_array(arr, 5, 10);  // change the size to 10.
27
28    // DANGER!!! Accessing the newly allocate memory before assignment 
29    std::cout << arr[9] << " - do not do this!" << std::endl;
30    
31    delete[] arr;
32
33    return 0;
34}

15.9. Additional Notes#

Be cognizant that C++ can create temporary objects through seeming innocuous appearing code. For instance: cout << string1 + string2 << endl; creates a temporary object from the concatenation of the objects string1 and string2. Scott Meyers has the following in his book More Effective C++ in the section “Understand the Origin of Temporary Objects”:

True temporary objects in C++ are invisible - they don't appear in your source code. They arise whenever a non-heap object is created but not named. Such unnamed objects usually arise in one of two situations: when implicit type conversions are applied to make function calls succeed and when functions return objects.

Consider first the case in which temporary objects are created to make function calls succeed. This happens when the type of object passed to a function is not the same as the type of the parameter to which it is being bound.

These conversions occur only when passing objects by value or when passing to a reference-to-const parameter. They do not occur when passing an object to a reference-to-non-const parameter.

The second set of circumstances under which temporary objects are created is when a function returns an object.

Anytime you see a reference-to-const parameter, the possibility exists that a temporary will be created to bind to that parameter. Anytime you see a function returning an object, a temporary will be created (and later destroyed).

C++ will make implicit conversions. With any constructor that takes just one argument, the compiler will look for a way to convert from the argument type to that of the constructor’s parameter. To avoid this, place the keyword explicit in front of the constructor. Generally speaking, any single argument constructors (except the copy constructor) should use explicit.

15.10. Memory States#

The following diagram shows the different states that dynamically-allocated memory can have. Initially, as the memory is unallocated, all that can be done with that space is allocated via new. The memory that is allocated is now available to be written and/or freed. However, it’s invalid to read from the location the contents of the allocated memory could be anything. Once, we then write to the memory location, we can then read those contents.

15.11. Summary#

  • Programmers are responsible for memory.

  • You must allocate the right amount of memory required.

  • You must free any memory that you allocate.

  • Resource leaks occur when you don’t release unneeded resources - eventually programs will stop functioning correctly.