Home CPSC 340

Dynamic Memory Allocation Continued

Copy Constructors

Suppose we used our new String class to write this program:


int main( ) {
    /* create one String object */
    String name = "Bob Jones";

    /* create another one as a copy of the first */
    String name2 = name;
    return 0;
}

This program looks innocent enough, but it crashes!

The reason for this is something called a copy constructor. A copy constructor is used to make an object that is a copy of another one, as in our flawed example above. If a class does not provide a copy constructor, the compiler gives it one by default which copies each element over.

When the default copy constructor copies over the pointer from the first object to the second one, they end up both pointing to the same memory location. Then they both try to de-allocate that address in their destructor which is an error.

To fix this issue, we need to provide our own copy constructor. They have this form:


class Object {

  // the copy constructor
  Object(const Object& other) {

  }

};

The copy constructor is just a regular constructor which takes an object of the same type as a parameter. The parameter must be pass by const reference.

How can we add a copy constructor to String?


String(const String& other) {
    size = other.size;
    array = new char[size]; 
    for (int i = 0; i < size; i++) {
        array[i] = other.array[i];
    }
}

Overloading Assignment

The copy constructor is used when setting up a new object to another one. Unfortunately, it doesn't work if we are setting up an existing object to another one, as this example shows:


int main( ) {
    // create one String object
    String name = "Bob Jones";

    // create another one that's initially empty
    String name2;
   
    // then copy it over
    name2 = name;

    return 0;
}

Another crash!

To fix this one, we need to overload the assignment operator. The default assignment operator works the same way as the default copy constructor, which means it too must be replaced.

Like any operator, its name is operator, followed by the operator sign:


class Object {

  Object& operator=(const Object& other) {

  }

};

The assignment operator should always return the object itself so that chains of assignment will work. "this" when used in a class is a pointer to the object itself, so we can return the object itself by returning "*this".

Also note that since operator= is used on existing objects, it must first clear up any memory that was being used by the object previously.

How can we implement an assignment operator for String?


String operator=(const char cstring[]) {
    delete [] array;

    size = strlen(cstring);
    array = new char[size]; 
    for (int i = 0; i < size; i++) {
        array[i] = cstring[i];
    }

    return *this;
}

Self Assignment

One more caveat of the assignment operator is that, by default, it does not work well if we assign an object to itself like so:

String name = "Bob";

name = name;

If we did this, what would happen is the assignment operator would de-allocate its own array - which is the same array of the "other" object! It may seem implausible that anyone would do self-assignment with our objects, but it can be done indirectly as follows:


void someFunction(String& one, String& two) {
    one = two;    // looks fine
}

...

String name;
someFunction(name, name);   // causes indirect self-assignment

Also, you should write code that won't crash or cause subtle bugs no matter what silly thing somebody does with it. Fortunately, self-assignment can be handled easily by adding the following line to the top of operator=


if(this == &other) {
    return *this;
}
This works by checking if "this", the address of the current object, is the same as the address of the other object. If so, the objects are the same, so we should just return the *this object (as they are already equal)

The Rule of Three

In general, if your object needs a destructor, a copy constructor or an assignment operator, then you should almost certainly have all three. Any time an object allocates memory, or other resources, it should provide all three of these functions.

This is called the "Rule of Three" in C++

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.