Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 86 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
86
Dung lượng
350,97 KB
Nội dung
772 Thinking in C++ www.BruceEckel.com location as the existing iterator that you create it from, effectively making a bookmark into the container. The operator+= and operator-= member functions allow you to move an iterator by a number of spots, while respecting the boundaries of the container. The overloaded increment and decrement operators move the iterator by one place. The operator+ produces a new iterator that’s moved forward by the amount of the addend. As in the previous example, the pointer dereference operators are used to operate on the element the iterator is referring to, and remove( ) destroys the current object by calling the container’s remove( ) . The same kind of code as before ( a la the Standard C++ Library containers) is used for creating the end sentinel: a second constructor, the container’s end( ) member function, and operator== and operator!= for comparison. The following example creates and tests two different kinds of Stash objects, one for a new class called Int that announces its construction and destruction and one that holds objects of the Standard library string class. //: C16:TPStash2Test.cpp #include "TPStash2.h" #include " /require.h" #include <iostream> #include <vector> #include <string> using namespace std; class Int { int i; public: Int(int ii = 0) : i(ii) { cout << ">" << i << ' '; } ~Int() { cout << "~" << i << ' '; } operator int() const { return i; } friend ostream& operator<<(ostream& os, const Int& x) { return os << "Int: " << x.i; 16: Introduction to Templates 773 } friend ostream& operator<<(ostream& os, const Int* x) { return os << "Int: " << x->i; } }; int main() { { // To force destructor call PStash<Int> ints; for(int i = 0; i < 30; i++) ints.add(new Int(i)); cout << endl; PStash<Int>::iterator it = ints.begin(); it += 5; PStash<Int>::iterator it2 = it + 10; for(; it != it2; it++) delete it.remove(); // Default removal cout << endl; for(it = ints.begin();it != ints.end();it++) if(*it) // Remove() causes "holes" cout << *it << endl; } // "ints" destructor called here cout << "\n \n"; ifstream in("TPStash2Test.cpp"); assure(in, "TPStash2Test.cpp"); // Instantiate for String: PStash<string> strings; string line; while(getline(in, line)) strings.add(new string(line)); PStash<string>::iterator sit = strings.begin(); for(; sit != strings.end(); sit++) cout << **sit << endl; sit = strings.begin(); int n = 26; sit += n; for(; sit != strings.end(); sit++) cout << n++ << ": " << **sit << endl; } ///:~ For convenience, Int has an associated ostream operator<< for both an Int& and an Int* . 774 Thinking in C++ www.BruceEckel.com The first block of code in main( ) is surrounded by braces to force the destruction of the PStash<Int> and thus the automatic cleanup by that destructor. A range of elements is removed and deleted by hand to show that the PStash cleans up the rest. For both instances of PStash , an iterator is created and used to move through the container. Notice the elegance produced by using these constructs; you aren’t assailed with the implementation details of using an array. You tell the container and iterator objects what to do, not how. This makes the solution easier to conceptualize, to build, and to modify. Why iterators? Up until now you’ve seen the mechanics of iterators, but understanding why they are so important takes a more complex example. It’s common to see polymorphism, dynamic object creation, and containers used together in a true object-oriented program. Containers and dynamic object creation solve the problem of not knowing how many or what type of objects you’ll need. And if the container is configured to hold pointers to base-class objects, an upcast occurs every time you put a derived-class pointer into the container (with the associated code organization and extensibility benefits). As the final code in Volume 1 of this book, this example will also pull together various aspects of everything you’ve learned so far – if you can follow this example, then you’re ready for Volume 2. Suppose you are creating a program that allows the user to edit and produce different kinds of drawings. Each drawing is an object that contains a collection of Shape objects: //: C16:Shape.h #ifndef SHAPE_H #define SHAPE_H 16: Introduction to Templates 775 #include <iostream> #include <string> class Shape { public: virtual void draw() = 0; virtual void erase() = 0; virtual ~Shape() {} }; class Circle : public Shape { public: Circle() {} ~Circle() { std::cout << "Circle::~Circle\n"; } void draw() { std::cout << "Circle::draw\n";} void erase() { std::cout << "Circle::erase\n";} }; class Square : public Shape { public: Square() {} ~Square() { std::cout << "Square::~Square\n"; } void draw() { std::cout << "Square::draw\n";} void erase() { std::cout << "Square::erase\n";} }; class Line : public Shape { public: Line() {} ~Line() { std::cout << "Line::~Line\n"; } void draw() { std::cout << "Line::draw\n";} void erase() { std::cout << "Line::erase\n";} }; #endif // SHAPE_H ///:~ This uses the classic structure of virtual functions in the base class that are overridden in the derived class. Notice that the Shape class includes a virtual destructor, something you should automatically add to any class with virtual functions. If a container holds pointers or references to Shape objects, then when the virtual destructors are called for those objects everything will be properly cleaned up. 776 Thinking in C++ www.BruceEckel.com Each different type of drawing in the following example makes use of a different kind of templatized container class: the PStash and Stack that have been defined in this chapter, and the vector class from the Standard C++ Library. The “use”’ of the containers is extremely simple, and in general inheritance might not be the best approach (composition could make more sense), but in this case inheritance is a simple approach and it doesn’t detract from the point made in the example. //: C16:Drawing.cpp #include <vector> // Uses Standard vector too! #include "TPStash2.h" #include "TStack2.h" #include "Shape.h" using namespace std; // A Drawing is primarily a container of Shapes: class Drawing : public PStash<Shape> { public: ~Drawing() { cout << "~Drawing" << endl; } }; // A Plan is a different container of Shapes: class Plan : public Stack<Shape> { public: ~Plan() { cout << "~Plan" << endl; } }; // A Schematic is a different container of Shapes: class Schematic : public vector<Shape*> { public: ~Schematic() { cout << "~Schematic" << endl; } }; // A function template: template<class Iter> void drawAll(Iter start, Iter end) { while(start != end) { (*start)->draw(); start++; } } 16: Introduction to Templates 777 int main() { // Each type of container has // a different interface: Drawing d; d.add(new Circle); d.add(new Square); d.add(new Line); Plan p; p.push(new Line); p.push(new Square); p.push(new Circle); Schematic s; s.push_back(new Square); s.push_back(new Circle); s.push_back(new Line); Shape* sarray[] = { new Circle, new Square, new Line }; // The iterators and the template function // allow them to be treated generically: cout << "Drawing d:" << endl; drawAll(d.begin(), d.end()); cout << "Plan p:" << endl; drawAll(p.begin(), p.end()); cout << "Schematic s:" << endl; drawAll(s.begin(), s.end()); cout << "Array sarray:" << endl; // Even works with array pointers: drawAll(sarray, sarray + sizeof(sarray)/sizeof(*sarray)); cout << "End of main" << endl; } ///:~ The different types of containers all hold pointers to Shape and pointers to upcast objects of classes derived from Shape . However, because of polymorphism, the proper behavior still occurs when the virtual functions are called. Note that sarray , the array of Shape* , can also be thought of as a container. 778 Thinking in C++ www.BruceEckel.com Function templates In drawAll( ) you see something new. So far in this chapter, we have been using only class templates , which instantiate new classes based on one or more type parameters. However, you can as easily create function templates , which create new functions based on type parameters. The reason you create a function template is the same reason you use for a class template: You’re trying to create generic code, and you do this by delaying the specification of one or more types. You just want to say that these type parameters support certain operations, not exactly what types they are. The function template drawAll( ) can be thought of as an algorithm (and this is what most of the function templates in the Standard C++ Library are called). It just says how to do something given iterators describing a range of elements, as long as these iterators can be dereferenced, incremented, and compared. These are exactly the kind of iterators we have been developing in this chapter, and also – not coincidentally – the kind of iterators that are produced by the containers in the Standard C++ Library, evidenced by the use of vector in this example. We’d also like drawAll( ) to be a generic algorithm , so that the containers can be any type at all and we don’t have to write a new version of the algorithm for each different type of container. Here’s where function templates are essential, because they automatically generate the specific code for each different type of container. But without the extra indirection provided by the iterators, this genericness wouldn’t be possible. That’s why iterators are important; they allow you to write general-purpose code that involves containers without knowing the underlying structure of the container. (Notice that, in C++, iterators and generic algorithms require function templates in order to work.) You can see the proof of this in main( ) , since drawAll( ) works unchanged with each different type of container. And even more interesting, drawAll( ) also works with pointers to the beginning 16: Introduction to Templates 779 and end of the array sarray . This ability to treat arrays as containers is integral to the design of the Standard C++ Library, whose algorithms look much like drawAll( ) . Because container class templates are rarely subject to the inheritance and upcasting you see with “ordinary” classes, you’ll almost never see virtual functions in container classes. Container class reuse is implemented with templates, not with inheritance. Summary Container classes are an essential part of object-oriented programming. They are another way to simplify and hide the details of a program and to speed the process of program development. In addition, they provide a great deal of safety and flexibility by replacing the primitive arrays and relatively crude data structure techniques found in C. Because the client programmer needs containers, it’s essential that they be easy to use. This is where the template comes in. With templates the syntax for source-code reuse (as opposed to object- code reuse provided by inheritance and composition) becomes trivial enough for the novice user. In fact, reusing code with templates is notably easier than inheritance and composition. Although you’ve learned about creating container and iterator classes in this book, in practice it’s much more expedient to learn the containers and iterators in the Standard C++ Library, since you can expect them to be available with every compiler. As you will see in Volume 2 of this book (downloadable from www.BruceEckel.com ), the containers and algorithms in the Standard C++ Library will virtually always fulfill your needs so you don’t have to create new ones yourself. The issues involved with container-class design have been touched upon in this chapter, but you may have gathered that they can go 780 Thinking in C++ www.BruceEckel.com much further. A complicated container-class library may cover all sorts of additional issues, including multithreading, persistence and garbage collection. Exercises Solutions to selected exercises can be found in the electronic document The Thinking in C++ Annotated Solution Guide , available for a small fee from www.BruceEckel.com. 1. Implement the inheritance hierarchy in the OShape diagram in this chapter. 2. Modify the result of Exercise 1 from Chapter 15 to use the Stack and iterator in TStack2.h instead of an array of Shape pointers. Add destructors to the class hierarchy so you can see that the Shape objects are destroyed when the Stack goes out of scope. 3. Modify TPStash.h so that the increment value used by inflate( ) can be changed throughout the lifetime of a particular container object. 4. Modify TPStash.h so that the increment value used by inflate( ) automatically resizes itself to reduce the number of times it needs to be called. For example, each time it is called it could double the increment value for use in the next call. Demonstrate this functionality by reporting whenever an inflate( ) is called, and write test code in main( ) . 5. Templatize the fibonacci( ) function on the type of value that it produces (so it can produce long , float , etc. instead of just int ). 6. Using the Standard C++ Library vector as an underlying implementation, create a Set template class that accepts only one of each type of object that you put into it. Make a nested iterator class that supports the “end sentinel” concept in this chapter. Write test code for your Set in main( ) , and then substitute the Standard C++ Library set template to verify that the behavior is correct. 16: Introduction to Templates 781 7. Modify AutoCounter.h so that it can be used as a member object inside any class whose creation and destruction you want to trace. Add a string member to hold the name of the class. Test this tool inside a class of your own. 8. Create a version of OwnerStack.h that uses a Standard C++ Library vector as its underlying implementation. You may need to look up some of the member functions of vector in order to do this (or just look at the <vector> header file). 9. Modify ValueStack.h so that it dynamically expands as you push( ) more objects and it runs out of space. Change ValueStackTest.cpp to test the new functionality. 10. Repeat Exercise 9 but use a Standard C++ Library vector as the internal implementation of the ValueStack . Notice how much easier this is. 11. Modify ValueStackTest.cpp so that it uses a Standard C++ Library vector instead of a Stack in main( ) . Notice the run-time behavior: Does the vector automatically create a bunch of default objects when it is created? 12. Modify TStack2.h so that it uses a Standard C++ Library vector as its underlying implementation. Make sure that you don’t change the interface, so that TStack2Test.cpp works unchanged. 13. Repeat Exercise 12 using a Standard C++ Library stack instead of a vector (you may need to look up information about the stack , or hunt through the <stack> header file). 14. Modify TPStash2.h so that it uses a Standard C++ Library vector as its underlying implementation. Make sure that you don’t change the interface, so that TPStash2Test.cpp works unchanged. 15. In IterIntStack.cpp , modify IntStackIter to give it an “end sentinel” constructor, and add operator== and operator!= . In main( ) , use an iterator to move through [...]... templatizing class Stackso that it automatically multiply inherits from the contained class and from Object The generated Stack should accept and produce only pointers of the contained type Repeat Exercise 20 using vector instead of Stack Inherit a class StringVectorfrom vectorand redefine the push_back( )and operator[]member functions to accept and produce only string* (and perform the proper casting)... class: 790 Thinking in C+ + www.BruceEckel.com class Thing; is a class name declaration, and class Thing { is a class definition You can tell by looking at the single line in all cases whether it’s a declaration or definition And of course, putting the opening brace on the same line, instead of a line by itself, allows you to fit more lines on a page So why do we have so many other styles? In particular,... functionality to your interface 1 Explained to me by Andrew Koenig B: Programming Guidelines 8 01 18 Watch for switch statements or chained if-else clauses This is typically an indicator of type-check coding, which means you are choosing what code to execute based on some kind of type information (the exact type may not be obvious at first) You can usually replace this kind of code with inheritance... Don’t use private inheritance Although it’s in the language and seems to have occasional functionality, it introduces 804 Thinking in C+ + www.BruceEckel.com significant ambiguities when combined with run-time type identification Create a private member object instead of using private inheritance 33 If two classes are associated with each other in some functional way (such as containers and iterators),... see the line: int func(int a) { you immediately know it’s a definition because the line finishes with an opening brace, not a semicolon By using this approach, there’s no difference in where you place the opening parenthesis for a multi-line definition: int func(int a) { int b = a + 1; return b * 2; } and for a single-line definition that is often used for inlines: int func(int a) { return (a + 1) * 2;... and/or pass objects in as arguments 17 Don’t repeat yourself If a piece of code is recurring in many functions in derived classes, put that code into a single function in the base class and call it from the derived-class functions Not only do you save code space, you provide for easy propagation of changes You can use an inline function for efficiency Sometimes the discovery of this common code will add... object (feel free to change the name of the class to something more appropriate) Templatize the IntArray class in IostreamOperatorOverloading.cpp from Chapter 12 , templatizing both the type of object that is contained and the size of the internal array Turn ObjContainerin NestedSmartPointer.cpp from Chapter 12 into a template Test it with two different classes Modify C1 5:OStack.hand C1 5:OStackTest.cpp... the code that uses the object, then look at the object and encapsulate its hard parts into other objects, etc 4 Don’t automatically rewrite all your existing C code in C+ + unless you need to significantly change its functionality (that is, don’t fix it if it isn’t broken) Recompiling C in C+ + is a valuable activity because it may reveal hidden bugs 798 Thinking in C+ + www.BruceEckel.com However, taking... #define), in which all of the letters in the identifier are uppercase The value of the style is that capitalization has meaning – you can see from the first letter whether you’re talking about a class or an object/method This is especially useful when static class members are accessed 794 Thinking in C+ + www.BruceEckel.com Order of header inclusion Headers are included in order from “the most specific to... www.BruceEckel.com 25 26 (Advanced) Modify the Stack class in TStack2.hto allow full granularity of ownership: Add a flag to each link indicating whether that link owns the object it points to, and support this information in the push( ) function and destructor Add member functions to read and change the ownership for each link (Advanced) Modify PointerToMemberOperator.cpp from Chapter 12 so that the FunctionObjectand . ///:~ For convenience, Int has an associated ostream operator<< for both an Int& and an Int* . 774 Thinking in C+ + www.BruceEckel.com The first block of code in main( ) is. approach (composition could make more sense), but in this case inheritance is a simple approach and it doesn’t detract from the point made in the example. //: C1 6:Drawing.cpp #include <vector>. about creating container and iterator classes in this book, in practice it’s much more expedient to learn the containers and iterators in the Standard C+ + Library, since you can expect them