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

📄 ec2.htm

📁 一个非常适合初学者入门的有关c++的文档
💻 HTM
📖 第 1 页 / 共 4 页
字号:
 Effective C++, 2E | Chapter 2: Memory Management Back to Item 4: Prefer C++-style comments.Continue to Item 5: Use the same form in corresponding uses of new and delete.Memory ManagementMemory management concerns in C++ fall into two general camps: getting it right and making it perform efficiently. Good programmers understand that these concerns should be addressed in that order, because a program that is dazzlingly fast and astoundingly small is of little use if it doesn't behave the way it's supposed to. For most programmers, getting things right means calling memory allocation and deallocation routines correctly. Making things perform efficiently, on the other hand, often means writing custom versions of the allocation and deallocation routines. Getting things right there is even more important.On the correctness front, C++ inherits from C one of its biggest headaches, that of potential memory leaks. Even virtual memory, wonderful invention though it is, is finite, and not everybody has virtual memory in the first place.In C, a memory leak arises whenever memory allocated through malloc is never returned through free. The names of the players in C++ are new and delete, but the story is much the same. However, the situation is improved somewhat by the presence of destructors, because they provide a convenient repository for calls to delete that all objects must make when they are destroyed. At the same time, there is more to worry about, because new implicitly calls constructors and delete implicitly calls destructors. Furthermore, there is the complication that you can define your own versions of operator new and operator delete, both inside and outside of classes. This gives rise to all kinds of opportunities to make mistakes. The following Items (as well as Item M8) should help you avoid some of the most common ones. Back to Memory ManagementContinue to Item 6:Use delete on pointer members in destructors.Item 5: Use the same form in corresponding uses of new and delete.What's wrong with this picture? string *stringArray = new string[100];...delete stringArray;Everything here appears to be in order the use of new is matched with a use of delete but something is still quite wrong: your program's behavior is undefined. At the very least, 99 of the 100 string objects pointed to by stringArray are unlikely to be properly destroyed, because their destructors will probably never be called.When you use new, two things happen. First, memory is allocated (via the function operator new, about which I'll have more to say in Items 7-10 as well as Item M8). Second, one or more constructors are called for that memory. When you use delete, two other things happen: one or more destructors are called for the memory, then the memory is deallocated (via the function operator delete see Items 8 and M8). The big question for delete is this: how many objects reside in the memory being deleted? The answer to that determines how many destructors must be called.Actually, the question is simpler: does the pointer being deleted point to a single object or to an array of objects? The only way for delete to know is for you to tell it. If you don't use brackets in your use of delete, delete assumes a single object is pointed to. Otherwise, it assumes that an array is pointed to: string *stringPtr1 = new string;string *stringPtr2 = new string[100];...delete stringPtr1;           // delete an objectdelete [] stringPtr2;        // delete an array of                             // objectsWhat would happen if you used the "[]" form on stringPtr1? The result is undefined. What would happen if you didn't use the "[]" form on stringPtr2? Well, that's undefined too. Furthermore, it's undefined even for built-in types like ints, even though such types lack destructors. The rule, then, is simple: if you use [] when you call new, you must use [] when you call delete. If you don't use [] when you call new, don't use [] when you call delete.This is a particularly important rule to bear in mind when you are writing a class containing a pointer data member and also offering multiple constructors, because then you've got to be careful to use the same form of new in all the constructors to initialize the pointer member. If you don't, how will you know what form of delete to use in your destructor? For a further examination of this issue, see Item 11.This rule is also important for the typedef-inclined, because it means that a typedef's author must document which form of delete should be employed when new is used to conjure up objects of the typedef type. For example, consider this typedef: typedef string AddressLines[4];      // a person's address                                     // has 4 lines, each of                                     // which is a stringBecause AddressLines is an array, this use of new, string *pal = new AddressLines;      // note that "new                                     // AddressLines" returns                                     // a string*, just like                                     // "new string[4]" wouldmust be matched with the array form of delete: delete pal;                          // undefined!delete [] pal;                       // fineTo avoid such confusion, you're probably best off abstaining from typedefs for array types. That should be easy, however, because the standard C++ library (see Item 49) includes string and vector templates that reduce the need for built-in arrays to nearly zero. Here, for example, AddressLines could be defined to be a vector of strings. That is, AddressLines could be of type vector<string>. Back to Item 5: Use the same form in corresponding uses of new and delete.Continue to Item 7: Be prepared for out-of-memory conditions.Item 6: Use delete on pointer members in destructors.Most of the time, classes performing dynamic memory allocation will use new in the constructor(s) to allocate the memory and will later use delete in the destructor to free up the memory. This isn't too difficult to get right when you first write the class, provided, of course, that you remember to employ delete on all the members that could have been assigned memory in any constructor.However, the situation becomes more difficult as classes are maintained and enhanced, because the programmers making the modifications to the class may not be the ones who wrote the class in the first place. Under those conditions, it's easy to forget that adding a pointer member almost always requires each of the following: Initialization of the pointer in each of the constructors. If no memory is to be allocated to the pointer in a particular constructor, the pointer should be initialized to 0 (i.e., the null pointer). Deletion of the existing memory and assignment of new memory in the assignment operator. (See also Item 17.) Deletion of the pointer in the destructor.If you forget to initialize a pointer in a constructor, or if you forget to handle it inside the assignment operator, the problem usually becomes apparent fairly quickly, so in practice those issues don't tend to plague you. Failing to delete the pointer in the destructor, however, often exhibits no obvious external symptoms. Instead, it manifests itself as a subtle memory leak, a slowly growing cancer that will eventually devour your address space and drive your program to an early demise. Because this particular problem doesn't usually call attention to itself, it's important that you keep it in mind whenever you add a pointer member to a class.Note, by the way, that deleting a null pointer is always safe (it does nothing). Thus, if you write your constructors, your assignment operators, and your other member functions such that each pointer member of the class is always either pointing to valid memory or is null, you can merrily delete away in the destructor without regard for whether you ever used new for the pointer in question.There's no reason to get fascist about this Item. For example, you certainly don't want to use delete on a pointer that wasn't initialized via new, and, except in the case of smart pointer objects (see Item M28), you almost never want to delete a pointer that was passed to you in the first place. In other words, your class destructor usually shouldn't be using delete unless your class members were the ones who used new in the first place.Speaking of smart pointers, one way to avoid the need to delete pointer members is to replace those members with smart pointer objects like the standard C++ Library's auto_ptr. To see how this can work, take a look at Items M9 and M10. Back to Item 6: Use delete on pointer members in destructors.Continue to Item 8: Adhere to convention when writing operator new and operator delete.Item 7: Be prepared for out-of-memory conditions.When operator new can't allocate the memory you request, it throws an exception. (It used to return 0, and some older compilers still do that. You can make your compilers do it again if you want to, but I'll defer that discussion until the end of this Item.) Deep in your heart of hearts, you know that handling out-of-memory exceptions is the only truly moral course of action. At the same time, you are keenly aware of the fact that doing so is a pain in the neck. As a result, chances are that you omit such handling from time to time. Like always, perhaps. Still, you must harbor a lurking sense of guilt. I mean, what if new really does yield an exception?You may think that one reasonable way to cope with this matter is to fall back on your days in the gutter, i.e., to use the preprocessor. For example, a common C idiom is to define a type-independent macro to allocate memory and then check to make sure the allocation succeeded. For C++, such a macro might look something like this: #define NEW(PTR, TYPE)                       \    try { (PTR) = new TYPE; }                \    catch (std::bad_alloc&) { assert(0); }("Wait! What's this std::bad_alloc business?", you ask. bad_alloc is the type of exception operator new throws when it can't satisfy a memory allocation request, and std is the name of the namespace (see Item 28) where bad_alloc is defined. "Okay," you continue, "what's this assert business?" Well, if you look in the standard C include file <assert.h> (or its namespace-savvy C++ equivalent, <cassert> see Item 49), you'll find that assert is a macro. The macro checks to see if the expression it's passed is non-zero, and, if it's not, it issues an error message and calls abort. Okay, it does that only when the standard macro NDEBUG isn't defined, i.e., in debug mode. In production mode, i.e., when NDEBUG is defined, assert expands to nothing to a void statement. You thus check assertions only when debugging.)This NEW macro suffers from the common error of using an assert to test a condition that might occur in production code (after all, you can run out of memory at any time), but it also has a drawback specific to C++: it fails to take into account the myriad ways in which new can be used. There are three common syntactic forms for getting new objects of type T, and you need to deal with the possibility of exceptions for each of these forms: new T;new T(constructor arguments);new T[size];This oversimplifies the problem, however, because clients can define their own (overloaded) versions of operator new, so programs may contain an arbitrary number of different syntactic forms for using new.How, then, to cope? If you're willing to settle for a very simple error-handling strategy, you can set things up so that if a request for memory cannot be satisfied, an error-handling function you specify is called. This strategy relies on the convention that when operator new cannot satisfy a request, it calls a client-specifiable error-handling function often called a new-handler before it throws an exception. (In truth, what operator new really does is slightly more complicated. Details are provided in Item 8.)To specify the out-of-memory-handling function, clients call set_new_handler, which is specified in the header <new> more or less like this: typedef void (*new_handler)();new_handler set_new_handler(new_handler p) throw();As you can see, new_handler is a typedef for a pointer to a function that takes and returns nothing, and set_new_handler is a function that takes and returns a new_handler.set_new_handler's parameter is a pointer to the function operator new should call if it can't allocate the requested memory. The return value of set_new_handler is a pointer to the function in effect for that purpose before set_new_handler was called.You use set_new_handler like this: // function to call if operator new can't allocate enough memoryvoid noMoreMemory(){  cerr << "Unable to satisfy request for memory\n";  abort();}  int main(){  set_new_handler(noMoreMemory);  int *pBigDataArray = new int[100000000];  ...}If, as seems likely, operator new is unable to allocate space for 100,000,000 integers, noMoreMemory will be called, and the program will abort after issuing an error message. This is a marginally better way to terminate the program than a simple core dump. (By the way, consider what happens if memory must be dynamically allocated during the course of writing the error message to cerr...)When operator new cannot satisfy a request for memory, it calls the new-handler function not once, but repeatedly until it can find enough memory. The code giving rise to these repeated calls is shown in Item 8, but this high-level description is enough to conclude that a well-designed new-handler function must do one of the following: Make more memory available. This may allow operator new's next attempt to allocate the memory to succeed. One way to implement this strategy is to allocate a large block of memory at program start-up, then release it the first time the new-handler is invoked. Such a release is often accompanied by some kind of warning to the user that memory is low and that future requests may fail unless more memory is somehow made available. Install a different new-handler. If the current new-handler can't make any more memory available, perhaps it knows of a different new-handler that is more resourceful. If so, the current new-handler can install the other new-handler in its place (by calling set_new_handler). The next time operator new calls the new-handler function, it will get the one most recently installed. (A variation on this theme is for a new-handler to modify its own behavior, so the next time it's invoked, it does something different. One way to achieve this is to have the new-handler modify static or global data that affects the new-handler's behavior.) Deinstall the new-handler, i.e., pass the null pointer to set_new_handler. With no new-handler installed, operator new will throw an exception of type std::bad_alloc when its attempt to allocate memory is unsuccessful. Throw an exception of type std::bad_alloc or some type derived from std::bad_alloc. Such exceptions will not be caught by operator new, so they will propagate to the site originating the request for memory. (Throwing an exception of a different type will violate operator new's exception specification. The default action when that happens is to call abort, so if your new-handler is going to throw an exception, you definitely want to make sure it's from the std::bad_alloc hierarchy. For more information on exception specifications, see Item M14.) Not return, typically by calling abort or exit, both of which are found in the standard C library (and thus in the standard C++ library see Item 49).These choices give you considerable flexibility in implementing new-handler functions.Sometimes you'd like to handle memory allocation failures in different ways, depending on the class of the object being allocated: class X {public:  static void outOfMemory();  ...};class Y {public:  static void outOfMemory();  ...};X* p1 = new X;      // if allocation is unsuccessful,                    // call X::outOfMemoryY* p2 = new Y;      // if allocation is unsuccessful,                    // call Y::outOfMemoryC++ has no support for class-specific new-handlers, but it doesn't need to. You can implement this behavior yourself. You just have each class provide its own versions of set_new_handler and operator new. The class's set_new_handler allows clients to specify the new-handler for the class (just like the standard set_new_handler allows clients to specify the global new-handler). The class's operator new ensures that the class-specific new-handler is used in place of the global new-handler when memory for class objects is allocated.Consider a class X for which you want to handle memory allocation failures. You'll have to keep track of the function to call when operator new can't allocate enough memory for an object of type X, so you'll declare a static member of type new_handler to point to the new-handler function for the class. Your class X will look something like this: class X {public:  static new_handler set_new_handler(new_handler p);  static void * operator new(size_t size);private:  static new_handler currentHandler;};Static class members must be defined outside the class definition. Because you'll want to use the default initialization of static objects to 0, you'll define X::currentHandler without initializing it: new_handler X::currentHandler;      // sets currentHandler                                    // to 0 (i.e., null) by                                    // defaultThe set_new_handler function in class X will save whatever pointer is passed to it. It will return whatever pointer had been saved prior to the call. This is exactly what the standard version of set_new_handler does: new_handler X::set_new_handler(new_handler p){  new_handler oldHandler = currentHandler;  currentHandler = p;  return oldHandler;}Finally, X's operator new will do the following: Call the standard set_new_handler with X's error-handling function. This will install X's new-handler as the global new- handler. In the code below, notice how you explicitly reference the std scope (where the standard set_new_handler resides) by using the "::" notation. Call the global operator new to actually allocate the requested memory. If the initial attempt at allocation fails, the global operator new will invoke X's new-handler, because that function was just installed as the global new-handler. If the global operator new is ultimately unable to find a way to allocate the requested memory, it will throw a std::bad_alloc exception, which X's operator new will catch. X's operator new will then restore the global new-handler that was originally in place, and it will return by propagating the exception. Assuming the global operator new was able to successfully allocate enough memory for an object of type X, X's operator new will again call the standard set_new_handler to restore the global error-handling function to what it was originally. It will then return a pointer to the allocated memory. Here's how you say all that in C++: void * X::operator new(size_t size){  new_handler globalHandler =                // install X's    std::set_new_handler(currentHandler);    // handler  void *memory;  try {                                      // attempt    memory = ::operator new(size);           // allocation  }  catch (std::bad_alloc&) {                  // restore    std::set_new_handler(globalHandler);     // handler;    throw;                                   // propagate  }                                          // exception

⌨️ 快捷键说明

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