Core C++ A Software Engineering Approach phần 6 ppsx

120 321 0
Core C++ A Software Engineering Approach phần 6 ppsx

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm application. However, the use of overloaded operators is not uncommon. They are especially popular in C++ libraries, including the Standard Template Library (STL), and you have to understand what they do and how they are implemented. The comparison that I make between member functions and friend functions is very important. All too often we make design decisions on the basis of hearsay or arbitrary biases rather than from the point of view of goals of object-oriented programming. Make sure that you do not treat friend functions as X-rated material. Use them if they provide more flexibility for better implementation. But do not overuse them. Chapter 11. Constructors and Destructors: Potential Trouble Topics in this Chapter ϒΠ More on Passing Objects by Value ϒΠ Operator Overloading for Nonnumeric Classes ϒΠ More on the Copy Construction ϒΠ Overloading the Assignment Operator ϒΠ Practical Considerations: What to Implement ϒΠ Summary Overloaded operator functions give a new twist to object-oriented programming. Instead of concentrating on binding together data and operations and related ideas, we find ourselves busy with aesthetic considerations and the issues of equally treating built-in types and programmer- defined types by a C++ program. This chapter is a direct continuation of the previous chapter. In Chapter 10,"Operator Functions: Another Good Idea," I discussed the issues that are related to the design of numeric classes, such as classes Complex and Rational. Objects of these classes are object instances in their own right. All of the issues related to dealing with objects apply to them: class declaration, control of access to class members, design of member functions, object definition, object initialization, and messages file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (601 of 1187) [8/17/2002 2:57:58 PM] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 object- oriented 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 programmer- defined 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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 <iostream.h> class Rational { long nmr, dnm; // private data void normalize(); // private member function public: Rational(long n=0, long d=1) // general, conversion, default file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (604 of 1187) [8/17/2002 2:57:58 PM] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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) // references { return Rational(y.nmr*x.dnm + x.nmr*y.dnm, y.dnm*x.dnm); } 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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] Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com [...]...file://///Administrator/General%20English%20Learning/it2002-7 -6 /core. htm using the addition operator for text concatenation Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com 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... intimidate the maintainer rather than to help the maintainer understand the intent of the code developer One of the first high-level computer languages, APL (A Programming Language) was very complex It is still used, mostly for financial applications The character set of this language is so large that it needs a special keyboard Among other things, it includes powerful operations for array and matrix... data member that keeps the size of heap memory The first keeps the total size of heap memory allocated in the data member (the number of symbols to accommodate plus one) The second keeps the number of useful symbols as a data member and adds one to this value when characters are allocated on the heap I use the second approach, but I would be hard-pressed to explain why it is better than the first approach. .. 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... created on the // two data members plus 4 characters on the heap Here, an unnamed String object (pointed to by pointer p) gets an integer and a character pointer on the heap After the object is created, the constructor allocates four more characters on the heap and sets the pointer str to point to that memory This approach gives me the convenience of thinking that all objects of the same class are... construction, and Figure 11-4(b) shows the second phase of construction The rectangle represents the String object t with two data members, pointer str and integer len These data members might take the same amount of memory, but I am showing the pointer as a smaller rectangle to underscore the fact that it does not contain computational data The name of the object t and the names of data members str and len are... processing APL programmers love this language It is considered good taste to write a few lines of APL code, show it to a friend, and ask, "Guess what it means?" file://///Administrator/General%20English%20Learning/it2002-7 -6 /core. htm (61 6 of 1187) [8/17/2002 2:57:58 PM] file://///Administrator/General%20English%20Learning/it2002-7 -6 /core. htm I am far from suggesting that programmers with such a mindset... is not a serious limitation for the client programmers void operator += (const String s); // concatenate parameter to target object I know that it is not nice to pass objects by value, but I assume that there is no performance problem here After all, the object of type String has only two small data members, a character file://///Administrator/General%20English%20Learning/it2002-7 -6 /core. htm (61 9 of... printed and used as an rvalue in the function call to operator+=() I am doing this only because I know there is a problem with this implementation Clearly, the object has to have the same value that it had when it participated as an operand in the expression u+=v This is the conventional programming intuition, and it works in C++ most of the time¡Xbut not all the time, and you should develop an alternative... the formal parameter was called¡Xthe actual argument v was robbed of its heap memory The second bad thing happened when the client function main() was terminating, and the object v was going out of scope¡Xits heap memory was repeatedly deleted Actually, in C++, it is the repeated deleting of heap memory that is "an error." Deleting a NULL pointer is not an error This is "no operation." Some programmers . 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. http://www.simpopdf.com 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. http://www.simpopdf.com 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

Ngày đăng: 12/08/2014, 11:20

Mục lục

  • Part I: Introduction to Programming with C++

    • Chapter 1. Object-oriented approach: What's So Good About It?

    • Chapter 2. Getting Started Quickly: A Brief Overview of C++

    • Chapter 3. Working with C++ Data and Expressions

    • Chapter 5. Aggregation with Programmer-Defined Data Types

    • Chapter 6. Memory Management: the Stack and the Heap

    • Part II: Object-oriented programing with C++

      • Chapter 7. Programming With C++ Functions

      • Chapter 8. Object-Oriented Programming with Functions

      • Chapter 9. C++ Class as a Unit of Modularization

      • Chapter 10. Operator Functions: Another Good idea

      • Chapter 11. Constructors and Destructors: Potential Trouble

      • Part III: Object-Oriented Programming with Aggregation and Inheritance

        • Chapter 12. Composite Classes: Pitfalls and Advantages

        • Chapter 13. Similar Classes: How to Treat Them

        • Chapter 14. Choosing between Inheritance and Composition

        • Part IV: Advanced uses of C++

          • Chapter 15. Virtual Functions and other Advanced Uses of Inheritance

          • Chapter 16. Advanced Uses of Operator Overloading

          • Chapter 17. Templates: Yet Another Design Tool

          • Chapter 19. What We Have Learned

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan