Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 51 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
51
Dung lượng
454,52 KB
Nội dung
Ira Pohl’s C++ by Dissection 8.3 Virtual Functions: Dynamic Determination 340 case in inheritance that overridden methods be declared virtual. This allows them to select appropriate behavior at runtime. As a simple exercise, redo person with virtual functions. Should the get_age() method be virtual? Virtual functions require added work at runtime and are less efficient than nonvirtual methods. C++ programmers use them only where needed. 8.3.1 Overloading and Overriding Confusion Virtual functions and member function overloading cause confusion. Consider the fol- lowing program: In file virtual_err.cpp #include <iostream> using namespace std; class Base { public: virtual void foo(int i) { cout << "Base::i = " << i << endl; } virtual void foo(double x){ cout << "Base::x = " << x << endl; } }; class Derived : public Base { public: void foo(int i){ cout << "Derived::i = " << i << endl; } }; class Derived2 : public Derived { public: void foo(int i) { cout << "Derived2::i = " << i << endl; } void foo(double d) { cout << "Derived2::d = " << d << endl; } }; int main() { Derived d; Derived2 d2; Base b, *pb = &d; b.foo(9); // selects Base::foo(int); b.foo(9.5); // selects Base::foo(double); d.foo(9); // selects Derived::foo(int); d.foo(9.5); // selects Derived::foo(int); pb -> foo(9); // selects Derived::foo(int); pb -> foo(9.5); // selects Base::foo(double); pb = &d2; pb -> foo(9); // selects Derived2::foo(int); pb -> foo(9.5); // selects Derived2::foo(double) } Ira Pohl’s C++ by Dissection 8.3 Virtual Functions: Dynamic Determination 341 Only nonstatic member functions can be virtual. The virtual characteristic is inherited. Thus, the derived-class function is automatically virtual, and the presence of the vir- tual keyword is usually a matter of taste. Constructors cannot be virtual, but destruc- tors can be. As a rule of thumb, any class having virtual functions should have a virtual destructor. Some compilers, such as the latest version of g++, issues a warning if a class has virtual members and a nonvirtual destructor. Dissection of the virtual_error Program ■ class Base { public: virtual void foo(int i) { cout << "Base::i = " << i << endl; } virtual void foo(double x) { cout << "Base::x = " << x << endl; } }; Here, we have a classic case of signature overloading. In overloading, the compiler at compile time statically selects which method to call. ■ class Derived : public Base { public: void foo(int i) { cout << "Derived::i = " << i << endl; } }; The base-class member function Base::foo(int) is overridden. So far, this is not confusing. However, the base-class member function Base::foo(double) is inherited in the derived class but is not over- ridden. Here is the cause of the confusion: d.foo(9); // selects Derived::foo(int); d.foo(9.5); // selects Derived::foo(int); pb -> foo(9); // selects Derived::foo(int); pb -> foo(9.5); // selects Base::foo(double); In the statement d.foo(9.5), the double value 9.5 is converted to the integer value 9. To call the hidden member function, we need to use scope resolution as in d.Base::foo(double). On the other hand, when called with a pointer pb -> foo(9.5), the Base::foo(dou- ble) is selected. Obviously, this a confusing situation that should be avoided. When overriding base class virtual functions that are over- loaded, be sure to overload all of their definitions. This is what was done in Derived2 which does not suffer the same confusion. Ira Pohl’s C++ by Dissection 8.3 Virtual Functions: Dynamic Determination 342 8.3.2 A Canonical Example: Class shape Virtual functions allow runtime decisions. Consider a computer-aided design applica- tion in which the area of the shapes in a design has to be computed. The various shapes are derived from the shape base class. In file shape.cpp class shape { public: virtual double area() const { return 0; } // virtual double area is default behavior protected: double x, y; }; class rectangle : public shape { public: rectangle(double h = 0.0, double w=0.0): height(h), width(w) { } double area() const { return (height * width); } private: double height, width; }; class circle : public shape { public: circle(double r = 0.0) : radius(r) { } double area() const { return (PI * radius * radius); } private: double radius; }; In such a class hierarchy, the derived classes correspond to important, well-understood types of shapes. The system is readily expanded by deriving further classes. The area calculation is a local responsibility of a derived class. Client code that uses the polymorphic area calculation looks like this: const int N = 3; int main() { shape* p[N]; p[0] = new rectangle(2, 3); p[1] = new rectangle(2.5, 2.001); p[2] = new circle(1.5); double tot_area = 0.0; for (int i = 0; i < N; ++i) tot_area += p[i] -> area(); cout << tot_area << " is total area" << endl; } Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 343 A major advantage here is that the client code does not need to change if new shapes are added to the system. Change is managed locally and propagated automatically by the polymorphic character of the client code. 8.4 Abstract Base Classes A type hierarchy begins with a base class that contains a number of virtual functions. They provide for dynamic typing. In the base class, virtual functions are often dummy functions and have an empty body. In the derived classes, however, virtual functions are given specific meanings. In C++, the pure virtual function is introduced for this pur- pose. A pure virtual function is one whose body is normally undefined. Notationally, such a function is declared inside the class, as follows: virtual function prototype = 0; The pure virtual function is used to defer the implementation decision of the function. In OOP terminology, it is called a deferred method. A class that has at least one pure virtual function is an abstract class. In a type hierar- chy, it is useful for the base class to be an abstract class. This base class has the basic common properties of its derived classes but cannot itself be used to declare objects. Instead, it is used to declare pointers or references that can access subtype objects derived from the abstract class. We explain this concept while developing a primitive form of ecological simulation. OOP was originally developed as a simulation methodology using Simula67. Hence, many of its ideas are easily understood as an attempt to model a particular reality. The world in our example has various forms of life interacting; they inherit the interface of an abstract base class called living. Each position in a grid defined to be the world can either have a life-form or be empty. We shall have foxes as an archetypal predator, with rabbits as prey. The rabbits eat grass. Each of these life-forms lives, reproduces, and dies each iteration of the simulation. In file predator.cpp // Predator-Prey simulation using class living enum state { EMPTY, GRASS, RABBIT, FOX, STATES }; const int DRAB = 3, DFOX = 6, TMFOX = 5, CYCLES = 5, N = 40; // DRAB rabbits die at 3, DFOX foxes at 6, // TMFOX too many foxes, CYCLES of simulation, // N size of square world class living; // forward declaration typedef living* world[N][N]; 8.4 Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 344 class living { // what lives in world public: virtual state who() = 0; // state identification virtual living* next(world w) = 0; protected: int row, column; // location void sums(world w, int sm[]); }; void living::sums(world w, int sm[]) { int i, j; sm[EMPTY] = sm[GRASS] = 0; sm[RABBIT] = sm[FOX] = 0; for (i = -1; i <= 1; ++i) for (j = -1; j <= 1; ++j) sm[w[row + i][column + j] -> who()]++; } Dissection of the living Abstract Base Class ■ class living; // forward declaration typedef living* world[N][N]; The class living represents different life-forms, such as rabbits and grass. The life-forms are placed on an N by N square world. ■ class living { // what lives in world public: virtual state who() = 0; // state identity virtual living* next(world w) = 0; protected: int row, column; // location void sums(world w,int sm[]); }; This abstract base class is used as the base class for all derived indi- vidual life-forms needed by the simulation. There are two pure virtual functions and one ordinary member function, sums(). The pure vir- tual functions must be defined in any concrete class derived from class living. Virtual functions incur a small additional runtime cost over normal member functions. Therefore, we use virtual func- tions only when necessary to our implementations. Our simulation has rules for deciding who goes on living based on the populations in the neighborhood of a given square. These populations are computed by sums(). Note that the neighborhood includes the square itself. Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 345 The inheritance hierarchy is one level deep. // Currently only predator class class fox : public living { public: fox(int r, int c, int a = 0) : age(a) { row = r; column = c; } state who() { return FOX; }// deferred fox method living* next(world w); protected: int age; // used to decide on dying }; // Currently only prey class class rabbit : public living { public: rabbit(int r, int c, int a = 0) : age(a) { row = r; column = c; } state who() { return RABBIT; } living* next(world w); protected: int age; }; ■ void living::sums(world w, int sm[]) { int i, j; sm[EMPTY] = sm[GRASS] = 0; sm[RABBIT] = sm[FOX] = 0; for (i = -1; i <= 1; ++i) for ( j = -1; j <= 1; ++j) sm[w[row + i][column + j] -> who()]++; } This function collects the values of the different life-forms in the region immediately surrounding the life-form’s position in the world, namely (row, column). Each life-form has rules that use this sum to see if they propagate, die off, or stay alive. Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 346 // Currently only plant life class grass : public living { public: grass(int r, int c) { row = r; column = c; } state who() { return GRASS; } living* next(world w); }; // Nothing lives here class empty : public living { public: empty(int r, int c) { row = r; column = c; } state who() { return EMPTY; } living* next(world w); }; Notice that the design allows other forms of predator, prey, and plant life to be devel- oped, using a further level of inheritance. The characteristics of how each life-form behaves are captured in its version of next(). Grass can be eaten by rabbits. If there is more grass than the rabbits in the neighbor- hood can eat, the grass remains; otherwise, it is eaten up. (Feel free to substitute your own rules, as these are highly limited and artificial.) living* grass::next(world w) { int sum[STATES]; sums(w, sum); if (sum[GRASS] > sum[RABBIT]) // eat grass return (new grass(row, column)); else return (new empty(row, column)); } Rabbits die of old age if they exceed a defined limit DRAB; they are eaten if there are an appropriate number of foxes nearby. living* rabbit::next(world w) { int sum[STATES]; sums(w, sum); if (sum[FOX] >= sum[RABBIT]) // eat rabbits return (new empty(row, column)); else if (age > DRAB) // rabbit is too old return (new empty(row, column)); else return (new rabbit(row, column, age + 1)); } Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 347 Foxes die of overcrowding or old age. living* fox::next(world w) { int sum[STATES]; sums(w, sum); if (sum[FOX] > TMFOX) // too many foxes return (new empty(row, column)); else if (age > DFOX) // fox is too old return (new empty(row, column)); else return (new fox(row, column, age + 1)); } Empty squares are competed for by the various life-forms. living* empty::next(world w) // fill empty square { int sum[STATES]; sums(w, sum); if (sum[FOX] > 1) return (new fox(row, column)); else if (sum[RABBIT] > 1) return (new rabbit(row, column)); else if (sum[GRASS] > 0) return (new grass(row, column)); else return (new empty(row, column)); } The rules in the various versions of next() determine a possibly complex set of interac- tions. Of course, to make the simulation more interesting, other behaviors, such as sex- ual reproduction, whereby the animals have gender and can mate, could be simulated. The array type world is a container for the life-forms. The container has the responsi- bility of creating its current pattern. The container needs to have ownership of the liv- ing objects so as to allocate new ones and delete old ones. // World is all empty void init(world w) { int i, j; for (i = 0; i < N; ++i) for (j = 0; j < N; ++j) w[i][j] = new empty(i,j); } Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 348 This routine creates an empty world. Each square is initialized by the empty::empty() constructor. // New world w_new is computed from old world w_old void update(world w_new, world w_old) { int i, j; for (i = 1; i < N - 1; ++i) // borders are taboo for (j = 1; j < N - 1; ++j) w_new[i][j] = w_old[i][j] -> next(w_old); } This routine updates the world. The old state of the world stored in w_old[][] is used to compute what lives in the new state w_new[][]. This is computed from rules that next() uses. // Clean world up void dele(world w) { int i, j; for (i = 1; i < N - 1; ++i) //borders are taboo for (j = 1; j < N - 1; ++j) delete(w[i][j]); } This routine returns memory to the heap (free store). In a long-running large simulation, all these calls to new would burn up too much memory if not for this reclamation rou- tine. void eden(world w) { int i, j; for (i = 2; i < N - 2; ++i) for (j = 2; j < N - 2; ++j) { delete(w[i][j]); if ( (i + j) % 3 == 0) w[i][j] = new rabbit(i, j); else if ( (i + j) % 3 == 1) w[i][j] = new fox(i, j); else w[i][j] = new grass(i, j); } } Ira Pohl’s C++ by Dissection 8.4 Abstract Base Classes 349 We need a first state of the world. This version of an eden() routine should be replaced by a routine that allows the user to establish the Garden of Eden pattern. void pr_state(world w) { int i, j; for (i = 0; i < N; ++i) { cout << endl; for (j = 0; j < N; ++j) cout << static_cast<int>(w[i][j] -> who()); } cout << endl << endl; } The simulation has odd and even worlds, which alternate as the basis for the next cycle’s calculations. int main() { world odd, even; int i; init(odd); init(even); eden(even); // generate initial world pr_state(even); // print Garden of Eden state for (i = 0; i < CYCLES; ++i) { // simulation if (i % 2) { update(even, odd); pr_state(even); dele(odd); } else { update(odd, even); pr_state(odd); dele(even); } } } If the fox is eating the rabbit which eats the grass, can the fire be far behind? [...]... narrow it For example: Ira Pohl’s C++ by Dissection 8. 8 Software Engineering: Inheritance and Design 355 In file access_mod.cpp // Access modification class Base { public: int k; protected: int j, n; private: int i; }; class Derived : public Base { public: int m; Base::n; // illegal protected access can't broaden private: Base::j; // otherwise default is protected }; 8. 8 8. 8 Software Engineering: Inheritance...Ira Pohl’s C++ by Dissection 8. 5 Templates and Inheritance 350 The code runs the simulation for a number of iterations specified by the constant CYCLES The reader should experiment with modifications of this code The structure of the program lets you easily modify the rules and the initial configuration More advanced modifications would improve the user interface and add other life-forms 8. 5 8. 5 Templates... cases to be developed for each type, if necessary; it does not lead to large object-code modules Remember, each template instantiation is compiled to object code Ira Pohl’s C++ by Dissection 8. 9 8. 9 8. 9 Dr P’s Prescriptions 3 58 Dr P’s Prescriptions s Use interface inheritance, called ISA inheritance s Usually, a base class is abstract s Minimize interactions between classes s Base-class destructors... self-defeating There is a granularity decision whereby highly specialized classes do not provide enough benefit and are better folded into a larger concept Single inheritance (SI) conforms to a hierarchical decomposition of the key objects in the domain of discourse Multiple inheritance (MI) is more troubling as a modeling or Ira Pohl’s C++ by Dissection 8. 8 Software Engineering: Inheritance and Design... We start with the superclass Person1 This class is identical to Person in Section 4.14, C++ Compared with Java, on page 171 except that the private instance variables are changed to have access protected This access allows their use in the subclass but otherwise acts like private Ira Pohl’s C++ by Dissection 8. 10 C++ Compared with Java 359 In file Person1.java // An elementary Java implementation of... whereby the subclass Student is derived from it Other subclasses, such as GradStudent or Employee, could be added to this inheritance hierarchy In Java, polymorphism comes from both method overloading and method overriding Overriding occurs when a method is redefined in the subclass The toString() method is in Person1 and is redefined in Student extended from Person1 Ira Pohl’s C++ by Dissection 8. 10 C++. .. graphical interface (See Section 8. 4, Abstract Base Classes, on page 343, for the predator-prey C++ simulation.) This is one area that Java excels in CHAPTER 9 Input/Output T his chapter describes input/output in C++, using iostream and its associated librar- ies The standard input/output library for C, described by the header cstdio, is still available in C++ However, C++ introduces iostream, which... endl; cout . (MI) is more troubling as a modeling or 8. 8 Ira Pohl’s C++ by Dissection 8. 8 Software Engineering: Inheritance and. declaration typedef living* world[N][N]; 8. 4 Ira Pohl’s C++ by Dissection 8. 4 Abstract Base Classes 344 class living. This access allows their use in the subclass but otherwise acts like private. 8. 9 8. 10 Ira Pohl’s C++ by Dissection 8. 10 C++ Compared with Java 359 In file Person1.java // An elementary Java implementation