Tải bản đầy đủ - 0 (trang)
Chapter 11. Constructors and Destructors: Potential Trouble

Chapter 11. Constructors and Destructors: Potential Trouble

Tải bản đầy đủ - 0trang

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



sent to objects.

The programmer-defined types discussed earlier are inherently numeric. Even though they have a

more complex internal structure than integers and floating point numbers do, they can be handled

by the client code similar to integers and floating point numbers: They can be added, multiplied,

compared, and so on by the client code.

Notice the sloppiness of the previous statement. The first "they" at the beginning of the statement

denotes programmer-defined types. What do "integers and floating point numbers" denote? Since I

am comparing them to programmer-defined types, I am talking about integer and floating point

types, not integer and floating point variables. It would be better to say "built-in types" instead.

And what does the second "they" in the middle of the sentence mean? Probably the same thing as

the first "they" in the sentence, that is, programmer-defined types. But this is not the case, because

here I am talking about handling them by the client code! The client code does not handle

programmer-defined types; it handles objects of programmer-defined types. It is object instances,

or variables, that are multiplied, compared, and so on. I make this point because I think that you

have learned enough about classes and objects to be sensitive to the loose language in objectoriented discussions and to avoid it if possible.

In other words, objects of programmer-defined numeric classes can be handled by the client code

similar to variables of built-in types. This is why it makes perfect sense to support operator

overloading for them. The C++ principle of treating the instances of built-in types and programmerdefined classes equally works well for these classes. In this chapter, I am going to discuss

overloaded operators for classes whose objects cannot be added, multiplied, subtracted, or divided.

For example, class String can be designed to manage text in memory. Because of the nonnumeric

nature of such classes, overloaded operator functions for these classes look artificial. For example,

you can implement String concatenation using the overloaded addition operator or String

comparison using the overloaded equality operator. But you would be hard-pressed to come up

with a reasonable interpretation of multiplication or division for String objects. Nevertheless,

overloaded operator functions for nonnumeric classes are popular, and you should know how to

deal with them.

The important distinction of these nonnumeric classes is the variable amount of data that objects of

the same class can use. The objects of numeric classes always use the same amount of memory. In

class Rational, for example, there are always two data members, one for the numerator, another

for the denominator.

In class String, however, the amount of text that is stored in one object might be different from

the amount of text stored with another object. If the class reserves for each object the same (large

enough) amount of memory, the program has to deal with two unpleasant extremes¡Xthe waste of

memory (when the actual amount of text is less than the reserved amount of memory) and memory

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (602 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



overflow (when the object has to store too much text). These two dangers always haunt the

designers of classes that allocate the same amount of memory to each object.

C++ resolves this problem by allocating a fixed amount of memory to each object (either to the

heap or to the stack) according to the class description and then allocating additional memory to the

heap as required. This additional amount of heap memory changes from one object to another. It

might even change for an object during its lifetime. For example, a String object might receive

additional heap memory to accommodate text that is concatenated to the text currently in the object.

Dynamic management of heap memory entails the use of constructors and destructors. Their

unskilled use might negatively affect program performance. What is worse is that their use might

result in corruption of memory and loss of program integrity that is not known in any other

language but C++. Every C++ programmer should be aware of these dangers. This is why I

included these issues in the title of the chapter even though this chapter continues the discussion of

overloaded operator functions.

For simplicity of discussion, I will introduce necessary concepts for the fixed-sized class Rational

that you saw in Chapter 10. In this chapter I will apply these concepts to class String with

dynamic management of heap memory. As a result, you will hone your programming intuition

about relationships between object instances in the client code. You will see that the relationships

between objects are different from relationships between variables of built-in types, despite the

effort to treat them equally. In other words, you are in for a big surprise.

Make sure you do not skip the material in this chapter. The dangers related to the roles of

constructors and destructors in C++ are real, and you should know how to protect yourself, your

boss, and the users of your code.



More on Passing Objects by Value

Earlier, in Chapter 7, "Programming with C++ Functions," I argued against passing objects to

functions as value parameters or as pointer parameters and promoted passing parameters by

reference instead.

I explained that pass by reference is almost as simple as pass by value, but it is faster¡Xfor input

parameters that are not modified by the function. Pass by reference is as fast as pass by pointer, but

its syntax is much simpler¡Xfor output parameters that are modified by the function in the course of

its execution.

I also noticed that the syntax for pass by reference is exactly the same for both input and output

function parameters. This is why I suggested that you use the const modifier for input parameters,

indicating that the parameter does not change as the result of function execution. When you use no

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (603 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



modifiers, this should indicate that the parameter changes during function execution.

I also argued against returning object values from functions unless it is necessary for sending other

messages to the returned object (chain syntax in expressions).

With this approach, the pass by value should be limited to passing built-in types as input

parameters to functions and returning values of built-in types from functions. Why is this

acceptable for input values of built-in types? Passing them by pointer will add complexity and

could mislead the reader into believing that the parameter changes within the function. Passing

them by reference (with the const modifier) is not very difficult, but it adds a little bit of

complexity. Since they are small, passing them by reference has no performance advantages. This

is why the simplest way of passing parameters is appropriate for built-in types.

In the last chapter, you learned enough programming techniques to be able not only to discuss

advantages and disadvantages of different modes of passing parameters but also to see the actual

sequence of invocations.

Also on several occasions, I told you that initialization and assignment, even though they both use

the equal sign, are treated differently. In this section, I will use debugging code to demonstrate the

differences.

I will demonstrate both issues using the program in Listing 11.1, which contains a simplified (and

modified) class Rational from the last chapter with its test driver.

Of all Rational functions, I left only normalize(), show(), and operator+(). Notice that the

overloaded operator function operator+() is not a member function of class Rational; it is a

friend. This is why I was careful to say at the beginning of this paragraph, "of all Rational

functions," not "of all Rational member functions." I do this because I want to stress that a friend

function is, for all intents and purposes, a class member function. It is implemented in the same file

as are other member functions, it has the same access rights to class private members as do other

member functions, and it is useless for working with objects of any class other than class

Rational. It is only the invocation syntax that makes it different from member functions, but for

overloaded operators, the operator syntax is the same for member functions and friend functions.



Example 11.1. Example of passing object parameters by value.

#include

class Rational {

long nmr, dnm;

void normalize();

public:

Rational(long n=0, long d=1)



// private data

// private member function

// general, conversion, default



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (604 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



{ nmr = n; dnm = d;

this->normalize();

cout << " created: " << nmr << " " << dnm << endl; }

Rational(const Rational &r)

// copy constructor

{ nmr = r.nmr; dnm = r.dnm;

cout << " copied: " << nmr << " " << dnm << endl; }

void operator = (const Rational &r)

// assignment operator

{ nmr = r.nmr; dnm = r.dnm;

cout << " assigned: " << nmr << " " << dnm << endl; }

~Rational()

// destructor

{ cout << " destroyed: " << nmr << " " << dnm << endl; }

friend Rational operator + (const Rational x, const Rational y);

void show() const;

} ;

// end of class specification

void Rational::show() const

{ cout << " " << nmr << "/" << dnm; }

void Rational::normalize()

// private member function

{ if (nmr == 0) { dnm = 1; return; }

int sign = 1;

if (nmr < 0) { sign = -1; nmr = -nmr; }

// make both positive

if (dnm < 0) { sign = -sign; dnm = -dnm; }

long gcd = nmr, value = dnm;

// greatest common divisor

while (value != gcd) {

// stop when the GCD is found

if (gcd > value)

gcd = gcd - value; // subtract smaller from greater

else value = value - gcd; }

nmr = sign * (nmr/gcd); dnm = dnm/gcd; }

// make dnm positive

Rational operator + (const Rational x, const Rational y)

{ return Rational(y.nmr*x.dnm + x.nmr*y.dnm, y.dnm*x.dnm); }

int main()

{ Rational a(1,4), b(3,2), c;

cout << endl;

c = a + b;

a.show(); cout << " +"; b.show(); cout << " ="; c.show();

cout << endl << endl;

return 0;

}



In the general Rational constructor, I added the debugging printing statement. This statement

should fire each time a Rational object is created and initialized¡Xat the beginning of the main()

and within the operator+() function.

Rational::Rational(long n=0, long d=1)



// default values



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (605 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



{ nmr = n; dnm = d;

// initialize data

this->normalize();

cout << " created: " << nmr << " " << dnm << endl; }



I also added a copy constructor with the debugging printing statement. This statement fires when an

object of class Rational is initialized from the data members of another Rational object, for

example, when passing parameters by value to the operator+() function or when returning a

Rational object from this function.

Rational::Rational(const Rational &r)

// copy constructor

{ nmr = r.nmr; dnm = r.dnm;

// copy data members

cout << " copied: " << nmr << " " << dnm << endl; }



This constructor is called when Rational arguments are passed by value to the friend operator

function operator+(). Despite appearances, the copy constructor is not called when operator+()

returns the object value, since the general constructor with two arguments is called prior to

returning from the operator+() function.

The destructor does not have a meaningful job in the Rational class, and I added it only for the

sake of the debugging statement that fires when a Rational object is destroyed.

The most interesting function here is an overloaded assignment operator function. Its job is to copy

the data members of one Rational object into the data members of another Rational object. How

is its duty different from that of the copy constructor? The answer is that there is no difference, at

least at this stage. The return type is different¡Xthe copy constructor must not have the return type,

and the assignment operator, as most member functions, must have a return type. For simplicity, I

return void.

void Rational::operator = (const Rational &r)

// assignment

{ nmr = r.nmr; dnm = r.dnm;

// copy data

cout << " assigned: " << nmr << " " << dnm << endl; }



The overloaded assignment operator is a binary operator. How do I know this? First, it has one

parameter of the class type, and it is a member function, not a friend; as any member function with

one parameter, it operates on two objects: One is the message target and the other is the parameter.

The second explanation is from the syntax of the use of the assignment as an operator. The binary

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (606 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



operator is always written between the first and the second operand. When adding two operands,

write the first operand, the operator, and the second operand (e.g., a + b). When using the

assignment, write the first operand, the operator, and the second operand (e.g., a = b). In the

function call syntax, object a is the target of the message: In the assignment operator function

above, nmr and dnm belong to the target object a. The object b is the argument of this function call:

In the assignment operator function above, r.nmr and r.dnm belong to the actual argument b.

Hence the function call syntax for the assignment operator is a.operator= (b).

Because this operator returns void, it cannot support chain assignments in the client code, for

example, a = b = c. This expression is interpreted by the compiler as a= (b = c). This means

that the return value of the assignment b = c (or b.operator=(c)) is used as a parameter in the

assignment a.operator=(b.operator=(c)). For this expression to be valid, the assignment

operator should return the value of the class type (here, Rational), Since the assignment operator

was designed so that it returns void, the chain expression will be labeled by the compiler as a

syntax error. For our first look at the assignment operator, this is not important. The chain

assignment will be used later in the chapter.

The output of the program in Listing 11.1 is shown in Figure 11-1. The first three messages

"created" come from creation and initialization of three Rational objects in main(). The two

"copied" messages come from the data flow to the overloaded operator function operator+().

The next message "created" comes from the call to the Rational constructor in the body of the

function operator+().



Figure 11-1. Output for program in Listing 11.1.



All these calls to constructors take place at the beginning of the function execution. Next comes a

series of events that takes place when the execution reaches the closing brace of the function body

and local and temporary objects are destroyed. The first two "destroyed" messages occur when two

local copies of actual arguments (3/2 and 1/4) are destroyed, and the destructor is called for these

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (607 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



two objects. The object that contains the sum of parameters cannot be destroyed before it is used in

the assignment operator. The next message "assigned" comes from the call to the overloaded

assignment operator, and the message "destroyed" comes from the destructor for the object that was

created in the body of the function operator+(). The last three "destroyed" messages come from

the destructors that are called when the execution reaches the closing brace of main(), and objects

a, b, and c are destroyed. Since the copy constructor is not called, the message "copied" does not

appear in the output.

This sequence of events plays differently if two ampersand signs are added in the interface of the

operator+() function.

Rational operator + (const Rational &x, const Rational &y)

{ return Rational(y.nmr*x.dnm + x.nmr*y.dnm, y.dnm*x.dnm); }



// references



The requirement of consistency between different parts of code stands tall in C++ programming.

Here, I am changing the interface of a function prototype and updating the function declaration in

the class specification. (Again, it does not matter whether it is a member function or a friend

function.) In this case, failure to keep related parts of code consistent is not deadly¡Xthe compiler

would alert you that the code has syntax errors.

The results of the execution of program in Listing 11.1 with the operator+() above are shown in

Figure 11-2. You see that four function calls are missing: Two parameter objects are not created

and two parameter objects are not destroyed.



Figure 11-2. Output for program in Listing 11.1 and passing parameters by reference.



TIP

Avoid passing object instances as value parameters. This causes unnecessary function calls. Pass

parameters by reference, and label them as constant objects in the function interface (if

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (608 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



applicable).



Next, let me demonstrate the difference between initialization and assignment. In Listing 11.1,

variable c is assigned in the expression c = a + .b How do I know that it is assigned and not

initialized? Because there is no type name to the left of c. Its type is defined earlier at the

beginning of main(). In contrast, this version of main() creates and immediately initializes the

object c to the sum of a and b rather than creating and assigning to c in separate statements.

int main()

{ Rational a(1,4), b(3,2), c = a + b;

a.show(); cout << " +"; b.show(); cout << " ="; c.show();

cout << endl << endl;

return 0; }



Figure 11-3 shows the results of the execution of the program in Listing 11.1 with passing

parameters by reference and this main() function. You see that the assignment operator is not

called here. Neither is the copy constructor¡Xthe natural result of the switch from pass by value to

pass by reference.



Figure 11-3. Output for program in Listing 11.1, passing parameters by reference and

using object initialization rather than assignment.



Later, I will use a similar technique to demonstrate the difference between initialization and

assignment for the String class.

TIP

Distinguish between object initialization and object assignment. At initialization, a constructor is

called, and the assignment operator call is bypassed. At assignment, the assignment operator is

called, and the constructor call is bypassed.



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (609 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



These ideas about avoiding passing object parameters by value and distinguishing between

initialization and assignment are very important. Make sure that you are able to read the client code

and say, "Here a constructor is called, and here the assignment operator is called." Develop your

intuition to enable you to perform this type of analysis.



Operator Overloading for Nonnumeric Classes

As I noted in the introduction to this chapter, the extension of built-in operators to numeric classes

is natural. Overloaded operator functions for these classes are very similar to built-in operators.

Misinterpretation of their meaning by the client programmer or by the maintainer is not likely. The

idea of treating values of built-in types and programmer-defined types equally is a sound one that

lends itself to straightforward implementation.

Operators can be applied to the objects of nonmathematical classes as well, but the meaning of

addition, subtraction, and other operators might be stretched. This is similar to the story of icons for

command input in the graphical user interface.

In the beginning, there was the command line interface, and users had to type long commands with

parameters, keys, switches, and so on. Then there were menu bars with text entries. By selecting

the entry, the user was able to enter the command to be executed without having to type the whole

command. Then there were hot keys: By pressing the hot key combination, the user was able to

activate commands directly, without removing the hand from the keyboard and going through

several menus and submenus. Then there was the toolbar with command buttons: By clicking the

toolbar button, the user was able to activate a command without needing to know the hot key

combinations. The icons on the face of these command buttons were unambiguous and intuitively

clear: Open, Close, Cut, Print. When more and more icons were added, they became less and less

intuitive: New, Paste, Output, Execute, Go.

To help the user learn the icons, tool tip messages were added. The user interface has become more

complex; applications require more disk space, memory, and programming efforts; and users are

probably no better off now than they used to be with menus and hot keys. Similarly, we started with

operator overloading for numeric classes, and now we are going to use operator functions for

nonnumerical classes. This will require you to learn more rules, to write more code, and deal with

more complexity. And the client code might be better off using old-fashioned function calls rather

than modern overloaded operators.



The String Class

I will discuss a popular example of using overloaded operator functions for nonnumeric classes:

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (610 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



using the addition operator for text concatenation.

Let us consider a String class with two data members: a pointer to a dynamically allocated

character array and the integer with the maximum number of valid characters that can be inserted

into the dynamically allocated heap memory. Actually, the C++ Standard Library contains class

String (with the first letter in lowercase) that is designed to satisfy most requirements for text

manipulation. This is a great class to use. It is much more powerful than the class that I am going to

discuss here, but I cannot use class String for these examples because it is too complex, and

details would take away from the discussion of dynamic memory management and its

consequences.

The client code can create objects of the class in two ways, by specifying the maximum number of

valid characters and by specifying the text contents of the string. Specifying the number of

characters requires one integer parameter. Specifying the text contents also requires one parameter,

a character array. The types of these parameters are different, so they have to be used in different

constructors. Since each of these constructors has exactly one parameter of a nonclass type, which

they convert to a class value, they are called conversion constructors.

The first conversion constructor, with the parameter for the length of the string to allocate, has the

default argument value zero. If a String object is created using this default value (no parameters

are specified), then the length of the text allocated for the object is zero. In this case, the first

conversion constructor is used as a default constructor (e.g., String s;).

The second conversion constructor, with the character array as the parameter, does not have the

default argument value. It would not be difficult to give it the default value of, say, an empty string,

but then the compiler would have difficulty interpreting the function call String s¡Xdo I want to

call the first constructor with the default value of zero length, or do I want to call the second

constructor with the default value of an empty string?

The current contents of the string can be modified by the client code by calling the member

function modify() that specifies the new text contents of the target object. To access the contents

of the String object, the member function show() can be used. This function returns the pointer to

the heap memory allocated to the object. This pointer can be used by the client code to print the

contents of the string, compare it with other text, and so on. Listing 11.2 shows a program that

implements class String.



Example 11.2. Class String with dynamically allocated heap memory.

#include

using namespace std;

class String {

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (611 of 1187) [8/17/2002 2:57:58 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



char *str;

int len;

public:

String (int length=0);

String(const char*);

~String ();

void modify(const char*);

char* show() const;

} ;



// dynamically allocated char array



//

//

//

//

//



conversion/default constructor

conversion constructor

deallocate dynamic memory

change the array contents

return a pointer to the array



String::String(int length)

{ len = length;

str = new char[len+1];

if (str==NULL) exit(1);

str[0] = 0; }



// default size is 1

// test for success

// empty String of 0 length is ok



String::String(const char* s)

{ len = strlen(s);

str = new char[len+1];

if (str==NULL) exit(1);

strcpy(str,s); }



//

//

//

//



measure length of incoming text

allocate enough heap space

test for success

copy text into new heap memory



String::~String()

{ delete str; }

pointer!)



// return heap memory (not the



void String::modify(const char a[])

{ strncpy(str,a,len-1);

str[len-1] = 0; }



// no memory management here

// protect from overflow

// terminate String properly



char* String::show() const

{ return str; }



// not a good practice, but ok



int main()

{

String u("This is a test.");

String v("Nothing can go wrong.");

cout << " u = " << u.show() << endl;

cout << " v = " << v.show() << endl;

v.modify("Let us hope for the best.");

cout << " v = " << v.show() << endl;

strcpy(v.show(),"Hi there");

cout << " v = " << v.show() << endl;

return 0;

}



// result is ok

// result is ok

// input is truncated

// bad practice



Dynamic Management of Heap Memory

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (612 of 1187) [8/17/2002 2:57:58 PM]



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 11. Constructors and Destructors: Potential Trouble

Tải bản đầy đủ ngay(0 tr)

×