Overview
In C++, dynamic memory allocation is done by using the new and delete operators. There is a similar feature in C using malloc(), calloc(), and deallocation using the free() functions. Note that these are functions. This means that they are supported by an external library. C++, however, imbibed the idea of dynamic memory allocation into the core of the language feature by using the new and delete operators. Unlike C’s dynamic memory allocation and deallocation functions, new and delete in C++ are operators and are a part of the list of keywords used in C++.
Although, in both C and C++, dynamic memory is allocated in the heap area and gets a pointer returned to the object created, C++ is stricter in the construction norms. C++ never gives the memory of the allocated element direct access to initialize, but a constructor is used instead so as to ensure that:
- Initialization is guaranteed.
- There is no accidental access to an uninitialized object.
- A wrong sized object can’t be returned.
Prior to the use of object created, proper initialization is vital. This one thing can help in avoiding many programming problems. Also note that the operators in C++ actually performs twin functions—it dynamically allocates the memory and also invokes the constructor for proper initialization.
The cleanup procedure with the delete operator also automatically calls the destructor. Although both C and C++ both have a dynamic memory allocation feature, C++ has made it more elegant.
Dynamic Memory Management
The allocation or deallocation of an object—be it arrays of any built-in type or any user-defined type—can be controlled in memory at runtime by using the new and delete operators.
The new Operator
The new operator allocates the exact amount of memory required by the object during execution. The allocation is performed on the free memory area, called the heap, assigned to each and every program especially for the purpose of dynamic memory allocation. The allocation, if successful by using the new operator, returns a pointer to the object; if it fails, a bad_alloc exception is raised. However, on success we can access the object via pointer returned by the new operator.
For example, when we write,
IntArray *intArray = new IntArray(10); MyClass *obj = MyClass;
What happens here is that the new operator allocates the exact amount of memory required by the object in the free store or heap area, invokes the default constructor to initialize the properties of the object, and returns a pointer to the type-specified object allocated in the memory. However, during the allocation process, if the operator finds that there is not enough space in memory to allocate for the object, it throws an exception to indicate the error. The exception gives an opportunity to handle the error programmatically.
Handling new Failures
The following example illustrates how exceptions from new can be raised and handled efficiently through try..catch clauses. Here, we allocate an array of massive values to simulate the failure of the new operator. As soon as it fails, it raises the bad_alloc exception, handled by the catch handler, and is processed further. Here, we have simply printed what caused the exception in the catch block.
#include <iostream> #include <new> int main() { const int SIZE = 10; double *dArray[SIZE]; try { for(int i=0;i<SIZE;++i){ dArray[i] = new double[999999999]; std::cout<<i<<"th object allocated" <<std::endl; } } catch (std::bad_alloc &ex) { std::cerr<<"Exception: "<<ex.what() <<std::endl; } return 0; }
There is another way to handle new failures, by using a function called set_new_handler, defined in the standard <new> header. This function takes a pointer to a function that returns void as an argument. This function is called as soon as the new fails. The idea is similar to what was shown earlier, but without using try…catch.
Here is a quick example.
#include <iostream> #include <new> void myhandler(){ std::cerr<<"myhandler called."<<std::endl; abort(); } int main() { const int SIZE = 10; double *dArray[SIZE]; std::set_new_handler(myhandler); for(int i=0;i<SIZE;++i){ dArray[i] = new double[999999999]; std::cout<<i<<"th object allocated" <<std::endl; } return 0; }
The delete Operator
As soon as we do not need the allocated space, such as, due to object going out of scope, and so forth, we can ensure that the memory occupied is returned back to the store or freed by using the delete operator. The freed memory then again can be reused by succeeding new operator calls. For example, if we write as follows,
double *dArray = new double[50]{}; MyClass *obj = new MyClass;
The empty braces indicate that each element must be initialized with its default initialization. The default initialization of fundamental types is 0.
Now, to deallocate the memory we use the following statement:
delete [] dArray; delete obj;
Note that dArray is an array object; therefore, the square bracket ([]) ensures freeing all of the memory occupied by the array. If we do not use brackets, the result is undefined because some compilers call for the destructor for the first object only whereas other elements of the array remain allocated in the memory.
Therefore, using delete without brackets should be avoided both for user-defined or for built-in arrays. For built-in arrays, it is a runtime error. To ensuring proper destruction of single element, we must use delete instead of delete []; otherwise, the result is undefined.
The delete expression requires an address of an object and always calls the destructor before releasing the memory. Therefore, all the cleanup operations, such as closing an open file, releasing a database connection, and so on, must be put into the destructor.
Some say that it is a good idea to initialize a pointer with null/zero after deleting it because a zero pointer never calls for deallocation. Because deleting an object twice is a bad idea, initializing a pointer with zero/null after deletion just emphasizes the point that it will not call for destruction again.
Four Key Takeaways
The C++ as a language has implemented four noticeable things with dynamic memory allocation.
- Unlike C, the feature is made part of the language and not merely a support from an external library.
- The procedure altogether is made more elegant and efficient with operators.
- These operators are flexible to be overloaded. This means that we can change their behavior to suit our needs.
- We have an option to deal with when the heap runs out of storage.
These are the four key takeaways from dynamic object creation in C++.
Conclusion
Understand that object created in a stack is much more optimal and efficient that objects created in a heap, because the size and lifetime of the object is built into the generated code and known to the compiler prior to compilation. Heap objects, on the other hand, are dynamic and hence involve both time and space overhead. C++ 11’s introduction to unique_ptr made dynamic memory allocation management more efficient. For example, it automatically destroys itself when it goes out of scope and returns the space to the free store. The evolution of dynamic object creation in C++ has many other improvements worthy to be noted.