There are three methods that are more important than all other methods in a class. They're so important, that you should think of them as a group: the "three amigos" of C++, if you will.
In fact, they're so important that if you don't implement these methods, the compiler will generate implicit versions of these methods. Let's repeat that one more time: "if you don't implement these methods, the compiler will generate implicit versions of these methods". Some folks think that that if you don't write a copy constructor, then you can't copy. That's incorrect. The compiler generates implicit versions.
Let's list out the behavior of the implicit versions.
Copies each of the data members using the copy constructor for each type. What does the copy constructor do for various types?
Put simply, this is called "memberwise copy".
Copies each of the data members using the assignment operator for each type. What does the assignment operator do for various types?
Put simply, this is called "memberwise assignment". The assignment operator differs from the copy constructor in that a separate function is called. If dynamic memory is involved, the assignment operator will often deallocate memory, then allocate new memory and copy.
As you might guess, this calls the destructor for each data member.
That raises the question: when should you write these methods?
Sometimes, this is the behavior you want. However, other times, it's not the behavior you want. In fact, many times, it's not the behavior you want. For example, suppose you have variables x and y. They both contain linked lists. You copy y into x. Now both objects contain the "same" list. If you modify x, do you expect y to change? Probably not. If you have a shallow copy, then changing x will change y and vice versa.
When you need a deep copy of dynamic memory allocated objects, then you must write your own copy constructor, assignment operator, and destructor.
Yet, the answer is no. You don't need to, if it's an object. When a data member is an object (as opposed to a pointer to an object), then it's assumed the object handles its own memory allocation through its own copy constructor, assignment operator, and destructor.
You don't allocate memory for the string using "new", so you don't have to worry about deallocating using "delete" or copying it.
In fact, this is the behavior we want out of objects. When you make an object a data member, you really shouldn't have to worry about whether it has dynamic memory allocation or not. If you had to manage the memory of strings, then that would simply make programming that much more difficult. An object should take care of itself. If it doesn't, it hasn't been correctly implemented.
Here's a classic example of what happens. You have an object called x of type Foo. Assume x has a data member which is a pointer to a dynamically allocated array. You pass x to a function by value.
Normally, you wouldn't pass by value since it's far more efficient to pass by either reference or const reference (which only have to copy pointers). However, it's not that uncommon to pass by value (for example, lots of people pass C++ strings by value).
In any case, when an object is passed by value, the copy constructor is called. Since the copy constructor wasn't implemented, then the implicit version is used. If you refer to the list earlier on, you will recall that when an implicit copy constructor is called on pointers, the address is copied.
This is called a shallow copy. If the parameter is y, then y is a shallow copy of x, which means that y's pointer points to the same dynamic memory that x's does. Both point to the same dynamic array.
The copy constructor is called when the argument is passed. However, notice that there was no call to "new". Thus, the object is created without an explicit call to the constructor. Thus, there's no explicit call to the destructor. The destructor will get called when the object leaves the function scope.
Since you wrote the destructor, this will call the "delete" on the pointer in y. But recall that this is the same pointer as in x since a shallow copy was made. Thus, the array is deleted, and x now points to garbage memory.
Worse, the user usually doesn't suspect this will happen. They either get a mysterious core dump later on, or wonder why the data is mysteriously changing, before core dumping.
These are the helper functions you should write:
Let's consider an example of how they should be written.
class Array {
int size;
int *arr; // dyn alloc
public:
...
};
Here are all three helper methods.
void Array::init() {
arr = NULL;
size = 0;
}
// Note: NO deallocation
void Array::copy( const Array & other ) {
size = other.size;
if ( size > 0 )
{
arr = new int[ other.size ];
for ( int i = 0; i < size; i++ )
arr[ i ] = other.arr[ i ];
}
else
arr = NULL;
}
void Array::freeMem() {
delete [] arr;
}
The last two methods are the important one. init() is
something of a frill. Once you have these methods, you can implement
the three methods without thinking too hard.
// default
void Array::Array() {
init();
}
// copy constructor
void Array::Array( const Array & other ) {
init();
copy( other );
}
// assignment operator
Array & Array::operator=( const Array & other ) {
if ( this != &other ) {
freeMem();
init();
copy( other );
}
return *this;
}
By using helper functions, you can see the difference between
the copy constructor and assignment operator more clearly. In particular,
a copy constructor doesn't free any memory, while the assignment
operator does.
Also, the assignment operator checks for self assignment, while the copy constructor does not (since the object is just being constructed, it can hardly be said to exist, so how could one copy it). Furthermore, the assignment operator deallocates memory, and returns a pointer to the "this" object.
Actually, the init() part is not nearly as silly as I said before. In fact, it may be more important than you expect. When attempting to copy, often, you need to make sure the data members have been initialized to something reasonable. This is particularly true when trying to copy linked lists. You get core dumps if the data members are not initialized, when copying linked lists, since the pointer is garbage.
// copy constructor
void Array::Array( const Array & other ) {
*this = other; // calling op= AVOID
}
Some people think: "the overloaded operator = is so easy to write,
I will use that". It's not a bad idea, but recall the differences.
With a copy constructor, you know that the object is being constructed
for the first time, so there is no memory to be deallocated.
With an assignment operator, the object has been in existence for
a while, and therefore may deallocate memory. Thus, the assignment
operator does both deallocation and copying while the copy constructor
only copies.
For the copy constructor to call the assignment operator, this suggests that you want memory to be deallocated and then copied, which suggests a misunderstanding.
There's also the potential for an infinite loop. This occurs when you accidentally make the return type of the assignment operator an object, rather than an object reference.
By returning an object, you are returning by value. This calls the copy constructor, which calls the assignment operator, which cal;s the copy constructor, etc.
The following are other pitfalls.