The operators new and delete are the C++ way of allocating and deallocating memory for objects. Their use is quite error prone, but many problems can be avoided by following a few basic rules and recommendations.
Rule 8.1 delete should only be used with new .
Rule 8.2 delete [] should only be used with new [] .
Rule 8.3 Do not access a pointer or reference to a deleted object.
Rec 8.5 If you overload operator new for a class, you should have a corresponding overloaded operator delete .
Rec 8.6 Customize the memory management for a class if memory management is an unacceptably-large part of the allocation and deallocation of free store objects of that class.
Rule 10.4 , base class destructor.
Rule 12.5 , exceptions thrown inside destructors.
Rec 12.9 , using the stack instead of the free store.
Rule 8.1 delete should only be used with new.
Rule 8.2 delete [] should only be used with new [].
It is important to understand how memory is managed in C++. You should understand what happens when an object is created with new and what happens when the delete operator ends its lifetime.
Allocation and deallocation of free store objects are done in steps:
Since different functions are used for allocation and deallocation of single objects and arrays of objects, you must use the correct delete expression when a pointer is deleted. If not, the wrong function will be called to release the memory occupied by the object.
The reason that different functions are called is that it should be possible for an implementation to use the algorithms that are best suited for either case. If different algorithms are used, the memory will not be properly released if the wrong function is called, and the program will probably crash.
Allocate and deallocate free store object
Since EmcString does not overload neither operator new nor operator delete , the default functions for memory allocation will be called.
EmcString* sp = new EmcString("Hello"); // Calls ::operator new delete sp; // Calls ::operator delete() const size_t arraySize = 5; EmcString* sa = new EmcString[arraySize]; // Calls ::operator new[]() delete [] sa; // Calls ::operator delete[]()
Rule 8.3 Do not access a pointer or reference to a deleted object.
You must decide what to do with your pointer after you have deleted the object assigned to it. A pointer that has been used as argument to a delete expression should not be used again unless you have given it a new value, since the language does not define what should happen if you access a deleted object. You could either assign the pointer to 0 or a new valid object. Otherwise you get a "dangling" pointer.
Dangerous access to deleted object
The following code is legal, but the behavior is undefined.
EmcString* sp = new EmcString("Hello"); delete sp; cout << *sp << endl; // No: Undefined behavior !!
You should also avoid deleting the this pointer. It is potentially dangerous to do so, and your code will be more difficult to understand.
If a class provides a member function that deletes this , it can be dangerous to make such a member function an ordinary member function, since it is possible that the this pointer must be accessed when returning from the function.
You should not try to delete an object allocated on the stack with such a member function. A common trick is to declare the destructor as either private or protected to prevent objects on the stack from being created.
class W { public: W(); void goAway(); static void foo(); void bar(); // ... protected: ~W(); };
Objects of the class W can only be created with new since it has a protected destructor. For that reason, it is also not possible to delete the object outside the scope of the class. Instead the member function goAway() has been provided that deletes the object.
void W::goAway() { delete this; // No!! } W* w = new W; w->goAway();
After the call to goAway() , it is undefined what happens if you try to use the object.
w->foo(); // May crash !!! w->bar(); // May crash !!!
Rec 8.5 If you overload operator new for a class, you should have a corresponding overloaded operator delete.
Objects can be allocated with many different new expressions. The result of a new -expression is either a null-pointer or a pointer to an object with a lifetime that is determined by the programmer. When the object is no longer needed, some code is needed to properly return the memory and perhaps other resources allocated by the object. To delete a pointer to the object is not always the right thing to do, since memory could have been allocated by some other means than operator new(size_t) .
For example, it is possible to provide additional placement arguments in a new -expression. The function that allocates storage for such an object is also called a placement operator new.
A common form of placement new, that is part of the standard library, takes a memory address as argument.
const int maxSize = 100; // get storage for object // assumption: sizeof(A) < 100 void* storage = (void*)new char[maxSize]; // call placement new to create object A* ap1 = new (storage) A();
To delete a pointer pointing to such an object is not recommended, but the destructor should always be called. It is possible and correct to call the destructor explicitly in this situation.
// Use ap1 ap1->~A(); // call destructor, not delete // reuse storage: sizeof(B) < 100 B* bp1 = new ((void*)storage) B(); // ... delete [] storage;
It is possible to overload operator new, operator delete, operator new[] and operator delete [] for a class. If we want to customize memory management for a class this is the correct thing to do.
The interaction between exception handling and customized memory management must be understood to avoid memory-related errors.
If an exception is thrown by a constructor for an object created with new , the run-time system is responsible for returning the memory allocated for the object. The client has no way of doing this, since a pointer to the object is not available until the object has been fully constructed. For this to work, the run-time system must know how to correctly deallocate objects created by different new -expressions.
The scope of the operator new used by the new -expression is searched for a matching operator delete . A declaration of an operator delete matches the declaration of a operator new when it has the same number of parameters and all parameter types except the first are identical. The run-time system will then call the matching operator de lete to deallocate a partially constructed object.
Until recently it was not possible to provide additional arguments to operator delete and operator delete[] , but now it is both possible and recommended to overload these member functions if a class has its own memory management. If not, the program could crash before an exception handler is given the chance to handle the exception and there is also the risk of getting memory leaks.
If the compiler does not support this rather new language feature, one deallocation function that can be used with all different allocation functions is an alternative to an overloaded deallocation function, but then additional arguments to the new -expression will not be available when the deallocation function is called. This makes it difficult to customize memory management when only exceptions are supported by the compiler.
Class with customized memory management
The class A has customized memory management. An additional placement argument is provided to allow the client to control where in memory objects are placed.
class BadArgument { public: explicit BadArgument(int); // ... }; class A { public: A(); A(int) throw (BadArgument); ~A(); // ... void* operator new(size_t size); void* operator new[](size_t size); void* operator new(size_t size, const Pool<A>& p); void* operator new[](size_t size, const Pool<A>& p); void operator delete(void* vp); void operator delete[](void* vp); void operator delete(void* vp, const Pool<A>& p); void operator delete[](void* vp, const Pool<A>& p); // ... };
A has a constructor that throws an exception. If an exception is thrown the correct operator delete() will be called.
A::A(int i) throw (BadArgument) { // ... if (i == 42) throw BadArgument(42); } A* createA(int i) { // throws exception if i == 42 return new A(i); // if exception is thrown, call // A::operator delete(void*) } A* createA(int i, const Pool<A>& memoryPool) { // throws exception if i == 42 return new (memoryPool) A(i); // if exception is thrown, call // A::operator delete(void*, const Pool<A>& p) }
Rec 8.6 Customize the memory management for a class if memory management is an unacceptably-large part of the allocation and deallocation of free store objects of that class.
When should a class customize its memory management? Different memory management algorithms have different performance characteristics. When using a general algorithm, both the size and location of memory blocks must be stored and updated by the functions. A customized allocator, that only manage memory blocks of one size, can do less book-keeping and is therefore faster.
Some objects are very often created in large numbers on the free store. Sometimes the memory management of such objects can be a large part of the overall time spent on allocation of such objects. In these cases it can be very well spent effort to customize the memory management for such a class. Programs can be made to run 5 times faster by such customized memory management. Therefore this can be a good option for improvement if your programs runs unacceptably slow.