Dynamic Memory Allocation in C++

1. Memory Management

1.1 Stack vs. Heap

Analogy: Imagine a restaurant kitchen.
- Stack: Like a small countertop for immediate tasks (chopping veggies). Space is limited and managed automatically.
- Heap: Like a walk-in freezer for bulk storage. You manually track what’s stored and when to discard it.
Question: Which is better for storing a user-uploaded image?
Answer: Heap! Size is unknown at compile time.

1.2 Pointers and dynamically allocated arrays

A dynamically allocated array is an array whose size and memory location are determined during runtime. A programmer can use the new operator to allocate an array and use a pointer to hold the array's memory location:
  1. A pointer is a variable that holds a memory address, rather than holding data like most variables. A pointer is declared by including * before the pointer's name. Ex: double* recordedTimes declares a double pointer named recordedTimes. When a pointer is initialized with the address of a dynamically allocated array, the pointer "points to" the array.
  2. The new operator allocates memory for the given type and returns a pointer to the allocated memory. Ex: new double[3] dynamically allocates a double array with three elements.
Dynamic memory is allocated on the heap.

2. Single Object Allocation

2.1 C Allocation with malloc

/* Typically in header file */
typedef struct { 
    int x;
    int y; 
} Point;

/* Typically in c file */
Point* p = (Point*)malloc(sizeof(Point)); // Allocate enough space for Point
p->x = 10; // Manual initialization required!
p->y = 20;
...
free(p); // Don’t forget to deallocate!

2.2 C++ Allocation with new

// Typically in header file
class Point {
public:
    int x = 0; 
    int y = 0;
};

// Typically in cpp file
// Allocate and initialize
Point* p = new Point; // Constructor initializes x/y to 0
...
// Deallocate
delete p;

3. Array Allocation

3.1 C Arrays

/* Typically in header file */
typedef struct{
    int X;
    int Y;
} Point;

/* Typically in c file */
const int ArraySize = 15;
/* Allocate enough space for Point array */
Point* Ptr = (Point*)malloc(sizeof(Point) * ArraySize);

/* Initialize */
for(int Index = 0; Index < ArraySize; Index++){
    Ptr[Index].X = 0;
    Ptr[Index].Y = 0;
}
...
/* Deallocate */
free(Ptr);

3.2 C++ Arrays with new[]

// Typically in header file
class Point{
    private:
        int X = 0;
        int Y = 0;
    public:
};

//Typically in cpp file
const int ArraySize = 15;
// Allocate and initialize
Point* Ptr = new Point[ArraySize];
...
// Deallocate
delete[] Ptr;

Undefined Behavior Alert:
Using delete instead of delete[] corrupts memory.
Example: delete Ptr; → Only the first element is destroyed!
Exercise:
Which variable in the following program must be deallocated after use?
int main() {
   int numOfSprints;
   double sampleData[3];
   double* actualData;

   cin >> numOfSprints;
   actualData = new int[numOfSprints];
}
    
Answer: actualData. Double pointer actualData points to an array dynamically allocated using the new operator. Thus, the array pointed to by actualData must be deallocated. As a rule of thumb, every array allocated using new should be deallocated using delete[].

Example of dynamically allocated arrays

An athlete goes through 3 minutes of training. The athlete's body temperature before training is 96.9. Subsequent body temperatures after each additional minute are: 97.0, 97.2, and 97.5. So the input is 3 96.9 97.0 97.2 97.5. The program uses a dynamically allocated array to hold the body temperatures for further analysis. Flexibility on data size enables the program to adapt from small-scale personal applications like a smartwatch health-app, to large-scale use like research.
#include <iostream>
using namespace std;

int main() {
   int numMinutes;
   double* bodyTemperatures;
   int i;
   
   cin >> numMinutes;
   
   bodyTemperatures = new double[numMinutes + 1];
   
   for (i = 0; i <= numMinutes; ++i) {
      cin >> bodyTemperatures[i];
   }
   
   cout << "Initial body temperature: " << bodyTemperatures[0] << "F" << endl;
   
   for (i = 1; i <= numMinutes; ++i) {
      cout << "Body temperature after " << i << " minute(s) of exercise: " << bodyTemperatures[i] << "F ";
      cout << "Change: " << bodyTemperatures[i] - bodyTemperatures[i - 1] << "F" << endl;
   }
   
   delete[] bodyTemperatures;

   return 0;
}
Exercise:
What may happen if the program does not deallocate the dynamically allocated memory when the memory is no longer needed?
Answer: The program may crash.

nullptr

When a pointer is declared, the pointer variable holds an unknown address until the pointer is initialized. A programmer may wish to indicate that a pointer points to "nothing" by initializing a pointer to null. Null means "nothing". A pointer that is assigned with the keyword nullptr is said to be null.

For functions with pointer parameters, a good practice is to check that a pointer is not null before accessing the memory pointed to by the pointer. In the following example, the PrintArray() function ensures that arrayPtr is not null before accessing elements in the array.

void PrintArray(int* arrayPtr, int arraySize) {
int i;

if (arrayPtr != nullptr) {
  for (i = 0; i < arraySize - 1; ++i) {
     cout << arrayPtr[i] << ", ";
  }
  cout << arrayPtr[arraySize - 1] << endl;
}
}

int main() {
int* heartBeats = nullptr;
int numHeartBeats;

...

PrintArray(heartBeats, numHeartBeats);

...

delete[] heartBeats;

return 0;

4. Templates and Standard Containers

4.1 Introduction to Templates

4.2 Common Containers

  1. std::vector<T> – Similar to a dynamic array
  2. std::tuple<T> – Used for creating a tuple
  3. std::set<T> – Used for a set
  4. std::map<K,V> – Similar to a python dictionary, but a ordered
  5. std::unordered_map<K,V> – Similar to a python dictionary, usually implemented as hash table
// Dynamic array (like ArrayList in Java)
std::vector<std::string> names;
names.push_back("Alice");
names.push_back("Bob");

// Key-value store (ordered by keys)
std::map<std::string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;

// Fast lookup (hash table)
std::unordered_map<std::string, int> wordCount;
wordCount["hello"]++;
Analogy: Containers are like kitchen tools.
- Vector: A expandable mixing bowl.
- Map: A labeled spice rack (sorted).
- Unordered Map: A junk drawer (fast but messy).

4.3 Why Use Containers?

5. Smart Pointers

Memory Locations

Common Memory Issues

Memory Leak

A memory leak occurs when memory is allocated on the heap but not freed before the last reference to it is lost. Over time, this can lead to excessive memory usage and performance degradation.

Dangling Reference

A dangling reference occurs when a reference to memory is invalid because the memory has already been freed. This can lead to program crashes or undefined behavior if the system has repurposed the memory.

Exercise:
Would this result in a memory leak?

int main() {
   MyClass* ptrOne = new MyClass;
   MyClass* ptrTwo = new MyClass;

   ptrOne = ptrTwo;
   return 0;
}
    
Answer: Yes, ptrOne no longer points to the initial MyClass object, thus the object is inaccessible, causing a memory leak.
Exercise:
Would this result in a memory leak?

class MyClass {
   public:
      MyClass() {
         subObject = new int;
         *subObject = 0;
      }

      ~MyClass() {
         delete subObject;
      }

   private:
      int* subObject;
};

int main() {
   MyClass* ptrOne = new MyClass;
   MyClass* ptrTwo = new MyClass;
   ...

   delete ptrOne;
   ptrOne = ptrTwo;
   return 0;
}
    
Answer: No. delete ptrOne causes the MyClass destructor to delete the MyClass sub-object, so memory for the object and sub-object are both freed before ptrOne is reassigned.

5.1 Smart Pointers

A smart pointer is a class that wraps around a pointer to an object to simplify the memory management of the object. unique_ptr is a smart pointer that permits only one owner over an object. When a unique_ptr goes out of scope, the object owned by the unique_ptr is automatically deleted. To use the unique_ptr class, the programmer must include the statement #include <memory> for C++17 and later versions.
#include <memory>
class Sleep{
    public:
        Sleep();
        ~Sleep();
        void Set(int hoursVal, int minutesVal);
        void Print();
    private:
        int hours;
        int minutes;
};
...
void RunSleep(int hoursVal, int minutesVal) {
    unique_ptr<Sleep> sleepRecord(new Sleep());
    sleepRecord->Print();
    ...
}
  1. RunSleep()'s variable sleepRecord is a unique_ptr of type Sleep. sleepRecord has an internal Sleep pointer.
  2. When sleepRecord is initialized with a dynamically allocated Sleep object, sleepRecord's internal pointer is assigned with the address of the Sleep object. sleepRecord now owns the Sleep object.
  3. The Sleep object's member functions are accessed through sleepRecord using the -> operator. sleepRecord->Print() calls the Sleep object's Print() function.
  4. When RunSleep() ends, sleepRecord goes out of scope, and sleepRecord's destructor is called. The unique_ptr's destructor internally deletes the Sleep object pointed to by sleepRecord.

Unique Pointer (unique_ptr)

A unique_ptr is a smart pointer that has sole ownership of the dynamically allocated object. It ensures that the memory is freed when the unique_ptr goes out of scope.

Key Features

Using make_unique (C++14)

The preferred way to create an object managed by a unique_ptr is to use std::make_unique. This prevents issues like manual memory allocation errors and provides better performance.

Example Code

    
void foo(){
    std::unique_ptr<int> APtr = std::make_unique<int>(3);
    std::unique_ptr<int> BPtr;

    // The following line prints out 3
    std::cout << (*APtr) << std::endl;
    // The following line hands over ownership to BPtr
    BPtr = std::move(APtr);
}
    
    

Practice Problem

What's wrong with this code?


std::unique_ptr<int> a = std::make_unique<int>(5);
std::unique_ptr<int> b = a; 
        
Answer

Unique pointers cannot be copied! Use std::move() to transfer ownership:

b = std::move(a);

Shared Pointer (shared_ptr)

A shared_ptr allows multiple references to a dynamically allocated object. It uses reference counting to keep track of ownership, and when the reference count reaches zero, the memory is automatically freed.

Key Features

Using make_shared

The preferred way to create an object managed by a shared_ptr is to use std::make_shared. This reduces overhead by allocating control block and object in a single operation, compared to std::shared_ptr(new T(...)).

Example Code

    
void foo(){
    std::shared_ptr<int> APtr = std::make_shared<int>(3);
    std::shared_ptr<int> BPtr;

    // The following line prints out 3
    std::cout << (*APtr) << std::endl;
    // BPtr has a copy of the reference
    BPtr = APtr;
    // At end of foo the int will be freed because ref 
    // count goes to zero
}
    
    

Exercise: Reference Tracking

What is the reference count to the float of 3.14f at each point?


auto a = std::make_shared<float>(3.14f);  // Count: 1
{
    auto b = a;         // Count: ?
    auto c = b;         // Count: ?
}                       // Count: ?
        
Answer

Counts: 1 → 2 → 3 → 1

Weak Pointer (weak_ptr)

A weak_ptr is similar to a shared_ptr, but it does not contribute to the reference count. It is primarily used to prevent cyclic dependencies.

Key Features

Using lock()

The lock() function converts a weak_ptr to a shared_ptr. If the object is already freed, it returns a nullptr.

Example Code

    
std::weak_ptr<int> WPtr;
void foo(){
    // Has to be copied into a shared_ptr before usage
    if(auto SPt = WPtr.lock()){ 
        // Access to pointer through SPt
    }
    else{
        // Failed to gain access
    }
}

{
    auto SPtr = std::make_shared<int>(42);
    WPtr = SPtr;
    foo();
}
foo(); // WPtr will not be able to access through lock
    
    

Exercise:

What is wrong with the following code?


#include <iostream>
#include <memory>

struct Child; 

struct Parent {
    std::shared_ptr<Child> child; 
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

struct Child {
    std::shared_ptr<Parent> parent; 
    ~Child() { std::cout << "Child destroyed\n"; }
};

int main() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();

    p->child = c;
    c->parent = p; 

}

        
Answer

p and c share ownership of each other.

When main() ends, the reference count of both objects never reaches 0, which prevents the objects from being deallocated, resulting in memory leaks.

Shared Pointer from this Pointer – enable_shared_from_this

Don’t create a new shared_ptr from the this raw pointer, it will create a new reference count, need to use shared_from_this().

enable_shared_from_this – Template class that will allow creating a shared_ptr from this raw pointer

shared_from_this() – Returns a shared_ptr to the object that is calling the function

Key Features

Example Code

    
class C1 : public std::enable_shared_from_this<C1> {
    private:
        int Val;
    public:
        int foo();
        int bar(std::shared_ptr<C1> ptr);
};

int C1::foo(){
    return Val * bar(shared_from_this());
}

int C1::bar(std::shared_ptr<C1> ptr){
    return ptr->Val + 3;
}
    
    

PIMPL

PIMPL – Pointer to IMPLimentation idiom, hides the private implementation of the class by only showing a pointer and forward declaration of implementation class (or struct).


// Typically in header file
class ClassName{
    private:
        struct Implementation;   // Forward declaration
        std::unique PtrImpl;
    public:
        ClassName();
        ~ClassName();
        void foo(const std::string &str);
};

// Typcially in cpp file
struct ClassName::Implementation{
    std::string Name;
};

ClassName::ClassName(){
    PtrImpl = std::make_unique();
}

ClassName::~ClassName(){

}

void ClassName::foo(const std::string &str){
    PtrImpl->Name = str;
}
    

Comparison Table

Pointer TypeFeaturesWhen to use
unique_ptrA unique_ptr only allows an exclusive ownership of the object. The object's ownership can be transferred to a different unique_ptr, but not shared. When a unique_ptr goes out of scope, the object owned by the unique_ptr is deleted.As an efficient replacement of raw pointers
shared_ptrA shared_ptr permits shared ownership of an object. When the last owner of the object goes out of scope, the object is deleted. Internally, a counter, called the reference count, keeps track of the number of owners sharing an object.When a dynamically allocated object is shared by multiple pointers
weak_ptrA weak_ptr allows access to, but not ownership of, an object that is owned by a shared_ptr.To interact with a dynamically allocated object whose memory is managed elsewhere

5.2 Avoid Raw Pointers

Common Pitfalls:
  1. Memory leaks (forgetting delete).
  2. Dangling pointers (using a pointer after delete).
  3. Double-free (calling delete twice).

6. Summary

Exercises

Put your solutions on here.
  1. Given Student.cpp and Student.h, complete the program that creates three Student objects dynamically with the name and GPA values from user input. The program then calculates the average GPA of the three Student objects and outputs the results. Ex: If the input is:
    Louie 2.8
    Fila 3.6
    Sam 3.3
    
    the output is:
    Students:
    Louie (2.8)
    Fila (3.6)
    Sam (3.3)
    
    Average GPA: 3.233
    
    Complete the following code:
    #include <iostream>
    #include <iomanip>
    #include "Student.h"
    using namespace std;
    
    int main() {
       string name1, name2, name3;
       double gpa1, gpa2, gpa3, aveGpa;
       Student *stdn1, *stdn2, *stdn3;
       
       // Read input
       cin >> name1;
       cin >> gpa1;
       cin >> name2;
       cin >> gpa2;
       cin >> name3;
       cin >> gpa3;
       
       // TODO: Dynamically allocate Student objects
       
       
       // TODO: Calculate average GPA
       
       
       // Output results
       cout << "Students:" << endl;
       stdn1->PrintInfo();
       stdn2->PrintInfo();
       stdn3->PrintInfo();
       cout << endl;
       cout << fixed << setprecision(3); 
       cout << "Average GPA: " << aveGpa << endl;
       
       // Deallocate memory
       delete stdn1;
       delete stdn2;
       delete stdn3;
    
        return 0;
    }
    
  2. Given the main program in main.cpp and Student class (in Student.h and Student.cpp), complete function AddStudent() that takes a roster (Student*), the roster's size (int), a new student's name (String), and the new student's GPA (double) as parameters and adds the new student to the end of the roster. AddStudent() then returns the pointer to the updated roster. Hint: A new roster must be allocated dynamically to accommodate the new student, and the original roster must be deleted when no longer needed. The main program initializes a roster with four students, adds three more students to the roster, and outputs the updated roster. Ex: If the input of the program is:
    Ryley 3.84
    Jane 3.99
    Mcauley 3.67
    
    the output is:
    Ryley added:
    Louie (2.80)
    Fila (3.60)
    Sam (3.30)
    Alex (1.00)
    Ryley (3.84)
    
    Jane added:
    Louie (2.80)
    Fila (3.60)
    Sam (3.30)
    Alex (1.00)
    Ryley (3.84)
    Jane (3.99)
    
    Mcauley added:
    Louie (2.80)
    Fila (3.60)
    Sam (3.30)
    Alex (1.00)
    Ryley (3.84)
    Jane (3.99)
    Mcauley (3.67)
    
    Complete the following code:
    #include <iostream>
    #include <iomanip>
    #include "Student.h"
    using namespace std;
    
    // Add a new student to an existing roster
    Student* AddStudent(Student* sdList, int listSize, string sdName, double sdGpa) {
        
       //TODO: Type your code here.
    
       return sdList;
    }
    
    // Output student info
    void PrintRoster(Student* sdList, int listSize) {
       for(int i = 0; i < listSize; i++) {
          sdList[i].PrintInfo();
       }
       cout << endl;
    }
    
    int main() {
       string name1, name2, name3;
       double gpa1, gpa2, gpa3;
       Student* roster;
       int classSize = 4;
       
       roster = new Student[classSize];
       roster[0] = Student("Louie", 2.8);
       roster[1] = Student("Fila", 3.6);
       roster[2] = Student("Sam", 3.3);
       roster[3] = Student();  // Using default name and gpa
       
       // Read input
       cin >> name1;
       cin >> gpa1;
       cin >> name2;
       cin >> gpa2;
       cin >> name3;
       cin >> gpa3;
       
       cout << fixed << setprecision(2); 
       // Add first student
       roster = AddStudent(roster, classSize, name1, gpa1);
       ++classSize;
       
       cout << name1 << " added:" << endl;
       PrintRoster(roster, classSize);
       
       // Add second student
       roster = AddStudent(roster, classSize, name2, gpa2);
       ++classSize;
       
       cout << name2 << " added:" << endl;
       PrintRoster(roster, classSize);
       
       // Add third student
       roster = AddStudent(roster, classSize, name3, gpa3);
       ++classSize;
       
       cout << name3 << " added:" << endl;
       PrintRoster(roster, classSize);
       
       delete[] roster;
       return 0;
    }
    
  3. Mad Libs are activities that have a person provide various words, which are then used to complete a short story in unexpected (and hopefully funny) ways.

    Use the pointers declared to complete a program that outputs a short story based on four input values from a user (one integer and three strings). Because pointers are used, memory must be allocated before the input values are stored. Observe the output of the original cout statements (the input values are not correctly printed, why?) Fix the cout statements so that the correct values are printed.

    Ex: If the input is:

    
    5 Taylor tigers mall
    
    the output is:
    
    Taylor saw 5 different colors of tigers at the mall.
    
    Fill in the code below:
    
    #include <iostream>
    #include <string>
    using namespace std;
    
    int main() {
       int* wholeNumber;
       string* firstName;
       string* pluralAnimal;
       string* genericPlace;
    
       wholeNumber = new int;
       // TODO: Allocate memory for the other pointers
       
       
       // TODO: Store the input values in the allocated memory
       
       
       // FIXME: Output the correct values
       cout << firstName << " saw " << wholeNumber << " different colors of " << pluralAnimal;
       cout << " at the " << genericPlace << "." << endl;
       
       // Deallocate memory
       delete firstName;
       delete wholeNumber;
       delete pluralAnimal;
       delete genericPlace;
       
       return 0;
    }
    
  4. Now convert your above program to a version using smart pointers.