11.4 Classes and Dynamic Arrays

With all appliances and means to boot.

WILLIAM SHAKESPEARE, King Henry IV, Part III

A dynamic array can have a base type that is a class. A class can have a member variable that is a dynamic array. You can combine the techniques you learned about classes and the techniques you learned about dynamic arrays in just about any way. There are a few more things to worry about when using classes and dynamic arrays, but the basic techniques are the ones that you have already used. Let’s start with an example.

Programming Example A String Variable Class

In Chapter 8 we showed you how to define array variables to hold C strings. In the previous section you learned how to define dynamic arrays so that the size of the array can be determined when your program is run. In this example we will define a class called StringVar whose objects are string variables. An object of the class StringVar will be implemented using a dynamic array whose size is determined when your program is run. So objects of type StringVar will have all the advantages of dynamic arrays, but they will also have some additional features. We will define StringVar’s member functions so that if you try to assign a string that is too long to an object of type StringVar, you will get an error message. The version we define here provides only a small collection of operations for manipulating string objects. In Programming Project 1 you are asked to enhance the class definition by adding more member functions and overloaded operators.

Since you could use the standard class string, as discussed in Chapter 8, you do not really need the class StringVar, but it will be a good exercise to design and code it.

The definition for the type StringVar is given in Display 11.11. One constructor for the class StringVar takes a single argument of type int. This argument determines the maximum allowable length for a string value stored in the object. A default constructor creates an object with a maximum allowable length of 100. Another constructor takes an array argument that contains a C string of the kind discussed in Chapter 8. Note that this means the argument to this constructor can be a quoted string. This constructor initializes the object so that it can hold any string whose length is less than or equal to the length of its argument, and it initializes the object’s string value to a copy of the value of its argument. For the moment, ignore the constructor that is labeled Copy constructor. Also ignore the member function named ~StringVar. Although it may look like one, ~StringVar is not a constructor. We will discuss these two new kinds of member functions in later subsections. The meanings of the remaining member functions for the class StringVar are straight forward.

Display 11.11 Program Using the StringVar Class

An illustration shows a program using the “StringVar Class.”

Sample Dialogue

What is your name?
Kathryn Janeway
We are Borg
We will meet again Kathryn Janeway
End of demonstration

A simple demonstration program is given in Display 11.11. Two objects, yourName and ourName, are declared within the definition of the function conversation. The object yourName can contain any string that is maxNameSize or fewer characters long. The object ourName is initialized to the string value "Borg" and can have its value changed to any other string of length 4 or less.

As we indicated at the beginning of this subsection, the class is implemented using a dynamic array. The implementation is shown in Display 11.12. When an object of type StringVar is declared, a constructor is called to initialize the object. The constructor uses the new operator to create a new dynamic array of characters for the member variable value. The string value is stored in the array value as an ordinary string value, with '\0' used to mark the end of the string. Notice that the size of this array is not determined until the object is declared, at which point the constructor is called and the argument to the constructor determines the size of the dynamic array. As illustrated in Display 11.11, this argument can be a variable of type int. Look at the declaration of the object yourName in the definition of the function conversation. The argument to the constructor is the call-by-value parameter maxNameSize. Recall that a call-by-value parameter is a local variable, so maxNameSize is a variable. Any int variable may be used as the argument to the constructor in this way.

Display 11.12 Implementation of StringVar

An illustration shows a code segment illustrating the implementation of “StringVar.”

The implementation of the member functions length, input_line, and the overloaded output operator << are all straightforward. In the next few subsections we discuss the function ~StringVar and the constructor labeled Copy constructor.

Destructors

There is one problem with dynamic variables. They do not go away unless your program makes a suitable call to delete. Even if the dynamic variable was created using a local pointer variable and the local pointer variable goes away at the end of a function call, the dynamic variable will remain unless there is a call to delete. If you do not eliminate dynamic variables with calls to delete, they will continue to occupy memory space, which may cause your program to abort because it used up all the memory in the freestore. Moreover, if the dynamic variable is embedded in the implementation of a class, the programmer who uses the class does not know about the dynamic variable and cannot be expected to perform the call to delete. In fact, since the data members are normally private members, the programmer normally cannot access the needed pointer variables and so cannot call delete with these pointer variables. To handle this problem, C++ has a special kind of member function called a destructor.

A destructor is a member function that is called automatically when an object of the class passes out of scope. This means that if your program contains a local variable that is an object with a destructor, then when the function call ends, the destructor is called automatically. If the destructor is defined correctly, the destructor calls delete to eliminate all the dynamic variables created by the object. This may be done with a single call to delete or it may require several calls to delete. You might also want your destructor to perform some other cleanup details as well, but returning memory to the freestore is the main job of the destructor.

The member function ~StringVar is the destructor for the class StringVar shown in Display 11.11. Like a constructor, a destructor always has the same name as the class it is a member of, but the destructor has the tilde symbol,~, at the beginning of its name (so you can tell that it is a destructor and not a constructor). Like a constructor, a destructor has no type for the value returned, not even the type void. A destructor has no parameters. Thus, a class can have only one destructor; you cannot overload the destructor for a class. Otherwise, a destructor is defined just like any other member function.

Notice the definition of the destructor ~StringVar given in Display 11.12. ~StringVar calls delete to eliminate the dynamic array pointed to by the member pointer variable value. Look again at the function conversation in the sample program shown in Display 11.11. The local variables yourName and ourName both create dynamic arrays. If this class did not have a destructor, then after the call to conversation has ended, these dynamic arrays would still be occupying memory, even though they are useless to the program. This would not be a problem here because the sample program ends soon after the call to conversation is completed; but if you wrote a program that made repeated calls to functions like conversation, and if the class StringVar did not have a suitable destructor, then the function calls could consume all the memory in the freestore and your program would then end abnormally.

Copy Constructors

A copy constructor is a constructor that has one parameter that is of the same type as the class. The one parameter must be a call-by-reference parameter, and normally the parameter is preceded by the const parameter modifier, so it is a constant parameter. In all other respects, a copy constructor is defined in the same way as any other constructor and can be used just like other constructors.

For example, a program that uses the class StringVar defined in Display 11.11 might contain the following:

StringVar line(20), motto("Constructors can help.");
cout << "Enter a string of length 20 or less:\n";
line.inputLine(cin);
StringVar temp(line);//Initialized by the copy constructor.

The constructor used to initialize each of the three objects of type StringVar is determined by the type of the argument given in parentheses after the object’s name. The object line is initialized with the constructor that has a parameter of type int; the object motto is initialized by the constructor that has a parameter of type const char a[]. Similarly, the object temp is initialized by the constructor that has one argument of type const StringVar&. When used in this way, a copy constructor is being used just like any other constructor.

A copy constructor should be defined so that the object being initialized becomes a complete, independent copy of its argument. So, in the declaration

StringVar temp(line);

the member variable temp.value is not simply set to the same value as line.value; that would produce two pointers pointing to the same dynamic array. The definition of the copy constructor is shown in Display 11.12. Note that in the definition of the copy constructor, a new dynamic array is created and the contents of one dynamic array are copied to the other dynamic array. Thus, in the previous declaration, temp is initialized so that its string value is equal to the string value of line, but temp has a separate dynamic array. Thus, any change that is made to temp has no effect on line. This is called a deep copy. A shallow copy, discussed in Chapter 10, would only copy a reference to the same dynamic array in memory. A deep copy makes a new copy of any dynamic structures.

As you have seen, a copy constructor can be used just like any other constructor. A copy constructor is also called automatically in certain other situations. Roughly speaking, whenever C++ needs to make a copy of an object, it automatically calls the copy constructor. In particular, the copy constructor is called automatically in three circumstances: (1) when a class object is declared and is initialized by another object of the same type, (2) when a function returns a value of the class type, and (3) whenever an argument of the class type is “plugged in” for a call-by-value parameter. In this case, the copy constructor defines what is meant by “plugging in.”

To see why you need a copy constructor, let’s see what would happen if we did not define a copy constructor for the class StringVar. Suppose we did not include the copy constructor in the definition of the class StringVar and suppose we used a call-by-value parameter in a function definition, for example:

void showString(StringVar theString)
{
    cout << "The string is: "
         << theString << endl;
}

Consider the following code, which includes a function call:

StringVar greeting("Hello");
showString(greeting);
cout << "After call: " << greeting << endl;

Assuming there is no copy constructor, things proceed as follows: When the function call is executed, the value of greeting is copied to the local variable theString, so theString.value is set equal to greeting.value. But these are pointer variables, so during the function call, theString.value and greeting.value point to the same dynamic array, as follows:

Arrows point from two side-by-side boxes, labeled “greeting.value” and “theString.value”, up to a box containing the line “Hello” (including quotation marks).

When the function call ends, the destructor for StringVar is called to return the memory used by theString to the freestore. The definition of the destructor contains the following statement:

delete [] value;

Since the destructor is called with the object theString, this statement is equivalent to:

delete [] theString.value;

which changes the picture to the following:

Arrows point from two side-by-side boxes, labeled “greeting.value” and “theString.value”, up to an irregularly shaped outline containing the line “Undefined”.

Since greeting.value and theString.value point to the same dynamic array, deleting theString.value is the same as deleting greeting.value. Thus, greeting.value is undefined when the program reaches the statement

cout << "After call: " << greeting << endl;

This cout statement is therefore undefined. The cout statement may by chance give you the output you want, but sooner or later the fact that greeting.value is undefined will produce problems. One major problem occurs when the object greeting is a local variable in some function. In this case the destructor will be called with greeting when the function call ends. That destructor call will be equivalent to

delete [] greeting.value;

But, as we just saw, the dynamic array pointed to by greeting.value has already been deleted once, and now the system is trying to delete it a second time. Calling delete twice to delete the same dynamic array (or other variable created with new) can produce a serious system error that can cause your program to crash.

That was what would happen if there were no copy constructor. Fortunately, we included a copy constructor in our definition of the class StringVar, so the copy constructor is called automatically when the following function call is executed:

StringVar greeting("Hello");
showString(greeting);

The copy constructor defines what it means to “plug in” the argument greeting for the call-by-value parameter theString, so that now the picture is as follows:

Arrows point from each of two side-by-side boxes, labeled “greeting.value” and “theString.value”, up to separate boxes, each containing the line “Hello” (including quotation marks).

Thus, any change that is made to theString.value has no effect on the argument greeting, and there are no problems with the destructor. If the destructor is called for theString and then called for greeting, each call to the destructor deletes a different dynamic array.

When a function returns a value of a class type, the copy constructor is called automatically to copy the value specified by the return statement. If there is no copy constructor, then problems similar to what we described for value parameters will occur.

If a class definition involves pointers and dynamically allocated memory using the new operator, then you need to include a copy constructor. Normally your copy constructor should perform a deep copy of the dynamic memory structures. Classes that do not involve pointers or dynamically allocated memory do not need a copy constructor. A shallow copy, performed by default, will generally suffice.

Contrary to what you might expect, the copy constructor is not called when you set one object equal to another using the assignment operator.2 However, if you do not like what the default assignment operator does, you can redefine the assignment operator in the way described in the subsection entitled “Overloading the Assignment Operator.”

Self-Test Exercises

  1. If a class is named MyClass and it has a constructor, what is the constructor named? If MyClass has a destructor, what is the destructor named?

  2. Suppose you change the definition of the destructor in Display 11.12 to the following. How would the sample dialogue in Display 11.11 change?

    StringVar::~StringVar()
    {
        cout << endl
             << "Good-bye cruel world! The short life of\n"
             << "this dynamic array is about to end.\n"; 
        delete [] value;
    }
    

  3. The following is the first line of the copy constructor definition for the class StringVar. The identifier StringVar occurs three times and means something slightly different each time. What does it mean in each of the three cases?

    StringVar::StringVar(const StringVar& stringObject)
    
  4. Answer these questions about destructors.

    1. What is a destructor and what must the name of a destructor be?

    2. When is a destructor called?

    3. What does a destructor actually do?

    4. What should a destructor do?

Overloading the Assignment Operator

Suppose string1 and string2 are declared as follows:

StringVar string1(10), string2(20);

The class StringVar was defined in Displays 11.11 and 11.12. If string2 has somehow been given a value, then the following assignment statement is defined, but its meaning may not be what you would like it to be:

string1 = string2;

As usual, this predefined version of the assignment statement copies the value of each of the member variables of string2 to the corresponding member variables of string1, so the value of string1.maxLength is changed to be the same as string2.maxLength and the value of string1.value is changed to be the same as string2.value. But this can cause problems with string1 and probably even cause problems for string2.

The member variable string1.value contains a pointer, and the assignment statement sets this pointer equal to the same value as string2.value. Thus, both string1.value and string2.value point to the same place in memory. If you change the string value in string1, you will therefore also change the string value in string2. If you change the string value in string2, you will change the string value in string1.

In short, the predefined assignment statement does not do what we would like an assignment statement to do with objects of type StringVar. Using the predefined version of the assignment operator with the class StringVar can only cause problems. The way to fix this is to overload the assignment operator = so that it does what we want it to do with objects of the class StringVar.

The assignment operator cannot be overloaded in the way we have overloaded other operators, such as << and +. When you overload the assignment operator, it must be a member of the class; it cannot be a friend of the class. To add an overloaded version of the assignment operator to the class StringVar, the definition of StringVar should be changed to the following:

class StringVar
{
public:
    void operator =(const StringVar& rightSide);
    //Overloads the assignment operator = to copy a string
    //from one object to another.
    <The rest of the definition of the class can be the same as in
    Display 11.11.>

The assignment operator is then used just as you always use the assignment operator. For example, consider the following:

string1 = string2;

In this call, string1 is the calling object and string2 is the argument to the member operator =.

The definition of the assignment operator can be as follows:

//The following is acceptable, but
//we will give a better definition:
void StringVar::operator =(const StringVar& rightSide)
{
    int newLength = strlen(rightSide.value);
    if ((newLength) > maxLength)
        newLength = maxLength;
    for (int i = 0; i < newLength; i++)
        value[i] = rightSide.value[i];
    value[newLength] = '\0';
}

Notice that the length of the string in the object on the right side of the assignment operator is checked. If it is too long to fit in the object on the left side of the assignment operator (which is the calling object), then only as many characters as will fit are copied to the object receiving the string. But suppose you do not want to lose any characters in the copying process. To fit in all the characters, you can create a new, larger dynamic array for the object on the left-hand side of the assignment operator. You might try to redefine the assignment operator as follows:

//This version has a bug:
void StringVar::operator =(const StringVar& rightSide)
{
    delete [] value;
    int newLength = strlen(rightSide.value);
    maxLength = newLength;
    value = new char[maxLength + 1];
    for (int i = 0; i < newLength; i++)
        value[i] = rightSide.value[i];
    value[newLength] = '\0';
}

This version has a problem when used in an assignment with the same object on both sides of the assignment operator, like the following:

myString = my_string;

When this assignment is executed, the first statement executed is

delete [] value;

But the calling object is myString, so this means

delete [] myString.value;

So, the string value in myString is deleted and the pointer myString.value is undefined. The assignment operator has corrupted the object myString, and this run of the program is probably ruined.

One way to fix this bug is to first check whether there is sufficient room in the dynamic array member of the object on the left-hand side of the assignment operator and to delete the array only if extra space is needed. Our final definition of the overloaded assignment operator does just such a check:

//This is our final version:
void StringVar::operator =(const StringVar& rightSide)
{
    int newLength = strlen(rightSide.value);
    if (newLength > maxLength)
    {
        delete [] value;
        maxLength = newLength;
        value = new char[maxLength + 1];
    }
    for (int i = 0; i < newLength; i++)
        value[i] = rightSide.value[i];
    value[newLength] = '\0';
}

For many classes, the obvious definition for overloading the assignment operator does not work correctly when the same object is on both sides of the assignment operator. You should always check this case and be careful to write your definition of the overloaded assignment operator so that it also works in this case.

Self-Test Exercise

    1. Explain carefully why no overloaded assignment operator is needed when the only data consists of built-in types.

    2. Same as part (a) for a copy constructor.

    3. Same as part (a) for a destructor.