Pointers are a powerful tool in programming languages like C and C++, allowing for efficient memory management and data manipulation. However, they can also introduce various problems if not used correctly. In this discussion, we’ll explore some common problems related to pointers, along with examples and figures where applicable.

Dangling pointer

A dangling pointer poses a very serious concern as it occurs when a pointer points to memory that has been deleted or is no longer valid. This can result in unpredictable behavior, crashes, and other unintended consequences during program execution and should be handled carefully to prevent adverse effects on the program’s functionality. The given illustration describes the problem of the dangling pointer in which the pointer d is a dangling pointer.

Press + to interact
Dangling pointer problem
Dangling pointer problem
1 of 5

Let’s look at an example that demonstrates the use of dynamic memory allocation for character arrays and highlights the issue of dangling pointers.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
char * ptr = new char[6] {'D', 'A','V', 'I', 'D'}; // will initialize to 'D', 'A', 'V', 'I', 'D', '\0'
cout << "The content of ptr array is: " <<ptr <<'\n';
char * d;
d = ptr;
d[0] = 'M';
cout <<"ptr : "<< ptr<<'\n';
cout <<" d : "<< d<<'\n';
delete []ptr; // Will delete the memory pointed by ptr on heap
ptr = nullptr; // d is a dangling pointer
char* ptr2 = new char[5] {'M', 'I','K', 'E'}; // most probably MIKE will be overwritten on DAVID
d[0] = 'T'; // d is still pointing to the same location hence any change through d might affect ptr2's memory, which has nthg to do with d
cout <<" d : "<< d <<'\n'; // pointer d is dangling
return 0;
}
  • In the given code, a dangling pointer issue occurs when delete[] is used to deallocate the memory pointed to by ptr, but d still retains the address of the deallocated memory. This makes d a dangling pointer, which can lead to unpredictable behavior when accessed. When creating ptr2, it’s possible that it may reuse the same memory previously used by ptr, causing {'M','I','K', 'E', '\0'} to overwrite {'D','A','V','I','D','\0'}. Modifying d[0] after deallocating ptr can result in undefined behavior since d is now pointing to an invalid memory address. In this case, when we run the code, it prints d : TIKE.

To avoid such problems, it’s essential to handle pointers carefully and set them to nullptr after deallocation. Proper memory management is crucial to ensure stable and reliable code execution.

Fixing the dangling pointer: Deep copying the memory

Shallow copying is when two pointers start pointing to one location. One possible resolution for such an issue is to create a deep copy of the data. Instead of simply copying the address present in ptr to d, we allocate separate memory for d and copy the content pointed to by ptr to d. By doing so, ptr and d will have their own independent memory locations, eliminating the risk of dangling pointers and ensuring that each pointer points to valid and distinct memory blocks. The given illustration clearly describes the solution to a dangling pointer problem.

Press + to interact
Fixing the dangling pointer
Fixing the dangling pointer
1 of 6
Press + to interact
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char * ptr = new char[6] {'D', 'A','V', 'I', 'D'};
cout << "The content of msg0 array is: " <<ptr <<'\n';
char * d = new char[strlen(ptr)+1]; // strlen(ptr) returns 5
// (it returns string length excluding '\0')
for(int i = 0; i < strlen(ptr); i++)
d[i] = ptr[i];
cout <<"After copying \n";
cout <<"ptr : "<< ptr<<'\n';
cout <<" d : "<< d<<'\n';
delete []ptr; // Will delete the memory pointed by ptr on heap
cout <<"ptr : "<< ptr<<'\n';
cout <<" d : "<< d<<'\n';
return 0;
}
  • In the code, ptr is initially allocated on the heap and assigned the values {'D', 'A', 'V', 'I', 'D'}. To avoid a dangling pointer, a new pointer d is created with a new memory block of the appropriate size, calculated as strlen(ptr)+1. Then, a loop is used to copy each character from ptr to d, creating a deep copy of the original content. By doing so, d now holds its own independent memory location with the same content as ptr. After the deep copy is made, ptr can be safely deallocated using delete[], releasing the memory it was pointing to. However, the copy of the original content is preserved in d, ensuring that it remains a valid and accessible pointer. The code demonstrates a way to save the content from a potentially dangling pointer by creating a new pointer and a deep copy, thus avoiding undefined behavior when accessing or using the pointer later.

By creating a deep copy, the code eliminates the dangling pointer issue. Now, d holds a valid memory location with its own independent copy of the original content, allowing both pointers to be used independently without interfering with each other’s memory. This ensures proper memory management and avoids potential undefined behavior associated with dangling pointers.

Memory leakage

Dynamic memory allocation using the new operator requires explicit deletion with the delete operator to avoid memory leaks, where memory gradually accumulates and causes a detrimental impact on program performance. Failing to properly deallocate memory can result in memory saturation, leading to persistent memory leaks that can damage program execution and degrade system stability. It is crucial to ensure responsible memory management by diligently releasing dynamically allocated memory to mitigate the risks of memory leakage and ensure optimal program performance.

In the example discussed above, we addressed the issue of dangling pointers, but unfortunately, it resulted in a new problem: memory leakage. This occurred because when we copied the content of ptr to d, we allocated new memory for d without releasing the memory it was previously pointing to. As a result, we lost the reference to that memory block, leading to a memory leak. This can cause inefficiencies in memory usage and can lead to performance issues in the long run if not properly addressed. To solve this issue, we need to free the allocated memory before assigning d to another allocated memory. The following illustration describes the solution.

Press + to interact
Memory leakage
Memory leakage
1 of 3

Here’s the code for solving the memory leakage issue:

#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char * ptr = new char[6] {'D', 'A','V', 'I', 'D'};
cout << " ptr: " << ptr <<'\n';
ptr = new char[10]{'H', 'E','L', 'L', 'O',' ', 'C', '+', '+', '\0'};
cout << " ptr: " << ptr <<'\n';
return 0;
}
Memory leakage
  • The code in the MemeoryLeakage.cpp has a memory leakage issue. It allocates memory on the heap for the character array 'DAVID' using new, but fails to deallocate this memory before assigning a new memory block to the same pointer, ptr. As a result, the previously allocated memory is lost and can’t be freed, leading to a memory leak. The subsequent allocation of memory for the character array 'HELLO C++' overrides the previous memory block without releasing it, exacerbating the memory leakage problem. To prevent memory leaks, it is necessary to release the previously allocated memory using delete[] before assigning a new memory block to the pointer.

  • Line 9 in the TheFix.cpp file fixes this issue.

Null pointer/illegal address dereferencing

Another potential issue that can arise with dynamic memory allocation is the use of null pointers or illegal memory dereferencing. A null pointer is a pointer that doesn’t point to a valid memory location. Dereferencing a null pointer or attempting to access memory using an illegal address can lead to runtime errors and program crashes.

Similarly, if a pointer is not initialized or explicitly set to a valid memory address, dereferencing it can lead to undefined behavior. It is essential to initialize pointers properly and ensure that they point to valid memory locations before dereferencing them.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int* ptr;
// Dereferencing a pointer with a garbage address will lead to undefined behavior
// *ptr = 10; // uncomment it to check (the program will crash)
ptr = nullptr;
// Dereferencing a null pointer
// This will also lead to undefined behavior
// *ptr = 10; // uncomment it to check (the program will crash)
ptr = new int;
// Check if memory allocation was successful
if (ptr != nullptr)
{
*ptr = 42;
cout << "Dynamically allocated value: " << *ptr << endl;
}
// Deallocate the memory
delete ptr;
ptr = nullptr;
// Accessing the memory after deallocation
// This should not be executed
if (ptr != nullptr)
{
cout << "This should not be executed.";
}
return 0;
}

Deleting stack memory

Deleting stack memory refers to attempting to deallocate memory that was allocated on the stack, which is not allowed. Only memory allocated on the heap using new can be deallocated using delete.

#include <iostream>
using namespace std;
int main()
{
int x = 5;
int* ptr = &x;
cout << "x: "<< *ptr << endl;
// delete ptr; // Logical Error: Invalid deallocation of stack memory
int A[] = {1,2,3,4,5};
int size = sizeof(A)/sizeof(int);
ptr = A;
cout << "A = { ";
for(int i=0;i<size; i++)
cout << ptr[i] << " ";
cout << "}";
// delete [] ptr; // Logical Error: Invalid deallocation of stack memory
return 0;
}
Delete stack memory

In the above code, there are attempts to delete stack memory using the delete operator, which leads to logical errors.

  • When a variable is declared within a function, it is allocated memory on the stack. In the code, a pointer is assigned the address of a stack variable using the address-of operator (&). However, calling delete on this pointer is incorrect because delete is used to deallocate memory that was dynamically allocated on the heap using new. Stack memory is automatically managed by the compiler, and there is no need to explicitly deallocate it using delete.

  • Similarly, when an array is declared within a function, the memory for the array is allocated on the stack. Assigning the array’s address to a pointer and then calling delete[] on that pointer is also incorrect. delete[] is used to deallocate dynamically allocated arrays on the heap, not stack-allocated arrays. Stack memory is automatically released when the scope of the variable ends, and attempting to delete stack memory explicitly can result in undefined behavior and program crashes.

In both cases, trying to delete stack memory using delete or delete[] is a logical error because stack memory is managed by the compiler, and manual deallocation is unnecessary.

Exercise: Identify the error

There are several variants of the errors mentioned above. We’ll discuss them as we proceed in the later part of the course.

As a practice exercise, try to find the bug in the following program.

If you have trouble getting to the solution, you can click the “Solution” button to see how to solve the problem.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int* ptr = new int(5);
cout << "Before deleting => Value: "<<*ptr;
delete ptr;
cout << " After deleting => Value: "<<*ptr;
delete ptr;
return 0;
}