Dynamic Memory Allocation in C++

C++C++Beginner
Practice Now

Introduction

In this lab, you will learn how to handle dynamic memory allocation in C++. You will start by allocating memory using the new operator, then release the memory with the delete operator. You will also explore creating dynamic arrays, using smart pointers like shared_ptr and unique_ptr, and checking for memory leaks. These skills are essential for effective memory management in C++ programming.

The lab covers the following key steps: allocating memory using the new operator, releasing memory with the delete operator, creating dynamic arrays with new[], deleting arrays using delete[], implementing the shared_ptr smart pointer, using unique_ptr for exclusive ownership, checking for memory leaks, and handling memory allocation failures.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL cpp(("C++")) -.-> cpp/BasicsGroup(["Basics"]) cpp(("C++")) -.-> cpp/OOPGroup(["OOP"]) cpp(("C++")) -.-> cpp/AdvancedConceptsGroup(["Advanced Concepts"]) cpp/BasicsGroup -.-> cpp/arrays("Arrays") cpp/OOPGroup -.-> cpp/classes_objects("Classes/Objects") cpp/OOPGroup -.-> cpp/constructors("Constructors") cpp/AdvancedConceptsGroup -.-> cpp/pointers("Pointers") cpp/AdvancedConceptsGroup -.-> cpp/exceptions("Exceptions") subgraph Lab Skills cpp/arrays -.-> lab-446081{{"Dynamic Memory Allocation in C++"}} cpp/classes_objects -.-> lab-446081{{"Dynamic Memory Allocation in C++"}} cpp/constructors -.-> lab-446081{{"Dynamic Memory Allocation in C++"}} cpp/pointers -.-> lab-446081{{"Dynamic Memory Allocation in C++"}} cpp/exceptions -.-> lab-446081{{"Dynamic Memory Allocation in C++"}} end

Allocate Memory Using new Operator

In this step, you'll learn how to use the new operator in C++ to dynamically allocate memory at runtime. Dynamic memory allocation allows you to create variables and arrays whose size is determined during program execution, providing more flexibility compared to static memory allocation.

Open the WebIDE and create a new file called dynamic_memory.cpp in the ~/project directory:

touch ~/project/dynamic_memory.cpp

Add the following code to dynamic_memory.cpp:

#include <iostream>

int main() {
    // Dynamically allocate an integer using new
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value of the dynamically allocated integer
    std::cout << "Dynamically allocated integer value: " << *dynamicInteger << std::endl;

    // Print the memory address of the dynamically allocated integer
    std::cout << "Memory address: " << dynamicInteger << std::endl;

    return 0;
}

Let's break down the key concepts:

  1. int* dynamicInteger = new int;:

    • new operator allocates memory for an integer
    • Returns a pointer to the allocated memory
    • Creates the variable on the heap, not the stack
  2. *dynamicInteger = 42;:

    • Uses the dereference operator * to assign a value
    • Stores the value 42 in the dynamically allocated memory

Compile and run the program:

g++ dynamic_memory.cpp -o dynamic_memory
./dynamic_memory

Example output:

Dynamically allocated integer value: 42
Memory address: 0x55f4e8a042a0

Key points about new operator:

  • Allocates memory dynamically during runtime
  • Returns a pointer to the allocated memory
  • Memory is allocated on the heap
  • Size can be determined at runtime
  • Allows for more flexible memory management

Note: In the next steps, you'll learn how to properly release this dynamically allocated memory to prevent memory leaks.

Release Memory with delete Operator

In this step, you'll learn how to use the delete operator to properly release dynamically allocated memory in C++. Failing to release memory can lead to memory leaks, which can cause performance issues in your program.

Open the WebIDE and create a new file called memory_release.cpp in the ~/project directory:

touch ~/project/memory_release.cpp

Add the following code to memory_release.cpp:

#include <iostream>

int main() {
    // Dynamically allocate an integer
    int* dynamicInteger = new int;

    // Assign a value to the dynamically allocated integer
    *dynamicInteger = 42;

    // Print the value before releasing memory
    std::cout << "Value before release: " << *dynamicInteger << std::endl;

    // Release the dynamically allocated memory using delete
    delete dynamicInteger;

    // Set the pointer to nullptr after deletion
    dynamicInteger = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicInteger == nullptr) {
        std::cout << "Memory has been successfully released" << std::endl;
    }

    return 0;
}

Key points about the delete operator:

  1. delete dynamicInteger;:

    • Frees the memory previously allocated with new
    • Prevents memory leaks
    • Releases the memory back to the system
  2. dynamicInteger = nullptr;:

    • Sets the pointer to nullptr after deletion
    • Helps prevent accidental use of freed memory
    • Provides an additional safety check

Compile and run the program:

g++ memory_release.cpp -o memory_release
./memory_release

Example output:

Value before release: 42
Memory has been successfully released

Important memory management rules:

  • Always use delete for memory allocated with new
  • Set pointers to nullptr after deletion
  • Never use a pointer after it has been deleted
  • Each new should have a corresponding delete

Create Dynamic Arrays with new[]

In this step, you'll learn how to create dynamic arrays using the new[] operator in C++. Dynamic arrays allow you to allocate memory for multiple elements at runtime, providing more flexibility than static arrays.

Open the WebIDE and create a new file called dynamic_array.cpp in the ~/project directory:

touch ~/project/dynamic_array.cpp

Add the following code to dynamic_array.cpp:

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements
    std::cout << "Dynamic Array Contents:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    return 0;
}

Key points about dynamic arrays:

  1. int* dynamicArray = new int[arraySize];:

    • Allocates memory for an array of integers
    • arraySize determines the number of elements
    • Memory is allocated on the heap
    • Size can be determined at runtime
  2. Array initialization and access:

    • Use standard array indexing dynamicArray[i]
    • Can be manipulated like a regular array
    • Allows dynamic sizing based on runtime conditions

Compile and run the program:

g++ dynamic_array.cpp -o dynamic_array
./dynamic_array

Example output:

Dynamic Array Contents:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40

Important considerations:

  • Dynamic arrays provide flexibility in memory allocation
  • Size can be determined at runtime
  • Remember to use new[] for allocation and delete[] for deallocation
  • Always match the allocation and deallocation methods

Delete Arrays Using delete[]

In this step, you'll learn how to properly release memory allocated for dynamic arrays using the delete[] operator. Proper memory management is crucial to prevent memory leaks and ensure efficient resource utilization.

Open the WebIDE and create a new file called delete_array.cpp in the ~/project directory:

touch ~/project/delete_array.cpp

Add the following code to delete_array.cpp:

#include <iostream>

int main() {
    // Declare the size of the dynamic array
    int arraySize = 5;

    // Create a dynamic integer array using new[]
    int* dynamicArray = new int[arraySize];

    // Initialize array elements
    for (int i = 0; i < arraySize; ++i) {
        dynamicArray[i] = i * 10;
    }

    // Print array elements before deletion
    std::cout << "Array Contents Before Deletion:" << std::endl;
    for (int i = 0; i < arraySize; ++i) {
        std::cout << "Element " << i << ": " << dynamicArray[i] << std::endl;
    }

    // Release the dynamically allocated array using delete[]
    delete[] dynamicArray;

    // Set the pointer to nullptr after deletion
    dynamicArray = nullptr;

    // Demonstrate safe pointer usage
    if (dynamicArray == nullptr) {
        std::cout << "Dynamic array has been successfully deleted" << std::endl;
    }

    return 0;
}

Key points about deleting dynamic arrays:

  1. delete[] dynamicArray;:

    • Properly releases memory for entire array
    • Must use delete[] for arrays allocated with new[]
    • Prevents memory leaks
    • Calls destructor for each array element
  2. dynamicArray = nullptr;:

    • Sets pointer to nullptr after deletion
    • Prevents accidental access to freed memory
    • Provides an additional safety check

Compile and run the program:

g++ delete_array.cpp -o delete_array
./delete_array

Example output:

Array Contents Before Deletion:
Element 0: 0
Element 1: 10
Element 2: 20
Element 3: 30
Element 4: 40
Dynamic array has been successfully deleted

Important memory management rules:

  • Always use delete[] for arrays allocated with new[]
  • Never mix delete and delete[]
  • Set pointers to nullptr after deletion
  • Each new[] should have a corresponding delete[]

Implement Smart Pointer shared_ptr

In this step, you'll learn about shared_ptr, a smart pointer from the C++ Standard Library that provides automatic memory management and reference counting. Smart pointers help prevent memory leaks and simplify memory management.

Open the WebIDE and create a new file called shared_pointer.cpp in the ~/project directory:

touch ~/project/shared_pointer.cpp

Add the following code to shared_pointer.cpp:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "Constructor called. Value: " << data << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor called. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

int main() {
    // Create a shared_ptr to MyClass
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);

    // Create another shared_ptr pointing to the same object
    std::shared_ptr<MyClass> ptr2 = ptr1;

    // Print reference count
    std::cout << "Reference Count: " << ptr1.use_count() << std::endl;

    // Access object through shared_ptr
    std::cout << "Data from ptr1: " << ptr1->getData() << std::endl;
    std::cout << "Data from ptr2: " << ptr2->getData() << std::endl;

    // Pointers will automatically free memory when they go out of scope
    return 0;
}

Key points about shared_ptr:

  1. std::make_shared<MyClass>(42):

    • Creates a shared pointer with dynamic memory allocation
    • Initializes object with constructor argument
    • More efficient than separate new and shared_ptr creation
  2. ptr1.use_count():

    • Returns number of shared_ptr instances pointing to the same object
    • Helps track object's reference count
  3. Automatic memory management:

    • Memory is automatically freed when no shared_ptr references the object
    • Prevents manual memory management errors

Compile and run the program:

g++ -std=c++11 shared_pointer.cpp -o shared_pointer
./shared_pointer

Example output:

Constructor called. Value: 42
Reference Count: 2
Data from ptr1: 42
Data from ptr2: 42
Destructor called. Value: 42

Important shared_ptr characteristics:

  • Automatic memory management
  • Reference counting
  • Prevents memory leaks
  • Thread-safe reference counting

Use unique_ptr for Exclusive Ownership

In this step, you'll learn about unique_ptr, a smart pointer that provides exclusive ownership of dynamically allocated resources. Unlike shared_ptr, unique_ptr ensures that only one pointer can own the resource at a time.

Open the WebIDE and create a new file called unique_pointer.cpp in the ~/project directory:

touch ~/project/unique_pointer.cpp

Add the following code to unique_pointer.cpp:

#include <iostream>
#include <memory>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created. Value: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed. Value: " << data << std::endl;
    }

    int getData() const { return data; }

private:
    int data;
};

void processResource(std::unique_ptr<Resource> resource) {
    std::cout << "Processing resource with value: " << resource->getData() << std::endl;
    // Resource will be automatically deleted when function ends
}

int main() {
    // Create a unique_ptr
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>(42);

    // Print resource data
    std::cout << "Resource value: " << ptr1->getData() << std::endl;

    // Transfer ownership using std::move
    std::unique_ptr<Resource> ptr2 = std::move(ptr1);

    // ptr1 is now nullptr
    if (ptr1 == nullptr) {
        std::cout << "ptr1 is now null after moving ownership" << std::endl;
    }

    // Pass unique_ptr to a function (ownership is transferred)
    processResource(std::move(ptr2));

    return 0;
}

Key points about unique_ptr:

  1. std::make_unique<Resource>(42):

    • Creates a unique pointer with dynamic memory allocation
    • Ensures exclusive ownership of the resource
  2. std::move(ptr1):

    • Transfers ownership of the resource
    • Original pointer becomes nullptr
    • Prevents multiple pointers from owning the same resource
  3. Automatic memory management:

    • Resource is automatically deleted when unique_ptr goes out of scope
    • No manual memory management required

Compile and run the program:

g++ -std=c++14 unique_pointer.cpp -o unique_pointer
./unique_pointer

Example output:

Resource created. Value: 42
Resource value: 42
ptr1 is now null after moving ownership
Processing resource with value: 42
Resource destroyed. Value: 42

Important unique_ptr characteristics:

  • Exclusive ownership
  • Automatic memory management
  • Cannot be copied, only moved
  • Prevents resource leaks

Check for Memory Leaks

In this step, you'll learn how to detect and prevent memory leaks in C++ using Valgrind, a powerful memory debugging tool. Memory leaks occur when dynamically allocated memory is not properly freed, causing your program to consume increasing amounts of memory.

First, install Valgrind in the terminal:

sudo apt update
sudo apt install -y valgrind

Create a memory leak example in the WebIDE by creating memory_leak.cpp in the ~/project directory:

touch ~/project/memory_leak.cpp

Add the following code to memory_leak.cpp:

#include <iostream>

class Resource {
public:
    Resource(int value) : data(value) {
        std::cout << "Resource created: " << data << std::endl;
    }

    ~Resource() {
        std::cout << "Resource destroyed: " << data << std::endl;
    }

private:
    int data;
};

void createMemoryLeak() {
    // This creates a memory leak because the resource is not deleted
    Resource* leak = new Resource(42);
    // Missing: delete leak;
}

int main() {
    // Simulate multiple memory leaks
    for (int i = 0; i < 3; ++i) {
        createMemoryLeak();
    }

    return 0;
}

Compile the program with debugging symbols:

g++ -g memory_leak.cpp -o memory_leak

Run Valgrind to detect memory leaks:

valgrind --leak-check=full ./memory_leak

Example Valgrind output:

==1988== Memcheck, a memory error detector
==1988== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1988== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==1988== Command: ./memory_leak
==1988==
Resource created: 42
Resource created: 42
Resource created: 42
==1988==
==1988== HEAP SUMMARY:
==1988==     in use at exit: 12 bytes in 3 blocks
==1988==   total heap usage: 5 allocs, 2 frees, 73,740 bytes allocated
==1988==
==1988== 12 bytes in 3 blocks are definitely lost in loss record 1 of 1
==1988==    at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1988==    by 0x109241: createMemoryLeak() (memory_leak.cpp:19)
==1988==    by 0x109299: main (memory_leak.cpp:26)
==1988==
==1988== LEAK SUMMARY:
==1988==    definitely lost: 12 bytes in 3 blocks
==1988==    indirectly lost: 0 bytes in 0 blocks
==1988==      possibly lost: 0 bytes in 0 blocks
==1988==    still reachable: 0 bytes in 0 blocks
==1988==         suppressed: 0 bytes in 0 blocks
==1988==
==1988== For lists of detected and suppressed errors, rerun with: -s
==1988== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

As you can see, Valgrind detected a memory leak of 12 bytes in 3 blocks. The Resource object created in createMemoryLeak() was not deleted, causing the memory leak.

To fix the memory leak, modify the code to properly delete resources:

void createMemoryLeak() {
    // Properly delete the dynamically allocated resource
    Resource* leak = new Resource(42);
    delete leak;  // Add this line to prevent memory leak
}

Compile and run the program again with Valgrind to verify that the memory leak is fixed:

g++ -g memory_leak.cpp -o memory_leak
valgrind --leak-check=full ./memory_leak
==2347== Memcheck, a memory error detector
==2347== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2347== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2347== Command: ./memory_leak
==2347==
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
Resource created: 42
Resource destroyed: 42
==2347==
==2347== HEAP SUMMARY:
==2347==     in use at exit: 0 bytes in 0 blocks
==2347==   total heap usage: 5 allocs, 5 frees, 73,740 bytes allocated
==2347==
==2347== All heap blocks were freed -- no leaks are possible
==2347==
==2347== For lists of detected and suppressed errors, rerun with: -s
==2347== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Key points about memory leaks:

  • Always delete dynamically allocated memory
  • Use smart pointers like unique_ptr and shared_ptr
  • Use Valgrind to detect memory leaks
  • Compile with -g flag for better debugging information

Handle Memory Allocation Failures

In this step, you'll learn how to handle memory allocation failures in C++. When dynamic memory allocation fails, the new operator throws a std::bad_alloc exception, which you can catch and handle gracefully.

Open the WebIDE and create a new file called memory_allocation.cpp in the ~/project directory:

touch ~/project/memory_allocation.cpp

Add the following code to memory_allocation.cpp:

#include <iostream>
#include <new>
#include <limits>

void demonstrateMemoryAllocation() {
    try {
        // Attempt to allocate a large amount of memory
        const size_t largeSize = 1000000000000; // 1 trillion integers
        int* largeArray = new int[largeSize];

        // This line will not be reached if allocation fails
        std::cout << "Memory allocation successful" << std::endl;

        // Clean up allocated memory
        delete[] largeArray;
    }
    catch (const std::bad_alloc& e) {
        // Handle memory allocation failure
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
}

void safeMemoryAllocation() {
    // Use std::nothrow to prevent exception throwing
    int* safeArray = new(std::nothrow) int[1000000];

    if (safeArray == nullptr) {
        std::cerr << "Memory allocation failed silently" << std::endl;
        return;
    }

    // Use the allocated memory
    std::cout << "Safe memory allocation successful" << std::endl;

    // Clean up allocated memory
    delete[] safeArray;
}

int main() {
    std::cout << "Demonstrating memory allocation failure handling:" << std::endl;

    // Method 1: Using exception handling
    demonstrateMemoryAllocation();

    // Method 2: Using std::nothrow
    safeMemoryAllocation();

    return 0;
}

Key points about memory allocation failure handling:

  1. Exception handling with std::bad_alloc:

    • try-catch block catches memory allocation exceptions
    • Provides detailed error information
    • Prevents program crash
  2. std::nothrow allocation:

    • Prevents exception throwing
    • Returns nullptr on allocation failure
    • Allows silent failure handling

Compile and run the program:

g++ memory_allocation.cpp -o memory_allocation
./memory_allocation

Example output:

Demonstrating memory allocation failure handling:
Memory allocation failed: std::bad_alloc
Safe memory allocation successful

Important memory allocation strategies:

  • Always check for allocation failures
  • Use exception handling or std::nothrow
  • Implement fallback mechanisms
  • Avoid allocating extremely large memory blocks

Summary

In this lab, you will learn how to handle dynamic memory allocation in C++. First, you will learn to use the new operator to dynamically allocate memory at runtime, allowing for more flexible memory management compared to static allocation. Then, you will learn how to properly release this dynamically allocated memory using the delete operator to prevent memory leaks. Additionally, you will explore creating dynamic arrays with new[] and deleting them with delete[]. The lab also covers the use of smart pointers, such as shared_ptr and unique_ptr, to simplify memory management and avoid common pitfalls. Finally, you will learn techniques to check for and handle memory allocation failures.