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

📄 ec4.htm

📁 一个非常适合初学者入门的有关c++的文档
💻 HTM
📖 第 1 页 / 共 5 页
字号:
 Effective C++, 2E | Chapter 4: Classes and Functions: Design and Declaration Back to Item 17: Check for assignment to self in operator=.Continue to Item 18: Strive for class interfaces that are complete and minimal.Classes and Functions: Design and DeclarationDeclaring a new class in a program creates a new type: class design is type design. You probably don't have much experience with type design, because most languages don't offer you the opportunity to get any practice. In C++, it is of fundamental importance, not just because you can do it if you want to, but because you are doing it every time you declare a class, whether you mean to or not.Designing good classes is challenging because designing good types is challenging. Good types have a natural syntax, an intuitive semantics, and one or more efficient implementations. In C++, a poorly thought out class definition can make it impossible to achieve any of these goals. Even the performance characteristics of a class's member functions are determined as much by the declarations of those member functions as they are by their definitions.How, then, do you go about designing effective classes? First, you must understand the issues you face. Virtually every class requires that you confront the following questions, the answers to which often lead to constraints on your design: How should objects be created and destroyed? How this is done strongly influences the design of your constructors and destructor, as well as your versions of operator new, operator new[], operator delete, and operator delete[], if you write them. (Item M8 describes the differences among these terms.) How does object initialization differ from object assignment? The answer to this question determines the behavior of and the differences between your constructors and your assignment operators. What does it mean to pass objects of the new type by value? Remember, the copy constructor defines what it means to pass an object by value. What are the constraints on legal values for the new type? These constraints determine the kind of error checking you'll have to do inside your member functions, especially your constructors and assignment operators. It may also affect the exceptions your functions throw and, if you use them, your functions' exception specifications (see Item M14). Does the new type fit into an inheritance graph? If you inherit from existing classes, you are constrained by the design of those classes, particularly by whether the functions you inherit are virtual or nonvirtual. If you wish to allow other classes to inherit from your class, that will affect whether the functions you declare are virtual. What kind of type conversions are allowed? If you wish to allow objects of type A to be implicitly converted into objects of type B, you will want to write either a type conversion function in class A or a non-explicit constructor in class B that can be called with a single argument. If you wish to allow explicit conversions only, you'll want to write functions to perform the conversions, but you'll want to avoid making them type conversion operators or non-explicit single-argument constructors. (Item M5 discusses the advantages and disadvantages of user-defined conversion functions.) What operators and functions make sense for the new type? The answer to this question determines which functions you'll declare in your class interface. What standard operators and functions should be explicitly disallowed? Those are the ones you'll need to declare private. Who should have access to the members of the new type? This question helps you determine which members are public, which are protected, and which are private. It also helps you determine which classes and/or functions should be friends, as well as whether it makes sense to nest one class inside another. How general is the new type? Perhaps you're not really defining a new type. Perhaps you're defining a whole family of types. If so, you don't want to define a new class, you want to define a new class template.These are difficult questions to answer, so defining effective classes in C++ is far from simple. Done properly, however, user-defined classes in C++ yield types that are all but indistinguishable from built-in types, and that makes all the effort worthwhile.A discussion of the details of each of the above issues would comprise a book in its own right, so the guidelines that follow are anything but comprehensive. However, they highlight some of the most important design considerations, warn about some of the most frequent errors, and provide solutions to some of the most common problems encountered by class designers. Much of the advice is as applicable to non-member functions as it is to member functions, so in this section I consider the design and declaration of global and namespace-resident functions, too. Back to Classes and Functions: Design and DeclarationContinue to Item 19: Differentiate among member functions, non-member functions, and friend functions.Item 18: Strive for class interfaces that are complete and minimal.The client interface for a class is the interface that is accessible to the programmers who use the class. Typically, only functions exist in this interface, because having data members in the client interface has a number of drawbacks (see Item 20).Trying to figure out what functions should be in a class interface can drive you crazy. You're pulled in two completely different directions. On the one hand, you'd like to build a class that is easy to understand, straightforward to use, and easy to implement. That usually implies a fairly small number of member functions, each of which performs a distinct task. On other hand, you'd like your class to be powerful and convenient to use, which often means adding functions to provide support for commonly performed tasks. How do you decide which functions go into the class and which ones don't?Try this: aim for a class interface that is complete and minimal.A complete interface is one that allows clients to do anything they might reasonably want to do. That is, for any reasonable task that clients might want to accomplish, there is a reasonable way to accomplish it, although it may not be as convenient as clients might like. A minimal interface, on the other hand, is one with as few functions in it as possible, one in which no two member functions have overlapping functionality. If you offer a complete, minimal interface, clients can do whatever they want to do, but the class interface is no more complicated than absolutely necessary.The desirability of a complete interface seems obvious enough, but why a minimal interface? Why not just give clients everything they ask for, adding functionality until everyone is happy?Aside from the moral issue is it really right to mollycoddle your clients? there are definite technical disadvantages to a class interface that is crowded with functions. First, the more functions in an interface, the harder it is for potential clients to understand. The harder it is for them to understand, the more reluctant they will be to learn how to use it. A class with 10 functions looks tractable to most people, but a class with 100 functions is enough to make many programmers run and hide. By expanding the functionality of your class to make it as attractive as possible, you may actually end up discouraging people from learning how to use it.A large interface can also lead to confusion. Suppose you create a class that supports cognition for an artificial intelligence application. One of your member functions is called think, but you later discover that some people want the function to be called ponder, and others prefer the name ruminate. In an effort to be accommodating, you offer all three functions, even though they do the same thing. Consider then the plight of a potential client of your class who is trying to figure things out. The client is faced with three different functions, all of which are supposed to do the same thing. Can that really be true? Isn't there some subtle difference between the three, possibly in efficiency or generality or reliability? If not, why are there three different functions? Rather than appreciating your flexibility, such a potential client is likely to wonder what on earth you were thinking (or pondering, or ruminating over).A second disadvantage to a large class interface is that of maintenance (see Item M32). It's simply more difficult to maintain and enhance a class with many functions than it is a class with few. It is more difficult to avoid duplicated code (with the attendant duplicated bugs), and it is more difficult to maintain consistency across the interface. It's also more difficult to document.Finally, long class definitions result in long header files. Because header files typically have to be read every time a program is compiled (see Item 34), class definitions that are longer than necessary can incur a substantial penalty in total compile-time over the life of a project.The long and short of it is that the gratuitous addition of functions to an interface is not without costs, so you need to think carefully about whether the convenience of a new function (a new function can only be added for convenience if the interface is already complete) justifies the additional costs in complexity, comprehensibility, maintainability, and compilation speed.Yet there's no sense in being unduly miserly. It is often justifiable to offer more than a minimal set of functions. If a commonly performed task can be implemented much more efficiently as a member function, that may well justify its addition to the interface. (Then again, it may not. See Item M16.) If the addition of a member function makes the class substantially easier to use, that may be enough to warrant its inclusion in the class. And if adding a member function is likely to prevent client errors, that, too, is a powerful argument for its being part of the interface.Consider a concrete example: a template for classes that implement arrays with client-defined upper and lower bounds and that offer optional bounds-checking. The beginning of such an array template is shown below: template<class T>class Array {public:  enum BoundsCheckingStatus {NO_CHECK_BOUNDS = 0,                             CHECK_BOUNDS = 1};  Array(    int lowBound, int highBound,      BoundsCheckingStatus check = NO_CHECK_BOUNDS);  Array(const Array& rhs);  ~Array();  Array& operator=(const Array& rhs);private:  int lBound, hBound;         // low bound, high bound  vector<T> data;             // contents of array; see                              // Item 49 for vector info  BoundsCheckingStatus checkingBounds;};The member functions declared so far are the ones that require basically no thinking (or pondering or ruminating). You have a constructor to allow clients to specify each array's bounds, a copy constructor, an assignment operator, and a destructor. In this case, you've declared the destructor nonvirtual, which implies that this class is not to be used as a base class (see Item 14).The declaration of the assignment operator is actually less clear-cut than it might at first appear. After all, built-in arrays in C++ don't allow assignment, so you might want to disallow it for your Array objects, too (see Item 27). On the other hand, the array-like vector template (in the standard library see Item 49) permits assignments between vector objects. In this example, you'll follow vector's lead, and that decision, as you'll see below, will affect other portions of the classes's interface.Old-time C hacks would cringe to see this interface. Where is the support for declaring an array of a particular size? It would be easy enough to add another constructor, Array(int size,      BoundsCheckingStatus check = NO_CHECK_BOUNDS);but this is not part of a minimal interface, because the constructor taking an upper and lower bound can be used to accomplish the same thing. Nonetheless, it might be a wise political move to humor the old geezers, possibly under the rubric of consistency with the base language.What other functions do you need? Certainly it is part of a complete interface to index into an array: // return element for read/writeT& operator[](int index);// return element for read-onlyconst T& operator[](int index) const;By declaring the same function twice, once const and once non-const, you provide support for both const and non-const Array objects. The difference in return types is significant, as is explained in Item 21.As it now stands, the Array template supports construction, destruction, pass-by-value, assignment, and indexing, which may strike you as a complete interface. But look closer. Suppose a client wants to loop through an array of integers, printing out each of its elements, like so: Array<int> a(10, 20);      // bounds on a are 10 to 20...for (int i = lower bound of a; i <= upper bound of a; ++i)  cout << "a[" << i << "] = " << a[i] << '\n';How is the client to get the bounds of a? The answer depends on what happens during assignment of Array objects, i.e., on what happens inside Array::operator=. In particular, if assignment can change the bounds of an Array object, you must provide member functions to return the current bounds, because the client has no way of knowing a priori what the bounds are at any given point in the program. In the example above, if a was the target of an assignment between the time it was defined and the time it was used in the loop, the client would have no way to determine the current bounds of a.On the other hand, if the bounds of an Array object cannot be changed during assignment, then the bounds are fixed at the point of definition, and it would be possible (though cumbersome) for a client to keep track of these bounds. In that case, though it would be convenient to offer functions to return the current bounds, such functions would not be part of a truly minimal interface.Proceeding on the assumption that assignment can modify the bounds of an object, the bounds functions could be declared thus: int lowBound() const;int highBound() const;Because these functions don't modify the object on which they are invoked, and because you prefer to use const whenever you can (see Item 21), these are both declared const member functions. Given these functions, the loop above would be written as follows: for (int i = a.lowBound(); i <= a.highBound(); ++i)  cout << "a[" << i << "] = " << a[i] << '\n';Needless to say, for such a loop to work for an array of objects of type T, an operator<< function must be defined for objects of type T. (That's not quite true. What must exist is an operator<< for T or for some other type to which T may be implicitly converted (see Item M5). But you get the idea.)Some designers would argue that the Array class should also offer a function to return the number of elements in an Array object. The number of elements is simply highBound()-lowBound()+1, so such a function is not really necessary, but in view of the frequency of off-by-one errors, it might not be a bad idea to add such a function.Other functions that might prove worthwhile for this class include those for input and output, as well as the various relational operators (e.g., <, >, ==, etc.). None of those functions is part of a minimal interface, however, because they can all be implemented in terms of loops containing calls to operator[].Speaking of functions like operator<<, operator>>, and the relational operators, Item 19 discusses why they are frequently implemented as non-member friend functions instead of as member functions. That being the case, don't forget that friend functions are, for all practical purposes, part of a class's interface. That means that friend functions count toward a class interface's completeness and minimalness. Back to Item 18: Strive for class interfaces that are complete and minimal.Continue to Item 20: Differentiate among member functions, non-member functions, and friend functions.Item 19: Differentiate among member functions, non-member functions, and friend functions.The biggest difference between member functions and non-member functions is that member functions can be virtual and non-member functions can't. As a result, if you have a function that has to be dynamically bound (see Item 38), you've got to use a virtual function, and that virtual function must be a member of some class. It's as simple as that. If your function doesn't need to be virtual, however, the water begins to muddy a bit.Consider a class for representing rational numbers: class Rational {public:  Rational(int numerator = 0, int denominator = 1);  int numerator() const;  int denominator() const;private:  ...};As it stands now, this is a pretty useless class. (Using the terms of Item 18, the interface is certainly minimal, but it's far from complete.) You know you'd like to support arithmetic operations like addition, subtraction, multiplication, etc., but you're unsure whether you should implement them via a member function, a non-member function, or possibly a non-member function that's a friend.When in doubt, be object-oriented. You know that, say, multiplication of rational numbers is related to the Rational class, so try bundling the operation with the class by making it a member function: class Rational {public:  ...  const Rational operator*(const Rational& rhs) const;};(If you're unsure why this function is declared the way it is returning a const by-value result, but taking a reference-to-const as its argument consult Items 21-23.)Now you can multiply rational numbers with the greatest of ease: Rational oneEighth(1, 8);Rational oneHalf(1, 2);Rational result = oneHalf * oneEighth;      // fineresult = result * oneEighth;                // fineBut you're not satisfied. You'd also like to support mixed-mode operations, where Rationals can be multiplied with, for example, ints. When you try to do this, however, you find that it works only half the time: result = oneHalf * 2;      // fineresult = 2 * oneHalf;      // error!This is a bad omen. Multiplication is supposed to be commutative, remember?The source of the problem becomes apparent when you rewrite the last two examples in their equivalent functional form: result = oneHalf.operator*(2);      // fineresult = 2.operator*(oneHalf);      // error!The object oneHalf is an instance of a class that contains an operator*, so your compilers call that function. However, the integer 2 has no associated class, hence no operator* member function. Your compilers will also look for a non-member operator* (i.e., one that's in a visible namespace or is global) that can be called like this, result = operator*(2, oneHalf);      // error!but there is no non-member operator* taking an int and a Rational, so the search fails.Look again at the call that succeeds. You'll see that its second parameter is the integer 2, yet Rational::operator* takes a Rational object as its argument. What's going on here? Why does 2 work in one position and not in the other?What's going on is implicit type conversion. Your compilers know you're passing an int and the function requires a Rational, but they also know that they can conjure up a suitable Rational by calling the Rational constructor with the int you provided, so that's what they do (see Item M19). In other words, they treat the call as if it had been written more or less like this: const Rational temp(2);      // create a temporary                             // Rational object from 2result = oneHalf * temp;     // same as                             // oneHalf.operator*(temp);Of course, they do this only when non-explicit constructors are involved, because explicit constructors can't be used for implicit conversions; that's what explicit means. If Rational were defined like this, class Rational {public:  explicit Rational(int numerator = 0,     // this ctor is                    int denominator = 1);  // now explicit  ...  const Rational operator*(const Rational& rhs) const;  ...};

⌨️ 快捷键说明

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