Section 4 - Destructors and Copy Constructors
3B.4.1 A Galaxy Destructor
Where is the data of a Galaxy object stored? Is it stored in the object itself? Normally that would be the case, but with the name member being a pointer to heap data, it is not true here. So a Galaxy object consists of a double and a name pointer, but the name itself lies outside the object in the heap. It could be miles from the Galaxy in memory, but the name pointer easily points to it no matter how far away it is.
If the heap data, name, is not part of the object, then when an object is destroyed by the C++ run-time routines (when it goes out of scope, for example) the heap associated with it is not automatically destroyed. We have to do that in our destructor. It is a simple matter in this example:
// destructor Galaxy::~Galaxy() { if (name) delete name; }
As you learned in CS 2a, destructors are called for us by the operating system when an object goes out of scope or is deleted. It is our job to do any clean-up inside the destructor that pertains to heap memory our object created in its short lifetime. It is that simple. And that is exactly what we did above.
3B.4.2 A Copy Constructor
We would like to be able to use old Galaxies as a template for creating new ones. This might sound odd, but this idea is so important in all programming languages we have to discuss it.
In a real application, a class -- even a Galaxy class -- would have many more members than in our simple examples. The number may be in the hundreds. Sometimes we want to create a new object that is almost the same as an existing one with a couple minor changes. Instead of starting from scratch, we would like to copy all the contents of the first object into the second in one step. Then, after the new object is created, we make the minor adjustments by setting the few members that are different, using individual mutator calls.
So we want a constructor that takes a Galaxy object as a parameter and copies its contents to the object we are creating. Without thinking too hard, we might try this:
// would-be constructor Galaxy::Galaxy(const Galaxy &gxy) { this->name = gxy.name; this->magnitude = gxy.magnitude; }
The above is pretty straightforward, right? And in many situations it might work. If the class has hundreds of members we might even try to be smart and use our knowledge of the this pointer to shorten the definition:
// would-be constructor Galaxy::Galaxy(const Galaxy &gxy) { *this = gxy; }
Great. Except for one thing. The name member is a pointer to heap, and when we copy the name member from one Galaxy to another we are only copying the pointer. The heap to which that name points is not copied. The result is that we would then have two name pointers of different Galaxy objects pointing to the same CString in heap. You only need to look at the setName() method or the destructor ~Galaxy() to see how terrible this would be. As usual, I'm going to ask you to do that yourself and see if you can tell me in the discussion area what you found (or didn't find). It's subtle, so there's no shame in your not seeing it immediately, but it is also profound in terms of C++ memory.
So, in our copy constructor, as in all copy constructors, we will allocate memory for the new Galaxy. Here is the code:
// copy constructor Galaxy::Galaxy(const Galaxy &gxy) { name = NULL; // so that setName knows not do delete setName(gxy.name); setMag(gxy.magnitude); }
You might not see where the memory is being allocated, but that's because we are being good programmers and re-using existing code. The setName() method takes care of allocation for us, so there is no need to duplicate that code here, ourselves. Look at setName() and imagine that was called from the copy constructor to see if you understand the flow of logic.
This also demonstrates the correct way to program methods. If we have one method that accomplishes something in our class, we call that method from other member functions. We do not duplicate the code.
These are all the new topics in the class itself, so it's time to look at the entire program.
3B.4.3 Additional Comment on Copy Constructors
Copy constructors are not needed in all languages. C++ is one of the only ones that allows programmers to manage their own memory, so they are essential. Before leaving this topic, let me tell you one more reason why copy constructors are necessary.
Consider an ordinary function (not member to any class) that takes Galaxy objects, such as:
Galaxy whichIsBrighter(Galaxy g1, Galaxy g2);
Although this is only the function prototype, I will assume that you could, if you tried, define this trivial method yourself, based on its name. The definition is not what is interesting here, but something else.
Look at the formal parameters g1 and g2. How do formal parameters get their values in any method? Answer: by copying the values of the arguments passed to them. If there is no copy constructor for Galaxy, this takes the form of a "shallow" default copy, which does not duplicate deep memory. If a copy constructor is supplied, then it is used during the invocation of whichIsBrighter(). And, when the method ends, i.e., returns, and all of the local variables go out of scope, what happens to the Galaxy objects g1 and g2? Answer: they are destructed. (Remember: destructors are called for you by the operating system whenever objects go "out of scope.") The destructor is always called at the end of the function, regardless of whether or not the local variables g1 and g2 were created using a default shallow copy or a copy constructor. With that said, can you imagine what would happen to the client's arguments passed to whichIsBrighter() if we did not supply a copy constructor? Think about it.
The name member of g1 and g2 would be deleted by their respective destructors when the function returns. But their name members were pointing to the same heap CStrings that contained the client names. Why? Because, without copy constructors, the data of the argument, including the name pointer, is just copied to formal parameter (shallow copy), causing the formal parameter name to point to the same value as the argument name. Therefore, the destructors, called when the function ends, would be deleting the names of the g1 and g2 Galaxies which are the same as the names of the client Galaxies! Our client's names would be deleted just because we called a method and passed them as parameters!
This is a classic example of why copy constructors are needed whenever you have deep, i.e., heap, memory.
