⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 ec6.htm

📁 一个非常适合初学者入门的有关c++的文档
💻 HTM
📖 第 1 页 / 共 5 页
字号:
};class SavingsAccount: public BankAccount { ... };class CheckingAccount: public BankAccount { ... };list<BankAccount*> allAccounts;// look ma, no cast!for (list<BankAccount*>::iterator p = allAccounts.begin();      p != allAccounts.end();      ++p) {  (*p)->creditInterest();}Notice that the virtual function BankAccount::creditInterest provides an empty default implementation. This is a convenient way to specify that its behavior is a no-op by default, but it can lead to unforeseen difficulties in its own right. For the inside story on why, as well as how to eliminate the danger, consult Item 36. Notice also that creditInterest is (implicitly) an inline function. There's nothing wrong with that, but because it's also virtual, the inline directive will probably be ignored. Item 33 explains why.As you have seen, downcasts can be eliminated in a number of ways. The best way is to replace such casts with calls to virtual functions, possibly also making each virtual function a no-op for any classes to which it doesn't truly apply. A second method is to tighten up the typing so that there is no ambiguity between the declared type of a pointer and the pointer type that you know is really there. Whatever the effort required to get rid of downcasts, it's effort well spent, because downcasts are ugly and error-prone, and they lead to code that's difficult to understand, enhance, and maintain (see Item M32).What I've just written is the truth and nothing but the truth. It is not, however, the whole truth. There are occasions when you really do have to perform a downcast.For example, suppose you faced the situation we considered at the outset of this Item, i.e., allAccounts holds BankAccount pointers, creditInterest is defined only for SavingsAccount objects, and you must write a loop to credit interest to every account. Further suppose that all those things are beyond your control; you can't change the definitions for BankAccount, SavingsAccount, or allAccounts. (This would happen if they were defined in a library to which you had read-only access.) If that were the case, you'd have to use downcasting, no matter how distasteful you found the idea.Nevertheless, there is a better way to do it than through a raw cast such as we saw above. The better way is called "safe downcasting," and it's implemented via C++'s dynamic_cast operator (see Item M2). When you use dynamic_cast on a pointer, the cast is attempted, and if it succeeds (i.e., if the dynamic type of the pointer (see Item 38) is consistent with the type to which it's being cast), a valid pointer of the new type is returned. If the dynamic_cast fails, the null pointer is returned.Here's the banking example with safe downcasting added: class BankAccount { ... };          // as at the beginning of                                    // this Itemclass SavingsAccount:               // ditto  public BankAccount { ... };class CheckingAccount:              // ditto again  public BankAccount { ... };list<BankAccount*> allAccounts;     // this should look                                    // familiar...void error(const string& msg);      // error-handling function;                                    // see below// well, ma, at least the casts are safe...for (list<BankAccount*>::iterator p = allAccounts.begin();      p != allAccounts.end();      ++p) {  // try safe-downcasting *p to a SavingsAccount*; see  // below for information on the definition of psa  if (SavingsAccount *psa =        dynamic_cast<SavingsAccount*>(*p)) {    psa->creditInterest();  }  // try safe-downcasting it to a CheckingAccount  else if (CheckingAccount *pca =        dynamic_cast<CheckingAccount*>(*p)) {    pca->creditInterest();  }  // uh oh  unknown account type  else {    error("Unknown account type!");  }}This scheme is far from ideal, but at least you can detect when your downcasts fail, something that's impossible without the use of dynamic_cast. Note, however, that prudence dictates you also check for the case where all the downcasts fail. That's the purpose of the final else clause in the code above. With virtual functions, there'd be no need for such a test, because every virtual call must resolve to some function. When you start downcasting, however, all bets are off. If somebody added a new type of account to the hierarchy, for example, but failed to update the code above, all the downcasts would fail. That's why it's important you handle that possibility. In all likelihood, it's not supposed to be the case that all the casts can fail, but when you allow downcasting, bad things start to happen to good programmers.Did you check your glasses in a panic when you noticed what looks like variable definitions in the conditions of the if statements above? If so, worry not; your vision's fine. The ability to define such variables was added to the language at the same time as dynamic_cast. This feature lets you write neater code, because you don't really need psa or pca unless the dynamic_casts that initialize them succeed, and with the new syntax, you don't have to define those variables outside the conditionals containing the casts. (Item 32 explains why you generally want to avoid superfluous variable definitions, anyway.) If your compilers don't yet support this new way of defining variables, you can do it the old way: for (list<BankAccount*>::iterator p = allAccounts.begin();      p != allAccounts.end();      ++p) {  SavingsAccount *psa;        // traditional definition  CheckingAccount *pca;       // traditional definition  if (psa = dynamic_cast<SavingsAccount*>(*p)) {    psa->creditInterest();  }  else   if (pca = dynamic_cast<CheckingAccount*>(*p)) {    pca->creditInterest();  }  else {    error("Unknown account type!");  }}In the grand scheme of things, of course, where you place your definitions for variables like psa and pca is of little consequence. The important thing is this: the if-then-else style of programming that downcasting invariably leads to is vastly inferior to the use of virtual functions, and you should reserve it for situations in which you truly have no alternative. With any luck, you will never face such a bleak and desolate programming landscape. Back to Item 39: Avoid casts down the inheritance hierarchy.Continue to Item 41: Differentiate between inheritance and templates.Item 40: Model "has-a" or "is-implemented-in-terms-of" through layering.Layering is the process of building one class on top of another class by having the layering class contain an object of the layered class as a data member. For example: class Address { ... };           // where someone livesclass PhoneNumber { ... };class Person {public:  ...private:  string name;                   // layered object  Address address;               // ditto  PhoneNumber voiceNumber;       // ditto  PhoneNumber faxNumber;         // ditto};In this example, the Person class is said to be layered on top of the string, Address, and PhoneNumber classes, because it contains data members of those types. The term layering has lots of synonyms. It's also known as composition, containment, and embedding.Item 35 explains that public inheritance means "isa." In contrast, layering means either "has-a" or "is-implemented-in-terms-of."The Person class above demonstrates the has-a relationship. A Person object has a name, an address, and telephone numbers for voice and FAX communication. You wouldn't say that a person is a name or that a person is an address. You would say that a person has a name and has an address, etc. Most people have little difficulty with this distinction, so confusion between the roles of isa and has-a is relatively rare.Somewhat more troublesome is the difference between isa and is-implemented-in-terms-of. For example, suppose you need a template for classes representing sets of arbitrary objects, i.e., collections without duplicates. Because reuse is a wonderful thing, and because you wisely read Item 49's overview of the standard C++ library, your first instinct is to employ the library's set template. After all, why write a new template when you can use an established one written by somebody else?As you delve into set's documentation, however, you discover a limitation your application can't live with: a set requires that the elements contained within it be totally ordered, i.e., for every pair of objects a and b in the set, it must be possible to determine whether a<b or b<a. For many types, this requirement is easy to satisfy, and having a total ordering among objects allows set to offer certain attractive guarantees regarding its performance. (See Item 49 for more on performance guarantees in the standard library.) Your need, however, is for something more general: a set-like class where objects need not be totally ordered, they need only be what the C++ standard colorfully terms "EqualityComparable": it's possible to determine whether a==b for objects a and b of the same type. This more modest requirement is better suited to types representing things like colors. Is red less than green or is green less than red? For your application, it seems you'll need to write your own template after all.Still, reuse is a wonderful thing. Being the data structure maven you are, you know that of the nearly limitless choices for implementing sets, one particularly simple way is to employ linked lists. But guess what? The list template (which generates linked list classes) is just sitting there in the standard library! You decide to (re)use it.In particular, you decide to have your nascent Set template inherit from list. That is, Set<T> will inherit from list<T>. After all, in your implementation, a Set object will in fact be a list object. You thus declare your Set template like this: // the wrong way to use list for Settemplate<class T>class Set: public list<T> { ... };Everything may seem fine and dandy at this point, but in fact there is something quite wrong. As Item 35 explains, if D isa B, everything true of B is also true of D. However, a list object may contain duplicates, so if the value 3051 is inserted into a list<int> twice, that list will contain two copies of 3051. In contrast, a Set may not contain duplicates, so if the value 3051 is inserted into a Set<int> twice, the set contains only one copy of the value. It is thus a vicious lie that a Set isa list, because some of the things that are true for list objects are not true for Set objects.Because the relationship between these two classes isn't isa, public inheritance is the wrong way to model that relationship. The right way is to realize that a Set object can be implemented in terms of a list object: // the right way to use list for Settemplate<class T>class Set {public:  bool member(const T& item) const;  void insert(const T& item);  void remove(const T& item);  int cardinality() const;private:  list<T> rep;                       // representation for a set};Set's member functions can lean heavily on functionality already offered by list and other parts of the standard library, so the implementation is neither difficult to write nor thrilling to read: template<class T>bool Set<T>::member(const T& item) const{ return find(rep.begin(), rep.end(), item) != rep.end(); }template<class T>void Set<T>::insert(const T& item){ if (!member(item)) rep.push_back(item); }template<class T>void Set<T>::remove(const T& item){  list<T>::iterator it =    find(rep.begin(), rep.end(), item);  if (it != rep.end()) rep.erase(it);}template<class T>int Set<T>::cardinality() const{ return rep.size(); }These functions are simple enough that they make reasonable candidates for inlining, though I know you'd want to review the discussion in Item 33 before making any firm inlining decisions. (In the code above, functions like find, begin, end, push_back, etc., are part of the standard library's framework for working with container templates like list. You'll find an overview of this framework in Item 49 and M35.)It's worth remarking that the Set class interface fails the test of being complete and minimal (see Item 18). In terms of completeness, the primary omission is that of a way to iterate over the contents of a set, something that might well be necessary for many applications (and that is provided by all members of the standard library, including set). An additional drawback is that Set fails to follow the container class conventions embraced by the standard library (see Items 49 and M35), and that makes it more difficult to take advantage of other parts of the library when working with Sets.Nits about Set's interface, however, shouldn't be allowed to overshadow what Set got indisputably right: the relationship between Set and list. That relationship is not isa (though it initially looked like it might be), it's "is-implemented-in-terms-of," and the use of layering to implement that relationship is something of which any class designer may be justly proud.Incidentally, when you use layering to relate two classes, you create a compile-time dependency between those classes. For information on why this should concern you, as well as what you can do to allay your worries, turn to Item 34. Back to Item 40: Model "has-a" or "is-implemented-in-terms-of" through layering.Continue to Item 42: Use private inheritance judiciously.Item 41: Differentiate between inheritance and templates.Consider the following two design problems: Being a devoted student of Computer Science, you want to create classes representing stacks of objects. You'll need several different classes, because each stack must be homogeneous, i.e., it must have only a single type of object in it. For ex

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -