Whenever a program instantiates or creates a new object, it automatically calls a constructor to construct, build, or initialize it. While programmers write most constructors, two are necessary for correct object behavior, and the compiler creates them automatically. Programmers can accept the compiler-provided default and copy constructors or replace them. (The compiler also provides an assignment operator, which we explore in the next chapter.) Although they are not always necessary, C++ also allows programmers to write similar but opposite functions called destructors.
Destructors destroy or remove objects, "cleaning up" programs when they no longer need the objects. In general, destroying or removing an object entails 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 examples. What it means to destroy an object depends on the object, what it contributes to the program, and what resources it uses. For instance, if the object creates some temporary files (using disk space), the program should remove them before it terminates. Or, if the object opens a database (using data), the program should close it before stopping. Nevertheless, the most frequent destructor task is deallocating dynamic memory allocated from 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 carefully 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 rapidly 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 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; // (i) ... p1 = new Person; // (ii) |
void f() { Person* p2 = new Person; ... } |
(a) | (b) |
new
operator allocates from the heap and returns its address, and the program typically saves the address 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 typically create memory leaks in two ways:
Destructors are member functions, so they can't prevent the memory leaks illustrated in the previous figure. However, they simplify managing an object's resources, including its dynamically allocated memory. The following examples demonstrate how programmers use destructors to support aggregation implemented with complex classes with pointer members. Safe and secure programs always initialize object pointers when creating aggregated parts, and they always release resources and deallocate unused heap memory when finished with aggregation. Constructors perform the initialization, and destructors handle the release and deallocation.
Constructors | Destructors | |
---|---|---|
Names | Same as the class | Same as the class, begins with a tilde, ~ |
Return type | None | None |
Parameters | Any number | None (implying they can't be overloaded) |
Call | Automatic with object instantiation | Automatic with object destruction |
Tasks | Allocate memory and initialize members | Deallocate memory and release resources |
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) {} |
|
In a whole-part relationship, the questions of object sharing and ownership further complicate dynamic memory management: what program objects share the part and which one "owns" it? The part object's owner is responsible for destroying or deleting it 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, leaving the programmer to manage all heap memory.
delete
operator triggers a call to the Person destructor, which performs any necessary "clean up" operations, and then signals the heap manager to return the dynamic memory to the heap.