Chapter 27

Coping with the Copy Constructor

In This Chapter

arrow Letting C++ make copies of an object

arrow Creating your own copy constructor

arrow Making copies of data members

arrow Avoiding making copies altogether

The constructor is a special function that C++ invokes when an object is created in order to allow the class to initialize the object to a legal state. Chapter 25 introduces the concept of the constructor. Chapter 26 demonstrates how to create constructors that take arguments. This chapter concludes the discussion of constructors by examining a particular constructor known as the copy constructor.

Copying an Object

A copy constructor is the constructor that C++ uses to make copies of objects. It carries the name X::X(const X&), where X is the name of the class. That is, it’s the constructor of class X that takes as its argument a reference to an object of class X. I know that sounds pretty useless, but let me explain why you need a constructor like that on your team.

remember.eps A reference argument type like fn(X&) says, “pass a reference to the object” rather than “pass a copy of the object.” I discuss reference arguments in Chapter 23.

Think for a minute about the following function call:

  void fn(Student s)
{
    // ...whatever fn() does...
}

void someOtherFn()
{
    Student s;
    fn(s);
};

Here the function someOtherFn() creates a Student object and passes a copy of that object to fn().

remember.eps By default, C++ passes objects by value, meaning that it must make a copy of the object to pass to the functions it calls (refer to Chapter 23 for more).

Consider that creating a copy of an object means creating a new object — and that process, by definition, means invoking a constructor. But what would the arguments to that constructor be? Why, a reference to the original object. That, by definition, is the copy constructor.

The default copy constructor

C++ provides a default copy constructor that works most of the time. This copy constructor does a member-by-member copy of the source object to the destination object.

tip.eps A member-by-member copy is also known as a shallow copy for reasons that soon will become clear.

There are times when copying one member at a time is not a good thing, however. Consider the Student class from Chapter 26:

  class Student
{
  protected:
    char* pszName;
    int   nID;

  // ...other stuff...
};

Copying the int data member nID from one object to another is no problem. However, copying the pointer pszName from the source to the destination object could cause problems.

For example, what if pszName points to heap memory (which it almost surely does)? Now you have two objects that both point to the same block of memory on the heap. This is shown in Figure 27-1.

9781118823873-fg2701.tif

Figure 27-1: By default, C++ performs a member-by-member, “shallow” copy to create copies of objects, as when passing an object to a function.

When the copy of the Student object goes out of scope, the destructor for that class will likely delete the pszName pointer, thereby returning the block of memory to the heap, even though the original object is still using that memory. When the original object continues to use the now deleted block of memory the program is sure to crash with a bizarre — and largely misleading — error message.

Looking at an example

The following ShallowStudent program demonstrates how making a shallow copy can cause serious problems:

  //
//  ShallowStudent - this program demonstrates why the
//                   default shallow copy constructor
//                   isn't always the right choice.
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

class Student
{
  protected:
    char*  pszName;
    int    nID;

  public:
    Student(const char* pszNewName, int nNewID)
    {
        cout << "Constructing " << pszNewName << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
    }
   ~Student()
    {
        cout << "Destructing " << pszName << endl;
        delete[] pszName;
        pszName = nullptr;
    }

    // access functions
    const char* getName()
    {
        return pszName;
    }
    int getID()
    {
        return nID;
    }
};

void someOtherFn(Student s)
{
    // we don't need to do anything here
}

void someFn()
{
    Student student("Adam Laskowski", 1234);
    someOtherFn(student);

    cout << "The student's name is now "
         << student.getName() << endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    someFn();

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

This deceptively simple program contains a serious problem: The function main() does nothing more than call the function someFn(). This function creates a local student object and passes it by value to the function someOtherFn(). This second function does nothing except return to the caller. The someFn() function then displays the name of the student and returns to main().

The output from the program shows some interesting results:

  Constructing Adam Laskowski
Destructing Adam Laskowski
The student's name is now X$±
Destructing X$±
Press Enter to continue …

The first message comes from the Student constructor as the student object is created at the beginning of someFn(). No message is generated by the default copy constructor that’s called to create the copy of Student for someOtherFn(). The destructor message is invoked at the end of someOtherFn() when the local object s goes out of scope.

The output message in someFn() shows that the object is now messed up as the memory allocated by the Student constructor to hold the student’s name has been returned to the heap. The subsequent destructor that’s invoked at the end of someFn() verifies that things are amiss.

warning.eps This type of error is normally fatal (to the program, not the programmer). The only reason this program didn’t crash is that it was about to stop anyway.

Creating a Copy Constructor

Classes that allocate resources in their constructor should normally include a copy constructor to create copies of these resources. For example, the Student copy constructor should allocate another block of memory off the heap for the name, and copy the original object’s name into this new block. This is shown in Figure 27-2.

9781118823873-fg2702.tif

Figure 27-2: A class that allocates resources in the constructor requires a copy constructor that performs a so-called deep copy of the source object.

tip.eps Allocating a new block of memory and copying the contents of the original into this new block is known as creating a deep copy (as opposed to the default shallow copy).

The following DeepStudent program includes a copy constructor that performs a deep copy of the student object:

  //
//  DeepStudent - this program demonstrates how a copy
//                constructor that performs a deep copy
//                can be used to solve copy problems
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;

class Student
{
  protected:
    char*  pszName;
    int    nID;

  public:
    Student(const char* pszNewName, int nNewID)
    {
        cout << "Constructing " << pszNewName << endl;
        int nLength = strlen(pszNewName) + 1;
        pszName = new char[nLength];
        strcpy(pszName, pszNewName);
        nID = nNewID;
    }
    Student(const Student& s)
    {
        cout<<"Constructing copy of "<< s.pszName << endl;

        int nLength = strlen(s.pszName) + 25;
        this->pszName = new char[nLength];
        strcpy(this->pszName, "Copy of ");
        strcat(this->pszName, s.pszName);
        this->nID = s.nID;
    }

   ~Student()
    {
        cout << "Destructing " << pszName << endl;
        delete[] pszName;
        pszName = nullptr;
    }

    // access functions
    const char* getName()
    {
        return pszName;
    }
    int getID()
    {
        return nID;
    }
};

void someOtherFn(Student s)
{
    // we don't need to do anything here
}

void someFn()
{
    Student student("Adam Laskowski", 1234);
    someOtherFn(student);

    cout << "The student's name is now "
         << student.getName() << endl;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    someFn();

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

This program is identical to its ShallowStudent cousin except for the addition of the copy constructor Student(const Student&), but what a difference it makes in the output from the program:

  Constructing Adam Laskowski
Constructing copy of Adam Laskowski
Destructing Copy of Adam Laskowski
The student's name is now Adam Laskowski
Destructing Adam Laskowski
Press Enter to continue …

The first message is output by the Student(const char*, int) constructor that’s invoked when the student object is created at the beginning of someFn(). The second message comes from the copy constructor Student(const Student&) that’s invoked to create the copy of student as part of the call to SomeOtherFn().

This constructor first allocates a new block of heap memory for the pszName of the copy. It then copies the string Copy of into this field before concatenating the student’s name in the next line.

tip.eps You would normally make a true copy of the name and not tack Copy of onto the front; I do so for instructional reasons.

The destructor that’s invoked as s goes out of scope at the end of someOtherFn() is now clearly returning the copy of the name to the heap and not the original string. This is verified back in someFn() when the student’s name is intact (as you would expect). Finally, the destructor at the end of someFn() returns the original string to the heap.

Avoiding Copies

Passing arguments by value is just one of several reasons that C++ invokes a copy constructor to create temporary copies of your object. You may be wondering, “Doesn’t all this creating and deleting copies of objects take time?” The obvious answer is, “You bet!” Is there some way to avoid creating copies?

Well, one way is not to pass objects by value; instead, you can pass the address of the object. There wouldn’t be a problem if someOtherFn() were declared as follows:

  // the following does not cause a copy to be created
void someOtherFn(const Student *pS)
{
    // ...whatever goes here...
}
void someFn()
{
    Student student("Adam Laskowski", 1234);
    someOtherFn(&student);
}

This is faster because a single address is smaller than an entire Student object, but it also avoids the need to allocate memory off the heap for holding copies of the student’s name.

technicalstuff.eps You can get the same effect using reference arguments, as in the following:

  // the following function doesn't create a copy either
void someOtherFn(const Student& s)
{
    // ...whatever you want to do...
}

void someFn()
{
     Student student("Adam Laskowski", 1234);
     someOtherFn(student);
}

See Chapter 23 for a refresher on referential arguments.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.74.211