1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in C plus plus (P14) ppt

50 185 0

Đ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

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 180,59 KB

Nội dung

630 Thinking in C++ www.BruceEckel.com Because we can now guarantee that we know exactly what kind of objects are in the container, the destructor works correctly and the ownership problem is solved – or at least, one approach to the ownership problem. Here, if you push( ) a string pointer onto the StringStack , then (according to the semantics of the StringStack ) you’re also passing ownership of that pointer to the StringStack . If you pop( ) the pointer, you not only get the pointer, but you also get ownership of that pointer. Any pointers that are left on the StringStack when its destructor is called are then deleted by that destructor. And since these are always string pointers and the delete statement is working on string pointers instead of void pointers, the proper destruction happens and everything works correctly. There is a drawback: this class works only for string pointers. If you want a Stack that works with some other kind of object, you must write a new version of the class so that it works only with your new kind of object. This rapidly becomes tedious, and is finally solved using templates, as you will see in Chapter 16. We can make an additional observation about this example: it changes the interface of the Stack in the process of inheritance. If the interface is different, then a StringStack really isn’t a Stack , and you will never be able to correctly use a StringStack as a Stack . This makes the use of inheritance questionable here; if you’re not creating a StringStack that is-a type of Stack , then why are you inheriting? A more appropriate version of StringStack will be shown later in this chapter. Functions that don’t automatically inherit Not all functions are automatically inherited from the base class into the derived class. Constructors and destructors deal with the creation and destruction of an object, and they can know what to 14: Inheritance & Composition 631 do with the aspects of the object only for their particular class, so all the constructors and destructors in the hierarchy below them must be called. Thus, constructors and destructors don’t inherit and must be created specially for each derived class. In addition, the operator= doesn’t inherit because it performs a constructor-like activity. That is, just because you know how to assign all the members of an object on the left-hand side of the = from an object on the right-hand side doesn’t mean that assignment will still have the same meaning after inheritance. In lieu of inheritance, these functions are synthesized by the compiler if you don’t create them yourself. (With constructors, you can’t create any constructors in order for the compiler to synthesize the default constructor and the copy-constructor.) This was briefly described in Chapter 6. The synthesized constructors use memberwise initialization and the synthesized operator= uses memberwise assignment. Here’s an example of the functions that are synthesized by the compiler: //: C14:SynthesizedFunctions.cpp // Functions that are synthesized by the compiler #include <iostream> using namespace std; class GameBoard { public: GameBoard() { cout << "GameBoard()\n"; } GameBoard(const GameBoard&) { cout << "GameBoard(const GameBoard&)\n"; } GameBoard& operator=(const GameBoard&) { cout << "GameBoard::operator=()\n"; return *this; } ~GameBoard() { cout << "~GameBoard()\n"; } }; class Game { GameBoard gb; // Composition 632 Thinking in C++ www.BruceEckel.com public: // Default GameBoard constructor called: Game() { cout << "Game()\n"; } // You must explicitly call the GameBoard // copy-constructor or the default constructor // is automatically called instead: Game(const Game& g) : gb(g.gb) { cout << "Game(const Game&)\n"; } Game(int) { cout << "Game(int)\n"; } Game& operator=(const Game& g) { // You must explicitly call the GameBoard // assignment operator or no assignment at // all happens for gb! gb = g.gb; cout << "Game::operator=()\n"; return *this; } class Other {}; // Nested class // Automatic type conversion: operator Other() const { cout << "Game::operator Other()\n"; return Other(); } ~Game() { cout << "~Game()\n"; } }; class Chess : public Game {}; void f(Game::Other) {} class Checkers : public Game { public: // Default base-class constructor called: Checkers() { cout << "Checkers()\n"; } // You must explicitly call the base-class // copy constructor or the default constructor // will be automatically called instead: Checkers(const Checkers& c) : Game(c) { cout << "Checkers(const Checkers& c)\n"; } Checkers& operator=(const Checkers& c) { // You must explicitly call the base-class // version of operator=() or no base-class // assignment will happen: 14: Inheritance & Composition 633 Game::operator=(c); cout << "Checkers::operator=()\n"; return *this; } }; int main() { Chess d1; // Default constructor Chess d2(d1); // Copy-constructor //! Chess d3(1); // Error: no int constructor d1 = d2; // Operator= synthesized f(d1); // Type-conversion IS inherited Game::Other go; //! d1 = go; // Operator= not synthesized // for differing types Checkers c1, c2(c1); c1 = c2; } ///:~ The constructors and the operator= for GameBoard and Game announce themselves so you can see when they’re used by the compiler. In addition, the operator Other( ) performs automatic type conversion from a Game object to an object of the nested class Other . The class Chess simply inherits from Game and creates no functions (to see how the compiler responds). The function f( ) takes an Other object to test the automatic type conversion function. In main( ) , the synthesized default constructor and copy- constructor for the derived class Chess are called. The Game versions of these constructors are called as part of the constructor- call hierarchy. Even though it looks like inheritance, new constructors are actually synthesized by the compiler. As you might expect, no constructors with arguments are automatically created because that’s too much for the compiler to intuit. The operator= is also synthesized as a new function in Chess using memberwise assignment (thus, the base-class version is called) because that function was not explicitly written in the new class. 634 Thinking in C++ www.BruceEckel.com And of course the destructor was automatically synthesized by the compiler. Because of all these rules about rewriting functions that handle object creation, it may seem a little strange at first that the automatic type conversion operator is inherited. But it’s not too unreasonable – if there are enough pieces in Game to make an Other object, those pieces are still there in anything derived from Game and the type conversion operator is still valid (even though you may in fact want to redefine it). operator= is synthesized only for assigning objects of the same type. If you want to assign one type to another you must always write that operator= yourself. If you look more closely at Game , you’ll see that the copy- constructor and assignment operators have explicit calls to the member object copy-constructor and assignment operator. You will normally want to do this because otherwise, in the case of the copy- constructor, the default member object constructor will be used instead, and in the case of the assignment operator, no assignment at all will be done for the member objects! Lastly, look at Checkers , which explicitly writes out the default constructor, copy-constructor, and assignment operators. In the case of the default constructor, the default base-class constructor is automatically called, and that’s typically what you want. But, and this is an important point, as soon as you decide to write your own copy-constructor and assignment operator, the compiler assumes that you know what you’re doing and does not automatically call the base-class versions, as it does in the synthesized functions. If you want the base class versions called (and you typically do) then you must explicitly call them yourself. In the Checkers copy- constructor, this call appears in the constructor initializer list: Checkers(const Checkers& c) : Game(c) { 14: Inheritance & Composition 635 In the Checkers assignment operator, the base class call is the first line in the function body: Game::operator=(c); These calls should be part of the canonical form that you use whenever you inherit a class. Inheritance and static member functions static member functions act the same as non- static member functions: 1. They inherit into the derived class. 2. If you redefine a static member, all the other overloaded functions in the base class are hidden. 3. If you change the signature of a function in the base class, all the base class versions with that function name are hidden (this is really a variation of the previous point). However, static member functions cannot be virtual (a topic covered thoroughly in Chapter 15). Choosing composition vs. inheritance Both composition and inheritance place subobjects inside your new class. Both use the constructor initializer list to construct these subobjects. You may now be wondering what the difference is between the two, and when to choose one over the other. Composition is generally used when you want the features of an existing class inside your new class, but not its interface. That is, you embed an object to implement features of your new class, but the user of your new class sees the interface you’ve defined rather than the interface from the original class. To do this, you follow the 636 Thinking in C++ www.BruceEckel.com typical path of embedding private objects of existing classes inside your new class. Occasionally, however, it makes sense to allow the class user to directly access the composition of your new class, that is, to make the member objects public . The member objects use access control themselves, so this is a safe thing to do and when the user knows you’re assembling a bunch of parts, it makes the interface easier to understand. A Car class is a good example: //: C14:Car.cpp // Public composition class Engine { public: void start() const {} void rev() const {} void stop() const {} }; class Wheel { public: void inflate(int psi) const {} }; class Window { public: void rollup() const {} void rolldown() const {} }; class Door { public: Window window; void open() const {} void close() const {} }; class Car { public: Engine engine; Wheel wheel[4]; Door left, right; // 2-door 14: Inheritance & Composition 637 }; int main() { Car car; car.left.window.rollup(); car.wheel[0].inflate(72); } ///:~ Because the composition of a Car is part of the analysis of the problem (and not simply part of the underlying design), making the members public assists the client programmer’s understanding of how to use the class and requires less code complexity for the creator of the class. With a little thought, you’ll also see that it would make no sense to compose a Car using a “vehicle” object – a car doesn’t contain a vehicle, it is a vehicle. The is-a relationship is expressed with inheritance, and the has-a relationship is expressed with composition. Subtyping Now suppose you want to create a type of ifstream object that not only opens a file but also keeps track of the name of the file. You can use composition and embed both an ifstream and a string into the new class: //: C14:FName1.cpp // An fstream with a file name #include " /require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName1 { ifstream file; string fileName; bool named; public: FName1() : named(false) {} 638 Thinking in C++ www.BruceEckel.com FName1(const string& fname) : fileName(fname), file(fname.c_str()) { assure(file, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // Don't overwrite fileName = newName; named = true; } operator ifstream&() { return file; } }; int main() { FName1 file("FName1.cpp"); cout << file.name() << endl; // Error: close() not a member: //! file.close(); } ///:~ There’s a problem here, however. An attempt is made to allow the use of the FName1 object anywhere an ifstream object is used by including an automatic type conversion operator from FName1 to an ifstream& . But in main, the line file.close(); will not compile because automatic type conversion happens only in function calls, not during member selection. So this approach won’t work. A second approach is to add the definition of close( ) to FName1 : void close() { file.close(); } This will work if there are only a few functions you want to bring through from the ifstream class. In that case you’re only using part of the class, and composition is appropriate. But what if you want everything in the class to come through? This is called subtyping because you’re making a new type from an 14: Inheritance & Composition 639 existing type, and you want your new type to have exactly the same interface as the existing type (plus any other member functions you want to add), so you can use it everywhere you’d use the existing type. This is where inheritance is essential. You can see that subtyping solves the problem in the preceding example perfectly: //: C14:FName2.cpp // Subtyping solves the problem #include " /require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName2 : public ifstream { string fileName; bool named; public: FName2() : named(false) {} FName2(const string& fname) : ifstream(fname.c_str()), fileName(fname) { assure(*this, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // Don't overwrite fileName = newName; named = true; } }; int main() { FName2 file("FName2.cpp"); assure(file, "FName2.cpp"); cout << "name: " << file.name() << endl; string s; getline(file, s); // These work too! file.seekg(-200, ios::end); file.close(); } ///:~ [...]... namespace std; class StringStack { Stack stack; // Embed instead of inherit public: void push(string* str) { stack.push(str); } string* peek() const { return (string*)stack.peek(); } string* pop() { return (string*)stack.pop(); 652 Thinking in C+ + www.BruceEckel.com } }; int main() { ifstream in( "InheritStack2.cpp"); assure (in, "InheritStack2.cpp"); string line; StringStack textlines; while(getline (in, line))... multiple inheritance Pointer & reference upcasting In Instrument.cpp the upcasting occurs during the function call – a , Wind object outside the function has its reference taken and becomes an Instrumentreference inside the function Upcasting can also occur during a simple assignment to a pointer or reference: Wind w; Instrument* ip = &w; // Upcast Instrument& ir = w; // Upcast 14: Inheritance & Composition... new class Earlier in this chapter, the Stack class was specialized using inheritance However, chances are the StringStackobjects will be used only as string containers and never upcast, so a more appropriate alternative is composition: //: C1 4:InheritStack2.cpp // Composition vs inheritance #include " /C0 9/Stack4.h" #include " /require.h" #include #include #include using... without calling the base-class copy-constructor and see what 14: Inheritance & Composition 657 25 26 27 28 29 658 happens Fix the problem by making a proper explicit call to the base-class copy constructor in the constructorinitializer list of the Child copy-constructor Modify InheritStack2.cpp use a vector to instead of a Stack Create a class Rock with a default constructor, a copyconstructor,... tune(Instrument& i) { // i.play(middleC); } int main() { Wind flute; tune(flute); // Upcasting 662 Thinking in C+ + www.BruceEckel.com } ///:~ The function tune( ) accepts (by reference) an Instrument but also , without complaint anything derived from Instrument In main( ), you can see this happening as a Wind object is passed to tune( ), with no cast necessary This is acceptable; the interface in Instrumentmust... shown in the output 650 Thinking in C+ + www.BruceEckel.com Parent(int ii) Member(int ii) Child(int ii) calling copy-constructor: Parent(const Parent&) Member(const Member&) values in c2 : Parent: 2 Member: 2 Child: 2 However, if you try to write your own copy-constructor for Child and you make an innocent mistake and do it badly: Child(const Child& c) : i (c. i), m (c. m) {} then the default constructor... a more specific type to a more general type – the only thing that can occur to the class interface is that it can lose member functions, not gain them This is why the compiler 648 Thinking in C+ + www.BruceEckel.com allows upcasting without any explicit casts or other special notation Upcasting and the copy-constructor If you allow the compiler to synthesize a copy-constructor for a derived class, it... C1 5:Instrument2.cpp // Inheritance & upcasting #include using namespace std; enum note { middleC, Csharp, Eflat }; // Etc class Instrument { public: void play(note) const { cout . Checkers(const Checkers& c) : Game (c) { cout << "Checkers(const Checkers& c) "; } Checkers& operator=(const Checkers& c) { // You must explicitly call. typically do) then you must explicitly call them yourself. In the Checkers copy- constructor, this call appears in the constructor initializer list: Checkers(const Checkers& c) : Game (c) . interface from the original class. To do this, you follow the 636 Thinking in C+ + www.BruceEckel.com typical path of embedding private objects of existing classes inside your new class. Occasionally,

Ngày đăng: 05/07/2014, 19:20

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN