C++ for C Developers | Lesson 4: Managing Memory: new, delete, and the Basics of RAII

Goal: Understand C++’s new and delete operators for managing memory on the heap, see how they differ from C’s malloc and free, and get introduced to the vital C++ concept of RAII for safer resource management.

1. Dynamic Memory in C: malloc and free Recap

In C, you use malloc (or calloc, realloc) from <stdlib.h> to allocate memory on the heap and free to release it.

C
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p_int = NULL;
    size_t num_elements = 5;

    // Allocate memory for one integer
    p_int = (int*)malloc(sizeof(int)); // Returns void*, requires cast, size calculation needed

    if (p_int == NULL) {
        perror("Failed to allocate memory");
        return 1;
    }
    *p_int = 42;
    printf("Value: %d\n", *p_int);
    free(p_int); // Deallocate memory
    p_int = NULL; // Good practice: nullify pointer after free

    // Allocate memory for an array of integers
    int *p_array = (int*)malloc(num_elements * sizeof(int));
    if (p_array == NULL) {
        perror("Failed to allocate array memory");
        return 1;
    }
    for (size_t i = 0; i < num_elements; ++i) {
        p_array[i] = i * 10;
    }
    printf("Array element 3: %d\n", p_array[3]);
    free(p_array); // Deallocate array memory
    p_array = NULL;

    return 0;
}

Key points about malloc/free:

  • Returns void*, requiring an explicit cast.
  • Requires manual calculation of bytes needed (sizeof).
  • Doesn’t initialize the allocated memory (unless using calloc).
  • Doesn’t understand C++ objects (doesn’t call constructors/destructors).
  • Error indication is via NULL return value.
  • You must free exactly what malloc returned.

2. C++ Way: new and delete Operators

C++ introduces the new and delete operators, which are type-aware and integrate with object lifecycles.

C++
#include <iostream>
#include <string> // For a simple object example

int main() {
    // Allocate memory for a single integer
    int *p_int = new int; // Type is known, no cast needed, no sizeof needed
    *p_int = 123;
    std::cout << "Value: " << *p_int << std::endl;
    delete p_int;   // Deallocate the integer's memory
    p_int = nullptr; // C++ equivalent of NULL (preferred)

    // Allocate memory for a single C++ object (std::string)
    std::string *p_str = new std::string("Hello from heap!"); // Allocates AND calls the string's constructor
    std::cout << "String: " << *p_str << std::endl;
    std::cout << "Length: " << p_str->length() << std::endl; // Use -> for member access via pointer
    delete p_str;   // Calls the string's destructor AND deallocates memory
    p_str = nullptr;

    // Allocate memory for an array of doubles
    size_t num_elements = 5;
    double *p_array = new double[num_elements]; // Allocate an array
    for (size_t i = 0; i < num_elements; ++i) {
        p_array[i] = i * 1.1;
    }
    std::cout << "Array element 2: " << p_array[2] << std::endl;
    delete[] p_array; // CRITICAL: Use delete[] for arrays allocated with new[]
    p_array = nullptr;

    return 0;
}

Key points about new/delete:

flowchart LR
    subgraph C [C: malloc/free]
        A1("malloc() returns void*")
        A2("Requires cast")
        A3("Requires sizeof")
        A4("Does NOT call constructor")
        A5("free() deallocates manually")
    end
    subgraph CPP [C++: new/delete]
        B1("new returns typed pointer")
        B2("No cast or sizeof needed")
        B3("Calls constructor")
        B4("delete calls destructor and frees memory")
        B5("new[] / delete[] for arrays")
    end
  • Type-Safe: new int returns an int*, new std::string returns a std::string*. No cast is needed.
  • Size Calculated Automatically: You just specify the type (new int), not the size in bytes.
  • Object Initialization: new calls the appropriate constructor for the object being created.
  • Object Cleanup: delete calls the appropriate destructor for the object before freeing memory. (We’ll cover constructors/destructors in detail soon).
  • Array Syntax: Use new Type[size] for arrays and delete[] pointer to free them. Mixing delete and delete[] is a serious error (undefined behavior)!
  • Error Handling: By default, if new fails to allocate memory, it throws an exception (std::bad_alloc) rather than returning nullptr. (You can request the non-throwing version: new (std::nothrow) int; which returns nullptr on failure, similar to malloc).

Comparison Table:

Featuremalloc/free (C)new/delete / new[]/delete[] (C++)
Return Typevoid* (needs cast)T* (typed pointer, no cast needed)
SizeManual calculation (sizeof)Automatic based on type
ObjectsUnaware (no constructor/destructor calls)Aware (calls constructor on new, destructor on delete)
ArraysManual calculation (N * sizeof(T))new T[N] syntax
Array Freefree(ptr)delete[] ptr (Must match new[])
ErrorReturns NULLThrows std::bad_alloc (default) or returns nullptr

3. The Dangers of Manual Memory Management

Whether using malloc/free or new/delete, manual management is error-prone:

  • Memory Leaks: Forgetting to delete or free memory that’s no longer needed. The program consumes more and more memory over time.
  • Dangling Pointers: Using a pointer after the memory it pointed to has been deleted or freed. Leads to crashes or unpredictable behavior.
  • Double Free/Delete: Freeing or deleting the same memory twice. Leads to corruption and crashes.
  • delete/delete[] Mismatch: Using delete on an array allocated with new[] or vice-versa. Undefined behavior.

These errors become much harder to manage in complex code with multiple exit points (returns, loops, and especially exceptions).

4. RAII: Resource Acquisition Is Initialization (The C++ Way)

This is one of the most fundamental C++ idioms for managing resources (like dynamic memory, file handles, network sockets, mutexes, etc.).

sequenceDiagram
    participant StackObj as "RAII Object"
    participant HeapRes as "Heap Resource"
    StackObj->>HeapRes: Constructor acquires resource (new)
    Note right of StackObj: Resource tied to object's lifetime
    alt Normal scope exit
        StackObj-->>HeapRes: Destructor releases resource (delete)
    else Exception thrown
        StackObj-->>HeapRes: Destructor still releases resource!
    end

Core Idea: Tie the lifetime of a resource to the lifetime of an object on the stack.

  • Acquire the resource in the object’s constructor.
  • Release the resource in the object’s destructor.

Since C++ guarantees that destructors of stack-based objects are called when the object goes out of scope (either normally or due to an exception), the resource is automatically cleaned up.

Simple Conceptual Example (Manual RAII Wrapper):

C++
#include <iostream>

// Simple wrapper class for a dynamically allocated int
class ManagedInt {
private:
    int* m_ptr; // Pointer to the heap-allocated integer

public:
    // Constructor: Acquires the resource (allocates memory)
    ManagedInt(int value) {
        m_ptr = new int(value); // Allocate and initialize
        std::cout << "ManagedInt created (allocated memory for " << *m_ptr << ")" << std::endl;
    }

    // Destructor: Releases the resource (deallocates memory)
    ~ManagedInt() {
        std::cout << "ManagedInt destroying (deallocating memory for " << *m_ptr << ")" << std::endl;
        delete m_ptr; // Free the memory
        m_ptr = nullptr;
    }

    // Method to access the value
    int getValue() const {
        return *m_ptr;
    }

    // Prevent copying (for simplicity in this example - copy requires careful handling)
    ManagedInt(const ManagedInt&) = delete;
    ManagedInt& operator=(const ManagedInt&) = delete;
};

void useManagedInt() {
    ManagedInt mi(100); // Object created on stack, constructor allocates memory
    std::cout << "Inside function, value = " << mi.getValue() << std::endl;
    // ... maybe some code that could return early or throw an exception ...
    if (mi.getValue() > 50) {
        std::cout << "Value is large enough." << std::endl;
        // No need to manually delete here!
    }
} // mi goes out of scope here. Its destructor (~ManagedInt) is automatically called!

int main() {
    std::cout << "Entering main..." << std::endl;
    useManagedInt();
    std::cout << "Exited useManagedInt. Memory should be freed." << std::endl;
    std::cout << "Exiting main." << std::endl;
    return 0;
}

The Power of RAII: Notice how we didn’t need a delete call inside useManagedInt. When mi goes out of scope (at the end of the function), its destructor ~ManagedInt() is automatically invoked by C++, ensuring the delete m_ptr; happens reliably. This works even if the function exits early or an exception occurs (exception handling details later).

flowchart TD
    A1["Manual Management"] -->|allocate| M1["new / malloc"]
    M1 -->|use| M2["do stuff"]
    M2 -->|free| M3["delete / free"]
    M2 -->|forget to free| M4["Memory Leak"]
    M2 -->|exception| M5["No cleanup"]

    A2["RAII"] -->|create object| R1["Constructor allocates"]
    R1 --> R2["use object"]
    R2 -->|scope ends| R3["Destructor auto-cleans"]

5. Smart Pointers: RAII in the Standard Library

Manually writing RAII wrappers for everything is tedious. The C++ Standard Library provides smart pointers that implement RAII for dynamically allocated memory. The most common one for single ownership is std::unique_ptr (from <memory>).

C++
#include <iostream>
#include <memory> // Include header for smart pointers
#include <string>

void useSmartPointer() {
    // Create a unique_ptr managing a dynamically allocated int initialized to 200
    std::unique_ptr<int> smart_p_int(new int(200));

    // Create a unique_ptr managing a string
    std::unique_ptr<std::string> smart_p_str(new std::string("Smart!"));

    // Access the managed object using * and -> just like raw pointers
    std::cout << "Smart int: " << *smart_p_int << std::endl;
    std::cout << "Smart string: " << *smart_p_str << ", Length: " << smart_p_str->length() << std::endl;

    // *smart_p_int = 300; // Can modify the managed object

} // smart_p_int and smart_p_str go out of scope here.
  // Their destructors are automatically called, which in turn call 'delete'
  // on the raw pointers they manage. No manual 'delete' needed!

int main() {
    std::cout << "Using smart pointers..." << std::endl;
    useSmartPointer();
    std::cout << "Smart pointers went out of scope. Memory freed automatically." << std::endl;
    return 0;
}

Takeaway: Prefer smart pointers (std::unique_ptr, std::shared_ptr) over raw new and delete. They implement RAII for you, making code significantly safer and easier to manage. We’ll revisit these later, but the principle is crucial.

Summary of Lesson 4:

  • C++ uses new (for single objects/primitives) and new[] (for arrays) to allocate memory dynamically.
  • C++ uses delete (for single objects/primitives) and delete[] (for arrays) to deallocate memory.
  • new/delete are type-safe and interact with object constructors and destructors.
  • Crucially, match new with delete and new[] with delete[].
  • Manual memory management (new/delete) is error-prone (leaks, dangling pointers, double deletes).
  • RAII (Resource Acquisition Is Initialization) is a core C++ idiom: tie resource lifetime to object lifetime using constructors and destructors for automatic cleanup.
  • Smart pointers (like std::unique_ptr) are standard library classes that implement RAII for dynamic memory, greatly reducing errors. Prefer smart pointers over raw new/delete in modern C++.

Next Lesson: We’ll officially dive into classes in C++, building upon C structs to add behavior (member functions) and control access (public/private) – the foundation of Object-Oriented Programming in C++.

More Sources:

cppreference – new expression: https://en.cppreference.com/w/cpp/language/new

cppreference – RAII Explanation: https://en.cppreference.com/w/cpp/language/raii

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top