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

📄 mi27.htm

📁 一个非常适合初学者入门的有关c++的文档
💻 HTM
📖 第 1 页 / 共 2 页
字号:
 More Effective C++ | Item 27: Requiring or prohibiting heap-based objects Back to Item 26: Limiting the number of objects of a classContinue to Item 28: Smart pointersItem 27: Requiring or prohibiting heap-based objects.Sometimes you want to arrange things so that objects of a particular type can commit suicide, i.e., can "delete this." Such an arrangement clearly requires that objects of that type be allocated on the heap. Other times you'll want to bask in the certainty that there can be no memory leaks for a particular class, because none of the objects could have been allocated on the heap. This might be the case if you are working on an embedded system, where memory leaks are especially troublesome and heap space is at a premium. Is it possible to produce code that requires or prohibits heap-based objects? Often it is, but it also turns out that the notion of being "on the heap" is more nebulous than you might think.Requiring Heap-Based ObjectsLet us begin with the prospect of limiting object creation to the heap. To enforce such a restriction, you've got to find a way to prevent clients from creating objects other than by calling new. This is easy to do. Non-heap objects are automatically constructed at their point of definition and automatically destructed at the end of their lifetime, so it suffices to simply make these implicit constructions and destructions illegal.The straightforward way to make these calls illegal is to declare the constructors and the destructor private. This is overkill. There's no reason why they both need to be private. Better to make the destructor private and the constructors public. Then, in a process that should be familiar from Item 26, you can introduce a privileged pseudo-destructor function that has access to the real destructor. Clients then call the pseudo-destructor to destroy the objects they've created.If, for example, we want to ensure that objects representing unlimited precision numbers are created only on the heap, we can do it like this: class UPNumber {public:  UPNumber();  UPNumber(int initValue);  UPNumber(double initValue);  UPNumber(const UPNumber& rhs);  // pseudo-destructor (a const member function, because  // even const objects may be destroyed)  void destroy() const { delete this; }  ...private:  ~UPNumber();};Clients would then program like this: UPNumber n;                          // error! (legal here, but                                     // illegal when n's dtor is                                     // later implicitly invoked)UPNumber *p = new UPNumber;          // fine...delete p;                            // error! attempt to call                                     // private destructorp->destroy();                        // fineAn alternative is to declare all the constructors private. The drawback to that idea is that a class often has many constructors, and the class's author must remember to declare each of them private. This includes the copy constructor, and it may include a default constructor, too, if these functions would otherwise be generated by compilers; compiler-generated functions are always public (see Item E45). As a result, it's easier to declare only the destructor private, because a class can have only one of those.Restricting access to a class's destructor or its constructors prevents the creation of non-heap objects, but, in a story that is told in Item 26, it also prevents both inheritance and containment: class UPNumber { ... };              // declares dtor or ctors                                     // privateclass NonNegativeUPNumber:  public UPNumber { ... };           // error! dtor or ctors                                     // won't compileclass Asset {private:  UPNumber value;  ...                                // error! dtor or ctors                                     // won't compile};Neither of these difficulties is insurmountable. The inheritance problem can be solved by making UPNumber's destructor protected (while keeping its constructors public), and classes that need to contain objects of type UPNumber can be modified to contain pointers to UPNumber objects instead: class UPNumber { ... };              // declares dtor protectedclass NonNegativeUPNumber:  public UPNumber { ... };           // now okay; derived                                     // classes have access to                                     // protected membersclass Asset {public:  Asset(int initValue);  ~Asset();  ...private:  UPNumber *value;};Asset::Asset(int initValue): value(new UPNumber(initValue))      // fine{ ... }Asset::~Asset(){ value->destroy(); }                 // also fineDetermining Whether an Object is On The HeapIf we adopt this strategy, we must reexamine what it means to be "on the heap." Given the class definition sketched above, it's legal to define a non-heap NonNegativeUPNumber object: NonNegativeUPNumber n;                // fineNow, the UPNumber part of the NonNegativeUPNumber object n is not on the heap. Is that okay? The answer depends on the details of the class's design and implementation, but let us suppose it is not okay, that all UPNumber objects even base class parts of more derived objects must be on the heap. How can we enforce this restriction?There is no easy way. It is not possible for a UPNumber constructor to determine whether it's being invoked as the base class part of a heap-based object. That is, there is no way for the UPNumber constructor to detect that the following contexts are different: NonNegativeUPNumber *n1 =  new NonNegativeUPNumber;            // on heapNonNegativeUPNumber n2;               // not on heapBut perhaps you don't believe me. Perhaps you think you can play games with the interaction among the new operator, operator new and the constructor that the new operator calls (see Item 8). Perhaps you think you can outsmart them all by modifying UPNumber as follows: class UPNumber {public:  // exception to throw if a non-heap object is created  class HeapConstraintViolation {};  static void * operator new(size_t size);  UPNumber();  ...private:  static bool onTheHeap;                 // inside ctors, whether                                         // the object being  ...                                    // constructed is on heap};// obligatory definition of class staticbool UPNumber::onTheHeap = false;void *UPNumber::operator new(size_t size){  onTheHeap = true;  return ::operator new(size);}UPNumber::UPNumber(){  if (!onTheHeap) {    throw HeapConstraintViolation();  }  proceed with normal construction here;  onTheHeap = false;                    // clear flag for next obj.}There's nothing deep going on here. The idea is to take advantage of the fact that when an object is allocated on the heap, operator new is called to allocate the raw memory, then a constructor is called to initialize an object in that memory. In particular, operator new sets onTheHeap to true, and each constructor checks onTheHeap to see if the raw memory of the object being constructed was allocated by operator new. If not, an exception of type HeapConstraintViolation is thrown. Otherwise, construction proceeds as usual, and when construction is finished, onTheHeap is set to false, thus resetting the default value for the next object to be constructed.This is a nice enough idea, but it won't work. Consider this potential client code: UPNumber *numberArray = new UPNumber[100];The first problem is that the memory for the array is allocated by operator new[], not operator new, but (provided your compilers support it) you can write the former function as easily as the latter. What is more troublesome is the fact that numberArray has 100 elements, so there will be 100 constructor calls. But there is only one call to allocate memory, so onTheHeap will be set to true for only the first of those 100 constructors. When the second constructor is called, an exception is thrown, and woe is you.Even without arrays, this bit-setting business may fail. Consider this statement: UPNumber *pn = new UPNumber(*new UPNumber);Here we create two UPNumbers on the heap and make pn point to one of them; it's initialized with the value of the second one. This code has a resource leak, but let us ignore that in favor of an examination of what happens during execution of this expression: new UPNumber(*new UPNumber)This contains two calls to the new operator, hence two calls to operator new and two calls to UPNumber constructors (see Item 8). Programmers typically expect these function calls to be executed in this order, Call operator new for first object Call constructor for first object Call operator new for second object Call constructor for second objectbut the language makes no guarantee that this is how it will be done. Some compilers generate the function calls in this order instead: Call operator new for first object Call operator new for second object Call constructor for first object Call constructor for second objectThere is nothing wrong with compilers that generate this kind of code, but the set-a-bit-in-operator-new trick fails with such compilers. That's because the bit set in steps 1 and 2 is cleared in step 3, thus making the object constructed in step 4 think it's not on the heap, even though it is.These difficulties don't invalidate the basic idea of having each constructor check to see if *this is on the heap. Rather, they indicate that checking a bit set inside operator new (or operator new[]) is not a reliable way to determine this information. What we need is a better way to figure it out.If you're desperate enough, you might be tempted to descend into the realm of the unportable. For example, you might decide to take advantage of the fact that on many systems, a program's address space is organized as a linear sequence of addresses, with the program's stack growing down from the top of the address space and the heap rising up from the bottom:On systems that organize a program's memory in this way (many do, but many do not), you might think you could use the following function to determine whether a particular address is on the heap: // incorrect attempt to determine whether an address// is on the heapbool onHeap(const void *address){  char onTheStack;                   // local stack variable  return address < &onTheStack;}The thinking behind this function is interesting. Inside onHeap, onTheStack is a local variable. As such, it is, well, it's on the stack. When onHeap is called, its stack frame (i.e., its activation record) will be placed at the top of the program's stack, and because the stack grows down (toward lower addresses) in this architecture, the address of onTheStack must be less than the address of any other stack-based variable or object. If the parameter address is less than the location of onTheStack, it can't be on the stack, so it must be on the heap.Such logic is fine, as far as it goes, but it doesn't go far enough. The fundamental problem is that there are three places where objects may be allocated, not two. Yes, the stack and the heap hold objects, but let us not forget about static objects. Static objects are those that are initialized only once during a program run. Static objects comprise not only those objects explicitly declared static, but also objects at global and namespace scope (see Item E47). Such objects have to go somewhere, and that somewhere is neither the stack nor the heap.Where they go is system-dependent, but on many of the systems that have the stack and heap grow toward one another, they go below the heap. The earlier picture of memory organization, while telling the truth and nothing but the truth for many systems, failed to tell the whole truth for those systems. With static objects added to the picture, it looks like this:Suddenly it becomes clear why onHeap won't work, not even on systems where it's purported to: it fails to distinguish between heap objects and static objects: void allocateSomeObjects(){  char *pc = new char;               // heap object: onHeap(pc)                                     // will return true  char c;                            // stack object: onHeap(&c)                                     // will return false  static char sc;                    // static object: onHeap(&sc)                                     // will return true  ...}Now, you may be desperate for a way to tell heap objects from stack objects, and in your desperation you may be willing to strike a deal with the portability Devil, but are you so desperate that you'll strike a deal that fails to guarantee you the right answers? Surely not, so I know you'll reject this seductive but unreliable compare-the-addresses trick.The sad fact is there's not only no portable way to determine whether an object is on the heap, there isn't even a semi-portable way that works most of the time. If you absolutely, positively have to tell whether an address is on the heap, you're going to have to turn to unportable, implementation-dependent system calls, and that's that. As such, you're better off trying to redesign your software so you don't need to determine whether an object is on the heap in the first place.If you find yourself obsessing over whether an object is on the heap, the likely cause is that you want to know if it's safe to invoke delete on it. Often such deletion will take the form of the infamous "delete this." Knowing whether it's safe to delete a pointer, however, is not the same as simply knowing whether that pointer points to something on the heap, because not all pointers to things on the heap can be safely deleted. Consider again an Asset object that contains a UPNumber object: class Asset {private:  UPNumber value;  ...};

⌨️ 快捷键说明

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