Whenever a program instantiates or creates a new object, it automatically calls a constructor to construct or initialize it. While programmers write most constructors, two are necessary for correct object behavior: the default and copy constructors. Programmers can use these constructors, but the compiler provides them if they don't. (The compiler also provides an assignment operator, which we explore in the next chapter.) Although they are not always required, C++ also allows programmers to write a similar but opposite function called a destructor.
A destructor destroys or "cleans up" an object when the program no longer needs it. In general, destroying or cleaning up an object means releasing any resources that the object holds. A resource is any part of a computer system - hardware, software, or data - needed to complete a task. Disk space, main memory, and CPU time are common resource examples. What it means to "clean up" an object depends on the object, what it contributes to the program, and what resources it uses. For example, if the object created some temporary files (using disk space), the program should remove them before it terminates. Or, if the object opened a database (using data), the database should be closed. But destructors are most often used to deallocate dynamic memory allocated on the heap.
Memory management for automatic or local objects is scope-driven. The program automatically allocates and deallocates stack memory when an object comes into or goes out of scope. However, memory management for dynamic objects allocated on the heap is under direct programmer control. Programmers allocate and deallocate heap memory with the new
and delete
operators, respectively.
Java has an automatic garbage collector, making it easier than C++ to manage heap memory. But automatic garbage collection also places a greater burden1 on a running program than explicitly deleting a single, specific object when the program no longer needs it. So, in this respect, a correct C++ program will generally outperform an equivalent Java program. But the C++ approach also burdens programmers who must be careful to avoid memory leaks. Destructors help programmers avoid memory leaks but can't prevent them.
Memory leaks are notoriously difficult to find. And the difficulty increases exponentially with the size of the program and the number of program files. So, the best programming practice is understanding what causes memory leaks and avoiding creating them. The following figure illustrates two common programming errors creating memory leaks that destructors can't prevent.
Overwriting An Address | Lost Address |
---|---|
Person* p1 = new Person; // (a) ... p1 = new Person; // (b) |
void f() { Person* p2 = new Person; // (c) ... } |
new
operator allocates and returns the address of heap memory, which the program typically stores in a pointer variable. If the program loses the address, it can't use, recover, or delete the allocated memory, creating a memory leak. (The memory isn't physically lost, and the operating system reclaims it when the program terminates.) Programmers inadvertently create memory leaks in two ways:
If destructors can't help us avoid the memory leaks illustrated above, what errors do they help prevent? Fortunately, they help us with aggregation: complex classes with pointer members. Safe and secure programs always initialize object pointers and deallocate all unused heap memory. Constructors perform the initialization, and destructors handle the deallocation. Destructors and constructors are similar in many ways:
~
, and they may not have any parameters (implying that programmers cannot overload them). Destructors can perform any "clean-up" steps necessary to destroy an object, but memory deallocation is the most common.
class Person { private: string* name = nullptr; int weight = 0; double height = 0; public: Person() {} Person(string n, int w, double h) : name(new string(n)), weight(w), height(h) {} Person(int w, double h) : name(nullptr), weight(w), height(h) {} |
|
The question of ownership in a whole-part relationship further complicates dynamic memory management. The part object's owner is responsible for destroying or deleting the part when it's no longer needed. When the part is not shared, the whole object is the owner. Assigning the ownership responsibilities to a specific class is more challenging when two or more objects share a part. If the underlying problem or other design constraints don't favor one, choose the one that makes the program easiest to write. The automatic destruction of a part object is a common destructor task.
Local Variables | Heap Variables |
---|---|
void g() { Person p1("Wally"); // (a) } |
void f() { Person* p2 = new Person("Dilbert"); delete p2; // (b) } |
new
operator. Programmers may use dynamic memory outside the function where it is allocated (for example, the function might return it). The compiler cannot "understand" when the programmer wants to discard or retain dynamic memory when a function ends. So, the compiler leaves managing all heap memory to the programmer.
delete
operator signals that the program is finished with the dynamic memory, and that is sufficient to trigger a call to the Person destructor.