Goodbye malloc/free: C++’s More Powerful Memory Management with new/delete

In C, we often use the functions malloc and free for manual memory allocation and management. However, in C++, two operators, new and delete, are specifically designed to assist programmers in memory allocation and management, significantly simplifying memory management operations, especially for class objects. The new/delete operations are essential and should be learned and mastered.

Basic Usage of the new Operator

First, we need to understand that the purpose of new is to allocate a block of memory of a specified size on the heap and return a pointer to that memory. This is similar to malloc. However, one key difference is that the new operator returns the size of memory based on the specified type, without requiring a cast like malloc. More importantly, when we use new to create an object, we can also initialize it. For classes, this means that the object’s constructor is automatically called to create an object.

Dynamic Creation of a Single Object

The most commonly used form of the new operator is for creating a single object, with the syntax: type* pointerVariable = new type(initializationParameters);. The initialization parameters are passed to the corresponding type’s constructor to complete the object’s initialization.

Creation of Built-in Types

int* p = new int(10);   // Allocate memory for int and initialize to 10
double* d = new double(3.14159);   // Initialize double to 3.14159

For example, with a custom Student class:

#include <string>
class Student {
private:
    string name;
    int age;
public:
    // Constructor with parameters
    Student(string n, int a) : name(n), age(a) {}
    void printInfo() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};
// Create Student object
Student* stu = new Student("Alice", 18);  // Directly call constructor to initialize
stu->printInfo(); // Output: Name: Alice, Age: 18

If no initialization parameters are specified, the default constructor will be called for custom classes, while basic data types will remain uninitialized (with random values in memory):

int* uninit = new int; // Uninitialized, value is uncertain

Creation and Initialization in Array Form

When creating a dynamic array, the new[] form must be used, with the syntax: type* pointerVariable = new type[arrayLength]{initializationList}. In C++11 and later standards, dynamic arrays can be initialized.

For arrays of basic data types:

// Create an array of 5 ints, initialize the first 3 elements, others are 0
int* arr = new int[5]{1, 2, 3};
// Output array elements
for (int i = 0; i < 5; i++) {
    cout << arr[i] << " "; // Output: 1 2 3 0 0
}

For arrays of custom types, each element will call the constructor:

// Create an array of 3 Student objects, all call the default constructor
Student* stuArr = new Student[3];
// If the class does not have a default constructor, initialization parameters must be explicitly provided
Student* stuArr2 = new Student[2]{Student("Bob", 20), Student("Charlie", 21)};

Additionally, it is worth mentioning that the length of the dynamic array created with new can be a variable, similar to variable-length arrays (VLA) in C:

int n = 10;
int* dynamicArr = new int[n]; // Valid, n is a variable

Placement new: Constructing Objects in Allocated Memory

Placement new is a special form that allows constructing objects in already allocated memory space, with the syntax: new (memoryAddress) type(initializationParameters);. This method does not allocate new memory but only calls the object’s constructor.

Placement new is mainly used in memory pools, buffer management, etc. For example:

#include <new> // Remember! Must include this header
// Pre-allocate enough memory to hold a Student object
char* memory = new char[sizeof(Student)];
// Construct Student object in specified memory
Student* stuInMem = new (memory) Student("David", 22);
stuInMem->printInfo(); // Normal use of the object
// Manually call destructor (placement new does not have a corresponding delete, must explicitly destroy)
stuInMem->~Student();
// Release original memory block
delete[] memory;

When using placement new, it is crucial to remember: you must manually call the destructor, and you cannot use delete or delete[] to release the object (since the memory was not allocated by that new), otherwise it will lead to double free errors.

Exception Handling for new Creation Failures

By default, when new fails to allocate memory, it throws a bad_alloc exception (older versions of C++ would return a null pointer). We will discuss exception handling later; for now, just remember the general handling syntax.

try {
    int* c = new int[1000000000]; // Allocate a large amount of memory, may fail
} catch (bad_alloc& e) {
    cout << "Memory allocation failed: " << e.what() << endl;
}

Compared to handling malloc failures, this is slightly more complicated, but there are also some other more convenient handling methods that we will learn later.

Usage of the delete Operator

The main function of delete is to release memory dynamically allocated by new and call the object’s destructor before releasing. Similar to new, delete has various forms that must be strictly matched to avoid memory leaks and other issues.

1. Releasing a Single Object: delete

To release a single object created by new, use the syntax delete pointerVariable;:

int* p = new int(5);
delete p; // Correctly releases, calls destructor (no actual operation for basic types)
p = nullptr; // Recommended to set to null to avoid dangling pointers

2. Releasing Dynamic Arrays: delete []

To release an array created by new[], use delete[]:

int* arr = new int[10];
delete[] arr; // Correctly releases the array
arr = nullptr;
*arr = new int[5];
delete arr;   // Undefined behavior, may cause memory leak!

delete[] will automatically call the destructor for each element in the array (for custom types) and then release the entire memory block.The form of delete must match the form of new, i.e., new with delete and new[] with delete[]. If they do not match, it will lead to undefined behavior, which must be avoided.Following this principle, if a class has multiple constructors that all use new, it is essential to ensure that the same form of new is used and that the matching delete is used in the destructor, as the destructor can only have one.

class student {
public:
    char *name;
    int age;
    student(char *_name, int _age) {
        name = new char[strlen(_name) + 1];
        strcpy(name, _name);
        age = _age;
    }
    student(char *_name) {
        name = new char[strlen(_name) + 1];
        strcpy(name, _name);
        age = 18;
    }
    student() {
        name = "unknown";  // Did not use new to create memory, error on destruction
        age = 18;
    }
    ~student() {
        delete name;   // Should use delete[]
    }
};
int main() {
    student s1("xiaoming", 20);
    student s2("xiaohong");
    student s3;
    return 0;
}

3. Handling Pointers After Release: Avoiding Dangling Pointers

After releasing memory, the original pointer does not automatically become a null pointer but becomes a dangling pointer (pointing to released memory). This is similar to the handling of free; after release, the pointer should be immediately set to null:

Student* stu = new Student("Eve", 23);
delete stu;
stu = nullptr; // Must set to null

4. Avoiding Multiple Releases

For memory created by new, delete can only be called once; otherwise, it may cause the program to crash.

5. Do Not Use delete on Memory Not Created by new

int a[10];
int *p = a;
delete[] p;   // Error, p does not point to memory created by new

Overloading new/delete

As operators, new and delete can be overloaded to implement custom memory allocation strategies (such as memory pools, debugging traces, etc.).

For example, overloading new and delete in a class can customize the memory allocation method for objects of that class:

class MyClass {
public:
    // Overload new
    void* operator new(size_t size) {
        cout << "Custom new allocation" << size << " bytes" << endl;
        void* p = malloc(size); // Can be replaced with memory pool allocation
        if (!p) throw bad_alloc();  // Exception handling
        return p;
    }
    // Overload delete
    void operator delete(void* p) {
        cout << "Custom delete releasing memory" << endl;
        free(p); // Corresponds to malloc release
    }
    // Overload new[]
    void* operator new[](size_t size) {
        cout << "Custom new[] allocation" << size << " bytes" << endl;
        void* p = malloc(size);
        if (!p) throw bad_alloc();
        return p;
    }
    // Overload delete[]
    void operator delete[](void* p) {
        cout << "Custom delete[] releasing memory" << endl;
        free(p);
    }
};
// Using overloaded new/delete
MyClass* obj = new MyClass;
delete obj;
MyClass* arr = new MyClass[5];
delete[] arr;

Note the following points:When overloading new, it must return void*, and the parameter must be size_t (indicating the size of memory to be allocated).When overloading delete, it must be of void type, and the parameter must be void* (indicating the memory address to be released).For new[] and delete[] arrays, they must also be overloaded in pairs to avoid mismatches.Limitations and Improvements of new/delete

As seen above, new and delete in C++ are much simpler to use compared to malloc and free in C, and are more suitable for operations on complex types like classes. However, it is also evident that new and delete are essentially still a manual memory management tool, just optimized at the syntax level to fit C++ data structures. Improper use still poses risks of memory leaks. To address this, C++11 introduced smart pointers, which can help us manage memory automatically, greatly reducing the risks of manual management, which we will learn about later.

Leave a Comment