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.
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.
StringVar
ClassSample 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.
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.
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.
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:
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:
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:
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.”
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?
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;
}
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)
Answer these questions about destructors.
What is a destructor and what must the name of a destructor be?
When is a destructor called?
What does a destructor actually do?
What should a destructor do?
Overloading = and == for a Class
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.>
Calling object for =
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.
Explain carefully why no overloaded assignment operator is needed when the only data consists of built-in types.
Same as part (a) for a copy constructor.
Same as part (a) for a destructor.