📄 ei10.htm
字号:
Effective C++, 2E | Item 10: Write operator delete if you write operator new Back to Item 9: Avoid hiding the "normal" form of new.Continue to Constructors, Destructors, and Assignment OperatorsItem 10: Write operator delete if you write operator new.Let's step back for a moment and return to fundamentals. Why would anybody want to write their own version of operator new or operator delete in the first place?More often than not, the answer is efficiency. The default versions of operator new and operator delete are perfectly adequate for general-purpose use, but their flexibility inevitably leaves room for improvements in their performance in a more circumscribed context. This is especially true for applications that dynamically allocate a large number of small objects.As an example, consider a class for representing airplanes, where the Airplane class contains only a pointer to the actual representation for airplane objects (a technique discussed in Item 34): class AirplaneRep { ... }; // representation for an // Airplane objectclass Airplane {public: ...private: AirplaneRep *rep; // pointer to representation};An Airplane object is not very big; it contains but a single pointer. (As explained in Items 14 and M24, it may implicitly contain a second pointer if the Airplane class declares virtual functions.) When you allocate an Airplane object by calling operator new, however, you probably get back more memory than is needed to store this pointer (or pair of pointers). The reason for this seemingly wayward behavior has to do with the need for operator new and operator delete to communicate with one another.Because the default version of operator new is a general-purpose allocator, it must be prepared to allocate blocks of any size. Similarly, the default version of operator delete must be prepared to deallocate blocks of whatever size operator new allocated. For operator delete to know how much memory to deallocate, it must have some way of knowing how much memory operator new allocated in the first place. A common way for operator new to tell operator delete how much memory it allocated is by prepending to the memory it returns some additional data that specifies the size of the allocated block. That is, when you say this, Airplane *pa = new Airplane;you don't necessarily get back a block of memory that looks like this:Instead, you often get back a block of memory that looks more like this:For small objects like those of class Airplane, this additional bookkeeping data can more than double the amount of memory needed for each dynamically allocated object (especially if the class contains no virtual functions).If you're developing software for an environment in which memory is precious, you may not be able to afford this kind of spendthrift allocation. By writing your own operator new for the Airplane class, you can take advantage of the fact that all Airplane objects are the same size, so there isn't any need for bookkeeping information to be kept with each allocated block.One way to implement your class-specific operator new is to ask the default operator new for big blocks of raw memory, each block of sufficient size to hold a large number of Airplane objects. The memory chunks for Airplane objects themselves will be taken from these big blocks. Currently unused chunks will be organized into a linked list the free list of chunks that are available for future Airplane use. This may make it sound like you'll have to pay for the overhead of a next field in every object (to support the list), but you won't: the space for the rep field (which is necessary only for memory chunks in use as Airplane objects) will also serve as the place to store the next pointer (because that pointer is needed only for chunks of memory not in use as Airplane objects). You'll arrange for this job-sharing in the usual fashion: you'll use a union.To turn this design into reality, you have to modify the definition of Airplane to support custom memory management. You do it as follows: class Airplane { // modified class now supportspublic: // custom memory management static void * operator new(size_t size); ...private: union { AirplaneRep *rep; // for objects in use Airplane *next; // for objects on free list }; // this class-specific constant (see Item 1) specifies how // many Airplane objects fit into a big memory block; // it's initialized below static const int BLOCK_SIZE; static Airplane *headOfFreeList;};Here you've added the declarations for operator new, the union that allows the rep and next fields to occupy the same memory, a class-specific constant for specifying how big each allocated block should be, and a static pointer to keep track of the head of the free list. It's important to use a static member for this last task, because there's one free list for the entire class, not one free list for each Airplane object.The next thing to do is to write the new operator new: void * Airplane::operator new(size_t size){ // send requests of the "wrong" size to ::operator new(); // for details, see Item 8 if (size != sizeof(Airplane)) return ::operator new(size); Airplane *p = // p is now a pointer to the headOfFreeList; // head of the free list // if p is valid, just move the list head to the // next element in the free list if (p) headOfFreeList = p->next; else { // The free list is empty. Allocate a block of memory // big enough to hold BLOCK_SIZE Airplane objects Airplane *newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane))); // form a new free list by linking the memory chunks // together; skip the zeroth element, because you'll // return that to the caller of operator new for (int i = 1; i < BLOCK_SIZE-1; ++i) newBlock[i].next = &newBlock[i+1]; // terminate the linked list with a null pointer newBlock[BLOCK_SIZE-1].next = 0; // set p to front of list, headOfFreeList to // chunk immediately following p = newBlock; headOfFreeList = &newBlock[1]; } return p;}If you've read Item 8, you know that when operator new can't satisfy a request for memory, it's supposed to perform a series of ritualistic steps involving new-handler functions and exceptions. There is no sign of such steps above. That's because this operator new gets all the memory it manages from ::operator new. That means this operator new can fail only if ::operator new does. But if ::operator new fails, it must engage in the new-handling ritual (possibly culminating in the throwing of an exception), so there is no need for Airplane's operator new to do it, too. In other words, the new-handler behavior is there, you just don't see it, because it's hidden inside ::operator new.Given this operator new, the only thing left to do is provide the obligatory definitions of Airplane's static data members: Airplane *Airplane::headOfFreeList; // these definitions // go in an implemen-const int Airplane::BLOCK_SIZE = 512; // tation file, not // a header fileThere's no need to explicitly set headOfFreeList to the null pointer, because static members are initialized to 0 by default. The value for BLOCK_SIZE, of course, determines the size of each memory block we get from ::operator new.This version of operator new will work just fine. Not only will it use a lot less memory for Airplane objects than the default operator new, it's also likely to be faster, possibly as much as two orders of magnitude faster. That shouldn't be surprising. After all, the general version of operator new has to cope with memory requests of different sizes, has to worry about internal and external fragmentation, etc., whereas your version of operator new just manipulates a couple of pointers in a linked list. It's easy to be fast when you don't have to be flexible.At long last we are in a position to discuss operator delete. Remember operator delete? This Item is about operator delete. As currently written, your Airplane class declares operator new, but it does not declare operator delete. Now consider what happens when a client writes the following, which is nothing if not eminently reasonable: Airplane *pa = new Airplane; // calls // Airplane::operator new...delete pa; // calls ::operator deleteIf you listen closely when you read this code, you can hear the sound of an airplane crashing and burning, with much weeping and wailing by the programmers who knew it. The problem is that operator new (the one defined in Airplane) returns a pointer to memory without any header information, but operator delete (the default, global one) assumes that the memory it's passed does contain header information! Surely this is a recipe for disaster.This example illustrates the general rule: operator new and operator delete must be written in concert so that they share the same assumptions. If you're going to roll your own memory allocation routine, be sure to roll one for deallocation, too. (For another reason why you should follow this advice, turn to the sidebar on placement new and placement delete in my article on counting objects in C++.)Here's how you solve the problem with the Airplane class: class Airplane { // same as before, except there'spublic: // now a decl. for operator delete ...
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -