📄 mc1.htm
字号:
they must generate code that does something like this: // destruct the objects in *array in the inverse order// in which they were constructedfor ( int i = the number of elements in the array - 1; i >= 0; --i) { array[i].BST::~BST(); // call array[i]'s } // destructorJust as this kind of loop failed to work when you wrote it, it will fail to work when your compilers write it, too. The language specification says the result of deleting an array of derived class objects through a base class pointer is undefined, but we know what that really means: executing the code is almost certain to lead to grief. Polymorphism and pointer arithmetic simply don't mix. Array operations almost always involve pointer arithmetic, so arrays and polymorphism don't mix.Note that you're unlikely to make the mistake of treating an array polymorphically if you avoid having a concrete class (like BalancedBST) inherit from another concrete class (such as BST). As Item 33 explains, designing your software so that concrete classes never inherit from one another has many benefits. I encourage you to turn to Item 33 and read all about them. Back to Item 3: Never treat arrays polymorphicallyContinue to OperatorsItem 4: Avoid gratuitous default constructors.A default constructor (i.e., a constructor that can be called with no arguments) is the C++ way of saying you can get something for nothing. Constructors initialize objects, so default constructors initialize objects without any information from the place where the object is being created. Sometimes this makes perfect sense. Objects that act like numbers, for example, may reasonably be initialized to zero or to undefined values. Objects that act like pointers ( Item 28) may reasonably be initialized to null or to undefined values. Data structures like linked lists, hash tables, maps, and the like may reasonably be initialized to empty containers.Not all objects fall into this category. For many objects, there is no reasonable way to perform a complete initialization in the absence of outside information. For example, an object representing an entry in an address book makes no sense unless the name of the thing being entered is provided. In some companies, all equipment must be tagged with a corporate ID number, and creating an object to model a piece of equipment in such companies is nonsensical unless the appropriate ID number is provided.In a perfect world, classes in which objects could reasonably be created from nothing would contain default constructors and classes in which information was required for object construction would not. Alas, ours is not the best of all possible worlds, so we must take additional concerns into account. In particular, if a class lacks a default constructor, there are restrictions on how you can use that class.Consider a class for company equipment in which the corporate ID number of the equipment is a mandatory constructor argument: class EquipmentPiece {public: EquipmentPiece(int IDNumber); ...};Because EquipmentPiece lacks a default constructor, its use may be problematic in three contexts. The first is the creation of arrays. There is, in general, no way to specify constructor arguments for objects in arrays, so it is not usually possible to create arrays of EquipmentPiece objects: EquipmentPiece bestPieces[10]; // error! No way to call // EquipmentPiece ctors EquipmentPiece *bestPieces = new EquipmentPiece[10]; // error! same problemThere are three ways to get around this restriction. A solution for non-heap arrays is to provide the necessary arguments at the point where the array is defined: int ID1, ID2, ID3, ..., ID10; // variables to hold // equipment ID numbers...EquipmentPiece bestPieces[] = { // fine, ctor arguments EquipmentPiece(ID1), // are provided EquipmentPiece(ID2), EquipmentPiece(ID3), ..., EquipmentPiece(ID10)};Unfortunately, there is no way to extend this strategy to heap arrays.A more general approach is to use an array of pointers instead of an array of objects: typedef EquipmentPiece* PEP; // a PEP is a pointer to // an EquipmentPiecePEP bestPieces[10]; // fine, no ctors calledPEP *bestPieces = new PEP[10]; // also fineEach pointer in the array can then be made to point to a different EquipmentPiece object: for (int i = 0; i < 10; ++i) bestPieces[i] = new EquipmentPiece( ID Number );There are two disadvantages to this approach. First, you have to remember to delete all the objects pointed to by the array. If you forget, you have a resource leak. Second, the total amount of memory you need increases, because you need the space for the pointers as well as the space for the EquipmentPiece objects.You can avoid the space penalty if you allocate the raw memory for the array, then use "placement new" (see Item 8) to construct the EquipmentPiece objects in the memory: // allocate enough raw memory for an array of 10// EquipmentPiece objects; see Item 8 for details on// the operator new[] functionvoid *rawMemory = operator new[](10*sizeof(EquipmentPiece));// make bestPieces point to it so it can be treated as an// EquipmentPiece arrayEquipmentPiece *bestPieces = static_cast<EquipmentPiece*>(rawMemory);// construct the EquipmentPiece objects in the memory// using "placement new" (see Item 8)for (int i = 0; i < 10; ++i) new (&bestPieces[i]) EquipmentPiece( ID Number );Notice that you still have to provide a constructor argument for each EquipmentPiece object. This technique (as well as the array-of-pointers idea) allows you to create arrays of objects when a class lacks a default constructor; it doesn't show you how to bypass required constructor arguments. There is no way to do that. If there were, it would defeat the purpose of constructors, which is to guarantee that objects are initialized.The downside to using placement new, aside from the fact that most programmers are unfamiliar with it (which will make maintenance more difficult), is that you must manually call destructors on the objects in the array when you want them to go out of existence, then you must manually deallocate the raw memory by calling operator delete[] (again, see Item 8): // destruct the objects in bestPieces in the inverse// order in which they were constructedfor (int i = 9; i >= 0; --i) bestPieces[i].~EquipmentPiece();// deallocate the raw memoryoperator delete[](rawMemory);If you forget this requirement and use the normal array-deletion syntax, your program will behave unpredictably. That's because the result of deleting a pointer that didn't come from the new operator is undefined: delete [] bestPieces; // undefined! bestPieces // didn't come from the new // operatorFor more information on the new operator, placement new and how they interact with constructors and destructors, see Item 8.The second problem with classes lacking default constructors is that they are ineligible for use with many template-based container classes. That's because it's a common requirement for such templates that the type used to instantiate the template provide a default constructor. This requirement almost always grows out of the fact that inside the template, an array of the template parameter type is being created. For example, a template for an Array class might look something like this: template<class T>class Array {public: Array(int size); ...private: T *data;};template<class T>Array<T>::Array(int size){ data = new T[size]; // calls T::T() for each ... // element of the array}In most cases, careful template design can eliminate the need for a default constructor. For example, the standard vector template (which generates classes that act like extensible arrays) has no requirement that its type parameter have a default constructor. Unfortunately, many templates are designed in a manner that is anything but careful. That being the case, classes without default constructors will be incompatible with many templates. As C++ programmers learn more about template design, this problem should recede in significance. How long it will take for that to happen, however, is anyone's guess.The final consideration in the to-provide-a-default-constructor-or-not-to-provide-a-default-constructor dilemma has to do with virtual base classes (see Item E43). Virtual base classes lacking default constructors are a pain to work with. That's because the arguments for virtual base class constructors must be provided by the most derived class of the object being constructed. As a result, a virtual base class lacking a default constructor requires that all classes derived from that class no matter how far removed must know about, understand the meaning of, and provide for the virtual base class's constructors' arguments. Authors of derived classes neither expect nor appreciate this requirement.Because of the restrictions imposed on classes lacking default constructors, some people believe all classes should have them, even if a default constructor doesn't have enough information to fully initialize objects of that class. For example, adherents to this philosophy might modify EquipmentPiece as follows: class EquipmentPiece {public: EquipmentPiece( int IDNumber = UNSPECIFIED); ...private: static const int UNSPECIFIED; // magic ID number value // meaning no ID was}; // specifiedThis allows EquipmentPiece objects to be created like this: EquipmentPiece e; // now okaySuch a transformation almost always complicates the other member functions of the class, because there is no longer any guarantee that the fields of an EquipmentPiece object have been meaningfully initialized. Assuming it makes no sense to have an EquipmentPiece without an ID field, most member functions must check to see if the ID is present. If it's not, they'll have to figure out how to stumble on anyway. Often it's not clear how to do that, and many implementations choose a solution that offers nothing but expediency: they throw an exception or they call a function that terminates the program. When that happens, it's difficult to argue that the overall quality of the software has been improved by including a default constructor in a class where none was warranted.Inclusion of meaningless default constructors affects the efficiency of classes, too. If member functions have to test to see if fields have truly been initialized, clients of those functions have to pay for the time those tests take. Furthermore, they have to pay for the code that goes into those tests, because that makes executables and libraries bigger. They also have to pay for the code that handles the cases where the tests fail. All those costs are avoided if a class's constructors ensure that all fields of an object are correctly initialized. Often default constructors can't offer that kind of assurance, so it's best to avoid them in classes where they make no sense. That places some limits on how such classes can be used, yes, but it also guarantees that when you do use such classes, you can expect that the objects they generate are fully initialized and are efficiently implemented. Back to IntroductionContinue to Chapter 2: Operators 1 A second, unrelated use of dynamic_cast is to find the beginning of the memory occupied by an object. We explore that capability in Item 27.Return
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -