Dynamic Memory Allocation in C++: New, Delete, Memory Leaks and Vectors

6
0

Dynamic Memory and Data Structures

Most programs need memory to store variables, arrays, and other data structures. Sometimes the amount of required memory is known while writing the program. In many practical situations, however, the required size becomes known only after the program starts running.

Dynamic memory allows a program to request storage at runtime. It is particularly useful when the amount of data depends on user input, a file, a network response, or the changing state of a data structure.

Why Dynamic Memory Is Needed

Consider a fixed-size array:

int values[100];

The array always contains space for 100 integers. If an integer occupies four bytes on a particular system, the array requires approximately 400 bytes. Its size is already known when the program is compiled.

A fixed-size array creates two possible problems:

  • If the program needs only 10 elements, most of the allocated capacity remains unused.

  • If the program later needs 1,000 elements, an array with capacity 100 is insufficient.

A runtime-sized sequence solves both problems by allocating exactly as many elements as required. In standard C++, declarations such as int values[n];, where n is read at runtime, are not standard variable-length arrays. Some compilers accept them as extensions, but portable C++ should use dynamic allocation or, preferably, a standard container such as vector.

#include <iostream>
using namespace std;
int main() {
int fixedValues[100]{};
cout << sizeof(fixedValues) << '\n';
int n;
cin >> n;
int* dynamicValues = new int[n]{};
delete[] dynamicValues;
dynamicValues = nullptr;
}

The number of bytes occupied by fixedValues depends on the size of int on the current system. The dynamic array, on the other hand, is created only after the value of n becomes available.

Object Lifetime

Every object has a lifetime: a period during which the object exists and may be accessed. Understanding object lifetime is necessary for using pointers and dynamic memory correctly.

Automatic Objects

A local variable declared inside a function or block normally has automatic storage duration. Its lifetime begins when execution reaches its declaration and ends when execution leaves the surrounding block.

#include <iostream>
using namespace std;
int* invalidAddress() {
int temporary = 2;
cout << &temporary << '\n';
return &temporary;
}
int main() {
int* pointer = invalidAddress();
// Dereferencing pointer here would be undefined behavior.
}

The local variable temporary is destroyed when invalidAddress() returns. The returned address may still look like an ordinary memory address, but it no longer points to a live object.

A compiler might print zero, preserve the old-looking address, or produce some other result. None of those outcomes makes the pointer valid. Dereferencing it results in undefined behavior.

Therefore, a function must never return the address of an automatic local variable.

Dynamic Objects

A dynamic object is created using new. Its lifetime is not tied to the block in which it was created. It continues to exist until it is explicitly destroyed using delete.

The expression new int(10) performs three conceptual operations:

  • It obtains enough dynamic storage for one integer.

  • It creates an integer in that storage and initializes it with 10.

  • It returns a pointer to the newly created integer.

#include <iostream>
using namespace std;
int* createValue() {
int* temporary = new int(10);
cout << temporary << '\n';
return temporary;
}
int main() {
int* pointer = createValue();
cout << pointer << '\n';
cout << *pointer << '\n';
delete pointer;
pointer = nullptr;
}

The pointer returned by createValue() remains valid after the function ends because the integer was created dynamically. Dereferencing the pointer produces 10.

The pointer variable itself is still an automatic local variable. The integer to which it points is the dynamic object. These are two separate objects with different lifetimes.

Releasing One Dynamic Object

Storage obtained using new must be released using delete. Releasing the storage ends the dynamic object's lifetime and makes the storage available for reuse.

#include <iostream>
using namespace std;
int main() {
int* pointer = new int(10);
cout << pointer << '\n';
cout << *pointer << '\n';
delete pointer;
pointer = nullptr;
cout << pointer << '\n';
}

After delete pointer;, the integer no longer exists. The pointer may still contain the old address, but that address must not be dereferenced. Assigning nullptr makes it explicit that the pointer currently points to no object.

The statement delete pointer; deletes the pointed-to object; it does not destroy the pointer variable itself. Assigning nullptr is a separate operation.

Creating a Dynamic Array

A dynamic array is created using the array form of new:

int* values = new int[n];

This expression obtains storage for n integers and returns a pointer to the first element. The array's length is not stored in the pointer, so the program must maintain n separately.

Without an initializer, fundamental elements such as integers have indeterminate values. Empty braces value-initialize the elements:

int* values = new int[n]{};

For integers, this initializes every element to zero.

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int* values = new int[n]{};
for (int index = 0; index < n; index++) {
cin >> values[index];
}
for (int index = 0; index < n; index++) {
cout << values[index] << ' ';
}
cout << '\n';
delete[] values;
values = nullptr;
return 0;
}

If n is 6, the input may contain six values such as 5 6 7 8 1 2. The valid indexes are 0 through 5, or more generally 0 through n - 1.

Indexing and Pointer Arithmetic

The pointer returned for a dynamic array points to its first element. Array indexing is defined in terms of pointer arithmetic:

values[index] is equivalent to *(values + index).

Adding index advances the pointer by that many elements, not merely by that many bytes. The compiler accounts for the size of the pointed-to data type.

#include <iostream>
using namespace std;
int main() {
int* values = new int[3]{10, 20, 30};
cout << values[0] << '\n';
cout << *(values + 0) << '\n';
cout << values[2] << '\n';
cout << *(values + 2) << '\n';
delete[] values;
values = nullptr;
}

Using the Correct Form of Delete

The form of delete must match the form of new:

  • Use delete pointer; for one object created with new Type.

  • Use delete[] pointer; for an array created with new Type[count].

Using the wrong deletion form results in undefined behavior. A dynamic array must therefore be released with delete[], even when it contains only one element.

Memory Leaks

A memory leak occurs when dynamically allocated storage remains occupied but the program no longer has a usable pointer through which it can release that storage.

One common cause is forgetting to call delete or delete[]. Another is overwriting the only pointer to an allocation.

#include <iostream>
using namespace std;
int main() {
int* pointer = new int(10);
pointer = new int(20);
delete pointer;
pointer = nullptr;
}

The first allocation stores 10. The second assignment replaces its address with the address of a newly allocated integer containing 20. The final delete releases only the second object. The address of the first object has been lost, so that allocation has leaked.

The existing object must be released before its only pointer is overwritten:

#include <iostream>
using namespace std;
int main() {
int* pointer = new int(10);
delete pointer;
pointer = new int(20);
cout << *pointer << '\n';
delete pointer;
pointer = nullptr;
}

A small leak may initially appear harmless, but repeatedly losing allocations can steadily consume the process's available memory. Long-running applications are especially vulnerable to this problem.

Dangling Pointers

A dangling pointer contains the address of an object whose lifetime has already ended. Deleting an object does not automatically change all pointers that contain its address.

#include <iostream>
using namespace std;
int main() {
int* pointer = new int(10);
cout << pointer << '\n';
cout << *pointer << '\n';
delete pointer;
// pointer now contains a dangling address.
// Reading or writing through it would be undefined behavior.
pointer = nullptr;
}

Dereferencing a dangling pointer does not reliably produce a particular “garbage value.” It causes undefined behavior. The program could appear to work, print an unexpected value, corrupt memory, or terminate.

Once storage has been released, it may be reused for another object. Reading or writing through the old pointer could therefore interfere with unrelated data.

Returning the Address of a Local Variable

A pointer returned from a function can also become dangling when it refers to an automatic local variable. The object's lifetime ends as soon as the function returns.

Returning the old-looking address does not extend the local variable's lifetime. A valid alternative is to return the value itself. If dynamic allocation is genuinely required, ownership of the dynamically created object must be transferred clearly and eventually released.

Double Deletion

Each dynamic allocation must be released exactly once. Deleting the same allocation twice causes undefined behavior.

#include <iostream>
using namespace std;
int main() {
int* pointer = new int(10);
delete pointer;
pointer = nullptr;
delete pointer;
return 0;
}

Deleting a null pointer is safe and has no effect. Assigning nullptr immediately after deletion can therefore help prevent accidental deletion through the same pointer variable.

However, setting one pointer to nullptr does not update other pointers that may contain the same old address. Clear ownership rules remain essential.

Ownership of Dynamic Memory

Ownership identifies the part of a program responsible for ending a resource's lifetime. When code directly requests dynamic storage, it must ensure that the storage is released on every possible execution path.

A clear ownership model answers three questions:

  • Which object or section of code owns the allocation?

  • Who is allowed to use the allocation without owning it?

  • When and how will the allocation be released?

Not every pointer is necessarily an owner. Several pointers may observe the same object, but only the owner is responsible for destroying it. If ownership is unclear, the program can develop leaks, double deletions, or dangling pointers.

Resource Acquisition Is Initialization

Resource Acquisition Is Initialization, commonly abbreviated as RAII, is a fundamental C++ technique. A resource is placed under the control of an object immediately after it is acquired. When that owning object is destroyed, its destructor automatically releases the resource.

A raw allocation must never be performed without preserving the returned pointer:

new int(10);

This statement creates an integer but immediately discards its address. Since the program has no pointer to the object, it cannot call delete on that allocation. The storage is leaked immediately.

Modern C++ prefers automatic resource-owning types over manually paired new and delete expressions. Standard containers and smart pointers implement RAII and substantially reduce lifetime errors.

Prefer Vector for Runtime-Sized Arrays

A vector is the standard choice for a runtime-sized sequence of elements. It knows its current size, manages its own storage, can grow and shrink, and releases its memory automatically.

#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> values(n);
for (int& value : values) {
cin >> value;
}
for (int value : values) {
cout << value << ' ';
}
cout << '\n';
}

No explicit deletion is required. When values leaves its scope, its destructor releases the managed storage automatically.

Raw dynamic arrays remain important for understanding memory, pointers, ownership, and the internal behavior of data structures. For ordinary application and problem-solving code, however, vector is usually safer and clearer.

Problem 1: Sum a Runtime-Sized Array

Given an integer n, read n integers into a dynamically allocated array, calculate their sum, print the result, and correctly release the allocated storage.

Approach

  • Read the number of elements.

  • Allocate an array of n integers.

  • Read the values using indexes from 0 to n - 1.

  • Maintain an accumulator initialized to zero.

  • Add every array element to the accumulator.

  • Print the accumulated sum.

  • Release the array using delete[].

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int* values = new int[n];
for (int index = 0; index < n; index++) {
cin >> values[index];
}
long long sum = 0;
for (int index = 0; index < n; index++) {
sum += values[index];
}
cout << sum << '\n';
delete[] values;
values = nullptr;
return 0;
}

Complexity Analysis

  • Time complexity: O(n), because every element is read and added once.

  • Auxiliary space: O(n), because the dynamically allocated array stores n integers.

Problem 2: Grow a Dynamic Array

A raw dynamic array cannot increase its original allocation in place through ordinary array operations. To append an element, create a larger array, copy the existing elements, place the new value at the end, and release the old storage.

For example, suppose the original array contains:

10 20 30

Appending 40 requires a new array of size four:

10 20 30 40

Approach

  • Read n and allocate an array of size n.

  • Read the original elements.

  • Read the value that must be appended.

  • Allocate a second array of size n + 1.

  • Copy the original n elements into the second array.

  • Store the new value at index n.

  • Print or otherwise use the enlarged array.

  • Release both allocations exactly once.

#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int* values = new int[n];
for (int index = 0; index < n; index++) {
cin >> values[index];
}
int newValue;
cin >> newValue;
int* enlarged = new int[n + 1];
for (int index = 0; index < n; index++) {
enlarged[index] = values[index];
}
enlarged[n] = newValue;
for (int index = 0; index < n + 1; index++) {
cout << enlarged[index] << ' ';
}
cout << '\n';
delete[] values;
values = nullptr;
delete[] enlarged;
enlarged = nullptr;
return 0;
}

Why the New Element Is Stored at Index n

The enlarged array has n + 1 elements, so its indexes range from 0 to n. The original elements occupy indexes 0 through n - 1. Therefore, index n is the final available position and is used for the appended value.

Complexity Analysis

  • Time complexity: O(n), because all original elements must be copied.

  • Auxiliary space: O(n), because a second array of size n + 1 is allocated.

A vector performs this kind of storage management automatically. Calling push_back() appends an element without requiring application code to manually allocate, copy, and release raw arrays.

Common Dynamic Memory Errors

  • Forgetting to release storage obtained through new.

  • Using delete for an allocation that requires delete[].

  • Overwriting the only pointer to an allocation before releasing it.

  • Dereferencing a pointer after the pointed-to object has been deleted.

  • Returning the address of an automatic local variable.

  • Deleting the same allocation more than once.

  • Accessing an index outside the valid range of a dynamic array.

  • Discarding the pointer returned by new.

  • Failing to preserve an array's length separately from its pointer.

Key Takeaways

  • Dynamic memory is useful when the required amount of storage is known only at runtime.

  • Automatic objects are destroyed when their scope ends.

  • Objects created with new remain alive until they are destroyed with the matching form of delete.

  • Use delete for one dynamic object and delete[] for a dynamic array.

  • A pointer does not store the length of a dynamic array.

  • Array indexing and pointer arithmetic provide equivalent access expressions.

  • A memory leak occurs when allocated storage can no longer be released.

  • A dangling pointer refers to an object whose lifetime has ended.

  • Each allocation must have a clear owner and must be released exactly once.

  • RAII types manage resources automatically and are preferred in modern C++.

  • For runtime-sized arrays, vector is normally safer and more convenient than manual new[] and delete[].

C++

Read Similar Blogs

Comments0