📄 mc2.htm
字号:
Consider what happens when an Array object is defined via the class's single-argument constructor: Array<int> a(10);Your compilers are asked to call a constructor in the Array<int> class that takes an int, but there is no such constructor. Compilers realize they can convert the int argument into a temporary ArraySize object, and that ArraySize object is just what the Array<int> constructor needs, so compilers perform the conversion with their usual gusto. This allows the function call (and the attendant object construction) to succeed.The fact that you can still construct Array objects with an int argument is reassuring, but it does you little good unless the type conversions you want to avoid are prevented. They are. Consider this code again: bool operator==( const Array<int>& lhs, const Array<int>& rhs);Array<int> a(10);Array<int> b(10);...for (int i = 0; i < 10; ++i) if (a == b[i]) ... // oops! "a" should be "a[i]"; // this is now an errorCompilers need an object of type Array<int> on the right-hand side of the "==" in order to call operator== for Array<int> objects, but there is no single-argument constructor taking an int argument. Furthermore, compilers cannot consider converting the int into a temporary ArraySize object and then creating the necessary Array<int> object from this temporary, because that would call for two user-defined conversions, one from int to ArraySize and one from ArraySize to Array<int>. Such a conversion sequence is verboten, so compilers must issue an error for the code attempting to perform the comparison.The use of the ArraySize class in this example might look like a special-purpose hack, but it's actually a specific instance of a more general technique. Classes like ArraySize are often called proxy classes, because each object of such a class stands for (is a proxy for) some other object. An ArraySize object is really just a stand-in for the integer used to specify the size of the Array being created. Proxy objects can give you control over aspects of your software's behavior in this case implicit type conversions that is otherwise beyond your grasp, so it's well worth your while to learn how to use them. How, you might wonder, can you acquire such learning? One way is to turn to Item 30; it's devoted to proxy classes.Before you turn to proxy classes, however, reflect a bit on the lessons of this Item. Granting compilers license to perform implicit type conversions usually leads to more harm than good, so don't provide conversion functions unless you're sure you want them. Back to Item 5: Be wary of user-defined conversion functionsContinue to Item 7: Never overload ||, or ,Item 6: Distinguish between prefix and postfix forms of increment and decrement operators.Long, long ago (the late '80s) in a language far, far away (C++ at that time), there was no way to distinguish between prefix and postfix invocations of the ++ and -- operators. Programmers being programmers, they kvetched about this omission, and C++ was extended to allow overloading both forms of increment and decrement operators.There was a syntactic problem, however, and that was that overloaded functions are differentiated on the basis of the parameter types they take, but neither prefix nor postfix increment or decrement takes an argument. To surmount this linguistic pothole, it was decreed that postfix forms take an int argument, and compilers silently pass 0 as that int when those functions are called: class UPInt { // "unlimited precision int"public: UPInt& operator++(); // prefix ++ const UPInt operator++(int); // postfix ++ UPInt& operator--(); // prefix -- const UPInt operator--(int); // postfix -- UPInt& operator+=(int); // a += operator for UPInts // and ints ...};UPInt i;++i; // calls i.operator++();i++; // calls i.operator++(0);--i; // calls i.operator--();i--; // calls i.operator--(0);This convention is a little on the odd side, but you'll get used to it. More important to get used to, however, is this: the prefix and postfix forms of these operators return different types. In particular, prefix forms return a reference, postfix forms return a const object. We'll focus here on the prefix and postfix ++ operators, but the story for the -- operators is analogous.From your days as a C programmer, you may recall that the prefix form of the increment operator is sometimes called "increment and fetch," while the postfix form is often known as "fetch and increment." These two phrases are important to remember, because they all but act as formal specifications for how prefix and postfix increment should be implemented: // prefix form: increment and fetchUPInt& UPInt::operator++(){ *this += 1; // increment return *this; // fetch}// postfix form: fetch and incrementconst UPInt UPInt::operator++(int){ UPInt oldValue = *this; // fetch ++(*this); // incrementreturn oldValue; // return what was} // fetchedNote how the postfix operator makes no use of its parameter. This is typical. The only purpose of the parameter is to distinguish prefix from postfix function invocation. Many compilers issue warnings (see Item E48) if you fail to use named parameters in the body of the function to which they apply, and this can be annoying. To avoid such warnings, a common strategy is to omit names for parameters you don't plan to use; that's what's been done above.It's clear why postfix increment must return an object (it's returning an old value), but why a const object? Imagine that it did not. Then the following would be legal: UPInt i;i++++; // apply postfix increment // twiceThis is the same as i.operator++(0).operator++(0);and it should be clear that the second invocation of operator++ is being applied to the object returned from the first invocation.There are two reasons to abhor this. First, it's inconsistent with the behavior of the built-in types. A good rule to follow when designing classes is when in doubt, do as the ints do, and the ints most certainly do not allow double application of postfix increment: int i;i++++; // error!The second reason is that double application of postfix increment almost never does what clients expect it to. As noted above, the second application of operator++ in a double increment changes the value of the object returned from the first invocation, not the value of the original object. Hence, if i++++;were legal, i would be incremented only once. This is counterintuitive and confusing (for both ints and UPInts), so it's best prohibited.C++ prohibits it for ints, but you must prohibit it yourself for classes you write. The easiest way to do this is to make the return type of postfix increment a const object. Then when compilers see i++++; // same as i.operator++(0).operator++(0);they recognize that the const object returned from the first call to operator++ is being used to call operator++ again. operator++, however, is a non-const member function, so const objects such as those returned from postfix operator++ can't call it.2 If you've ever wondered if it makes sense to have functions return const objects, now you know: sometimes it does, and postfix increment and decrement are examples. (For another example, turn to Item E21.)If you're the kind who worries about efficiency, you probably broke into a sweat when you first saw the postfix increment function. That function has to create a temporary object for its return value (see Item 19), and the implementation above also creates an explicit temporary object (oldValue) that has to be constructed and destructed. The prefix increment function has no such temporaries. This leads to the possibly startling conclusion that, for efficiency reasons alone, clients of UPInt should prefer prefix increment to postfix increment unless they really need the behavior of postfix increment. Let us be explicit about this. When dealing with user-defined types, prefix increment should be used whenever possible, because it's inherently more efficient.Let us make one more observation about the prefix and postfix increment operators. Except for their return values, they do the same thing: they increment a value. That is, they're supposed to do the same thing. How can you be sure the behavior of postfix increment is consistent with that of prefix increment? What guarantee do you have that their implementations won't diverge over time, possibly as a result of different programmers maintaining and enhancing them? Unless you've followed the design principle embodied by the code above, you have no such guarantee. That principle is that postfix increment and decrement should be implemented in terms of their prefix counterparts. You then need only maintain the prefix versions, because the postfix versions will automatically behave in a consistent fashion.As you can see, mastering prefix and postfix increment and decrement is easy. Once you know their proper return types and that the postfix operators should be implemented in terms of the prefix operators, there's very little more to learn. Back to Item 6: Distinguish between prefix and postfix forms of increment and decrement operatorsContinue to Item 8: Understand the different meanings of new and deleteItem 7: Never overload &&, ||, or ,.Like C, C++ employs short-circuit evaluation of boolean expressions. This means that once the truth or falsehood of an expression has been determined, evaluation of the expression ceases, even if some parts of the expression haven't yet been examined. For example, in this case, char *p;...if ((p != 0) && (strlen(p) > 10)) ...there is no need to worry about invoking strlen on p if it's a null pointer, because if the test of p against 0 fails, strlen will never be called. Similarly, given int rangeCheck(int index){ if ((index < lowerBound) || (index > upperBound)) ... ...}index will never be compared to upperBound if it's less than lowerBound.This is the behavior that has been drummed into C and C++ programmers since time immemorial, so this is what they expect. Furthermore, they write programs whose correct behavior depends on short-circuit evaluation. In the first code fragment above, for example, it is important that strlen not be invoked if p is a null pointer, because the standard for C++ states (as does the standard for C) that the result of invoking strlen on a null pointer is undefined.C++ allows you to customize the behavior of the && and || operators for user-defined types. You do it by overloading the functions operator&& and operator||, and you can do this at the global scope or on a per-class basis. If you decide to take advantage of this opportunity, however, you must be aware that you are changing the rules of the game quite radically, because you are replacing short-circuit semantics with function call semantics. That is, if you overload operator&&, what looks to you like this, if (expression1 && expression2) ...looks to compilers like one of these: if (expression1.operator&&(expression2)) ... // when operator&& is a // member functionif (operator&&(expression1, expression2)) ... // when operator&& is a // global functionThis may not seem like that big a deal, but function call semantics differ from short-circuit semantics in two crucial ways. First, when a function call is made, all parameters must be evaluated, so when calling the functions operator&& and operator||, both parameters are evaluated. There is, in other words, no short circuit. Second, the language specification leaves undefined the order of evaluation of parameters to a function call, so there is no way of knowing whether expression1 or expression2 will be evaluated first. This stands in stark contrast to short-circuit evaluation, which always evaluates its arguments in left-to-right order.As a result, if you overload && or ||, there is no way to offer programmers the behavior they both expect and have come to depend on. So don't overload && or ||.The situation with the comma operator is similar, but before we delve into that, I'll pause and let you catch the breath you lost when you gasped, "The comma operator? There's a comma operator?" There is indeed.The comma operator is used to form expressions, and you're most likely to run across it in the update part of a for loop. The following function, for example, is based on one in the second edition of Kernighan's and Ritchie's classic The C Programming Language (Prentice-Hall, 1988): // reverse string s in placevoid reverse(char s[]){ for (int i = 0, j = strlen(s)-1; i < j; ++i, --j) // aha! the comma operator! { int c = s[i]; s[i] = s[j]; s[j] = c; }}Here, i is incremented and j is decremented in the final part of the for loop. It is convenient to use the comma operator here, because only an expression is valid in the final part of a for loop; separate statements to change the values of i and j would be illegal.Just as there are rules in C++ defining how && and || behave for built-in types, there are rules defining how the comma operator behaves for such types. An expression containing a comma is evaluated by first evaluating the part of the expression to the left of the comma, then evaluating the expression to the right of the comma; the result of the overall comma expression is the value of the expression on the right. So in the final part of the loop above, compilers first evaluate ++i, then --j, and the result of the comma expression is the value returned from --j.Perhaps you're wondering why you need to know this. You need to know because you need to mimic this behavior if you're going to take it upon yourself to write your own comma operator. Unfortunately, you can't perform the requisite mimicry.If you write operator, as a non-member function, you'll never be able to guarantee that the left-hand expression is evaluated before the right-hand expression, because both expressions will be passed as arguments in a function call (to operator,). But you have no control over the order in which a function's arguments are evaluated. So the non-member approach is definitely out.That leaves only the possibility of writing operator, as a member function. Even here you can't rely on the left-hand operand to the comma operator being evaluated first, because compilers are not constrained to do things that way. Hence, you can't overload the comma operator and also guarantee it will behave the way it's supposed to. It therefore seems imprudent to overload it at all.You may be wondering if there's an end to this overloading madness. After all, if you can overload the comma operator, what can't you overload? As it turns out, there are limits. You can't overload the following operators: . .* :: ?:new delete sizeof typeidstatic_cast dynamic_cast const_cast reinterpret_castYou can overload these: operator new operator deleteoperator new[] operator delete[]+ - * / % ^ & | ~! = < > += -= *= /= %=^= &= |= << >> >>= <<= == !=<= >= && || ++ -- , ->* ->() []
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -