Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 50 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
50
Dung lượng
173,48 KB
Nội dung
580 Thinking in C++ www.BruceEckel.com initialization before the object is used, thus reintroducing a major source of bugs. It also turns out that many programmers seem to find C’s dynamic memory functions too confusing and complicated; it’s not uncommon to find C programmers who use virtual memory machines allocating huge arrays of variables in the static storage area to avoid thinking about dynamic memory allocation. Because C++ is attempting to make library use safe and effortless for the casual programmer, C’s approach to dynamic memory is unacceptable. operator new The solution in C++ is to combine all the actions necessary to create an object into a single operator called new . When you create an object with new (using a new-expression ), it allocates enough storage on the heap to hold the object and calls the constructor for that storage. Thus, if you say MyType *fp = new MyType(1,2); at runtime, the equivalent of malloc(sizeof(MyType)) is called (often, it is literally a call to malloc( ) ), and the constructor for MyType is called with the resulting address as the this pointer, using (1,2) as the argument list. By the time the pointer is assigned to fp , it’s a live, initialized object – you can’t even get your hands on it before then. It’s also automatically the proper MyType type so no cast is necessary. The default new checks to make sure the memory allocation was successful before passing the address to the constructor, so you don’t have to explicitly determine if the call was successful. Later in the chapter you’ll find out what happens if there’s no memory left. You can create a new-expression using any constructor available for the class. If the constructor has no arguments, you write the new-expression without the constructor argument list: 13: Dynamic Object Creation 581 MyType *fp = new MyType; Notice how simple the process of creating objects on the heap becomes – a single expression, with all the sizing, conversions, and safety checks built in. It’s as easy to create an object on the heap as it is on the stack. operator delete The complement to the new-expression is the delete-expression , which first calls the destructor and then releases the memory (often with a call to free( ) ). Just as a new-expression returns a pointer to the object, a delete-expression requires the address of an object. delete fp; This destructs and then releases the storage for the dynamically allocated MyType object created earlier. delete can be called only for an object created by new . If you malloc( ) (or calloc( ) or realloc( ) ) an object and then delete it, the behavior is undefined. Because most default implementations of new and delete use malloc( ) and free( ) , you’d probably end up releasing the memory without calling the destructor. If the pointer you’re deleting is zero, nothing will happen. For this reason, people often recommend setting a pointer to zero immediately after you delete it, to prevent deleting it twice. Deleting an object more than once is definitely a bad thing to do, and will cause problems. A simple example This example shows that initialization takes place: //: C13:Tree.h #ifndef TREE_H #define TREE_H #include <iostream> 582 Thinking in C++ www.BruceEckel.com class Tree { int height; public: Tree(int treeHeight) : height(treeHeight) {} ~Tree() { std::cout << "*"; } friend std::ostream& operator<<(std::ostream& os, const Tree* t) { return os << "Tree height is: " << t->height << std::endl; } }; #endif // TREE_H ///:~ //: C13:NewAndDelete.cpp // Simple demo of new & delete #include "Tree.h" using namespace std; int main() { Tree* t = new Tree(40); cout << t; delete t; } ///:~ We can prove that the constructor is called by printing out the value of the Tree . Here, it’s done by overloading the operator<< to use with an ostream and a Tree* . Note, however, that even though the function is declared as a friend , it is defined as an inline! This is a mere convenience – defining a friend function as an inline to a class doesn’t change the friend status or the fact that it’s a global function and not a class member function. Also notice that the return value is the result of the entire output expression, which is an ostream& (which it must be, to satisfy the return value type of the function). Memory manager overhead When you create automatic objects on the stack, the size of the objects and their lifetime is built right into the generated code, because the compiler knows the exact type, quantity, and scope. Creating objects on the heap involves additional overhead, both in 13: Dynamic Object Creation 583 time and in space. Here’s a typical scenario. (You can replace malloc( ) with calloc( ) or realloc( ) .) You call malloc( ) , which requests a block of memory from the pool. (This code may actually be part of malloc( ) .) The pool is searched for a block of memory large enough to satisfy the request. This is done by checking a map or directory of some sort that shows which blocks are currently in use and which are available. It’s a quick process, but it may take several tries so it might not be deterministic – that is, you can’t necessarily count on malloc( ) always taking exactly the same amount of time. Before a pointer to that block is returned, the size and location of the block must be recorded so further calls to malloc( ) won’t use it, and so that when you call free( ) , the system knows how much memory to release. The way all this is implemented can vary widely. For example, there’s nothing to prevent primitives for memory allocation being implemented in the processor. If you’re curious, you can write test programs to try to guess the way your malloc( ) is implemented. You can also read the library source code, if you have it (the GNU C sources are always available). Early examples redesigned Using new and delete , the Stash example introduced previously in this book can be rewritten using all the features discussed in the book so far. Examining the new code will also give you a useful review of the topics. At this point in the book, neither the Stash nor Stack classes will “own” the objects they point to; that is, when the Stash or Stack object goes out of scope, it will not call delete for all the objects it points to. The reason this is not possible is because, in an attempt to be generic, they hold void pointers. If you delete a void pointer, the 584 Thinking in C++ www.BruceEckel.com only thing that happens is the memory gets released, because there’s no type information and no way for the compiler to know what destructor to call. delete void* is probably a bug It’s worth making a point that if you call delete for a void* , it’s almost certainly going to be a bug in your program unless the destination of that pointer is very simple; in particular, it should not have a destructor. Here’s an example to show you what happens: //: C13:BadVoidPointerDeletion.cpp // Deleting void pointers can cause memory leaks #include <iostream> using namespace std; class Object { void* data; // Some storage const int size; const char id; public: Object(int sz, char c) : size(sz), id(c) { data = new char[size]; cout << "Constructing object " << id << ", size = " << size << endl; } ~Object() { cout << "Destructing object " << id << endl; delete []data; // OK, just releases storage, // no destructor calls are necessary } }; int main() { Object* a = new Object(40, 'a'); delete a; void* b = new Object(40, 'b'); delete b; } ///:~ 13: Dynamic Object Creation 585 The class Object contains a void* that is initialized to “raw” data (it doesn’t point to objects that have destructors). In the Object destructor, delete is called for this void* with no ill effects, since the only thing we need to happen is for the storage to be released. However, in main( ) you can see that it’s very necessary that delete know what type of object it’s working with. Here’s the output: Constructing object a, size = 40 Destructing object a Constructing object b, size = 40 Because delete a knows that a points to an Object , the destructor is called and thus the storage allocated for data is released. However, if you manipulate an object through a void* as in the case of delete b , the only thing that happens is that the storage for the Object is released – but the destructor is not called so there is no release of the memory that data points to. When this program compiles, you probably won’t see any warning messages; the compiler assumes you know what you’re doing. So you get a very quiet memory leak. If you have a memory leak in your program, search through all the delete statements and check the type of pointer being deleted. If it’s a void* then you’ve probably found one source of your memory leak (C++ provides ample other opportunities for memory leaks, however). Cleanup responsibility with pointers To make the Stash and Stack containers flexible (able to hold any type of object), they will hold void pointers. This means that when a pointer is returned from the Stash or Stack object, you must cast it to the proper type before using it; as seen above, you must also cast it to the proper type before deleting it or you’ll get a memory leak. The other memory leak issue has to do with making sure that delete is actually called for each object pointer held in the 586 Thinking in C++ www.BruceEckel.com container. The container cannot “own” the pointer because it holds it as a void* and thus cannot perform the proper cleanup. The user must be responsible for cleaning up the objects. This produces a serious problem if you add pointers to objects created on the stack and objects created on the heap to the same container because a delete-expression is unsafe for a pointer that hasn’t been allocated on the heap. (And when you fetch a pointer back from the container, how will you know where its object has been allocated?) Thus, you must be sure that objects stored in the following versions of Stash and Stack are made only on the heap, either through careful programming or by creating classes that can only be built on the heap. It’s also important to make sure that the client programmer takes responsibility for cleaning up all the pointers in the container. You’ve seen in previous examples how the Stack class checks in its destructor that all the Link objects have been popped. For a Stash of pointers, however, another approach is needed. Stash for pointers This new version of the Stash class, called PStash , holds pointers to objects that exist by themselves on the heap, whereas the old Stash in earlier chapters copied the objects by value into the Stash container. Using new and delete , it’s easy and safe to hold pointers to objects that have been created on the heap. Here’s the header file for the “pointer Stash ”: //: C13:PStash.h // Holds pointers instead of objects #ifndef PSTASH_H #define PSTASH_H class PStash { int quantity; // Number of storage spaces int next; // Next empty space // Pointer storage: void** storage; 13: Dynamic Object Creation 587 void inflate(int increase); public: PStash() : quantity(0), storage(0), next(0) {} ~PStash(); int add(void* element); void* operator[](int index) const; // Fetch // Remove the reference from this PStash: void* remove(int index); // Number of elements in Stash: int count() const { return next; } }; #endif // PSTASH_H ///:~ The underlying data elements are fairly similar, but now storage is an array of void pointers, and the allocation of storage for that array is performed with new instead of malloc( ) . In the expression void** st = new void*[quantity + increase]; the type of object allocated is a void* , so the expression allocates an array of void pointers. The destructor deletes the storage where the void pointers are held rather than attempting to delete what they point at (which, as previously noted, will release their storage and not call the destructors because a void pointer has no type information). The other change is the replacement of the fetch( ) function with operator[ ] , which makes more sense syntactically. Again, however, a void* is returned, so the user must remember what types are stored in the container and cast the pointers when fetching them out (a problem that will be repaired in future chapters). Here are the member function definitions: //: C13:PStash.cpp {O} // Pointer Stash definitions #include "PStash.h" #include " /require.h" #include <iostream> #include <cstring> // 'mem' functions 588 Thinking in C++ www.BruceEckel.com using namespace std; int PStash::add(void* element) { const int inflateSize = 10; if(next >= quantity) inflate(inflateSize); storage[next++] = element; return(next - 1); // Index number } // No ownership: PStash::~PStash() { for(int i = 0; i < next; i++) require(storage[i] == 0, "PStash not cleaned up"); delete []storage; } // Operator overloading replacement for fetch void* PStash::operator[](int index) const { require(index >= 0, "PStash::operator[] index negative"); if(index >= next) return 0; // To indicate the end // Produce pointer to desired element: return storage[index]; } void* PStash::remove(int index) { void* v = operator[](index); // "Remove" the pointer: if(v != 0) storage[index] = 0; return v; } void PStash::inflate(int increase) { const int psz = sizeof(void*); void** st = new void*[quantity + increase]; memset(st, 0, (quantity + increase) * psz); memcpy(st, storage, quantity * psz); quantity += increase; delete []storage; // Old storage storage = st; // Point to new memory } ///:~ 13: Dynamic Object Creation 589 The add( ) function is effectively the same as before, except that a pointer is stored instead of a copy of the whole object. The inflate( ) code is modified to handle the allocation of an array of void* instead of the previous design, which was only working with raw bytes. Here, instead of using the prior approach of copying by array indexing, the Standard C library function memset( ) is first used to set all the new memory to zero (this is not strictly necessary, since the PStash is presumably managing all the memory correctly – but it usually doesn’t hurt to throw in a bit of extra care). Then memcpy( ) moves the existing data from the old location to the new. Often, functions like memset( ) and memcpy( ) have been optimized over time, so they may be faster than the loops shown previously. But with a function like inflate( ) that will probably not be used that often you may not see a performance difference. However, the fact that the function calls are more concise than the loops may help prevent coding errors. To put the responsibility of object cleanup squarely on the shoulders of the client programmer, there are two ways to access the pointers in the PStash : the operator[] , which simply returns the pointer but leaves it as a member of the container, and a second member function remove( ) , which returns the pointer but also removes it from the container by assigning that position to zero. When the destructor for PStash is called, it checks to make sure that all object pointers have been removed; if not, you’re notified so you can prevent a memory leak (more elegant solutions will be forthcoming in later chapters). A test Here’s the old test program for Stash rewritten for the PStash : //: C13:PStashTest.cpp //{L} PStash // Test of pointer Stash #include "PStash.h" #include " /require.h" #include <iostream> [...]... which is expressed as a catch clause In main( ), you see the other part of the picture, which is the trycatch clause The try block is surrounded by braces and contains all the code that may throw exceptions – in this case, any call to new that involves Framis objects Immediately following the try block is one or more catch clauses, each one specifying the type of exception that they catch In this case,... MemoryCheckerthat has a destructor that prints out the number of Widget pointers in your vector Create a 13: Dynamic Object Creation 611 program with a single global instance of MemoryCheckerand in main( ), dynamically allocate and destroy several objects and arrays of Widget Show that MemoryCheckerreveals memory leaks 612 Thinking in C+ + www.BruceEckel.com 14: Inheritance & Composition One of the most compelling... to Counted that prints a message Move through the vector and call f( ) for each object Repeat Exercise 5 using a PStash Repeat Exercise 5 using Stack4.h from Chapter 9 Dynamically create an array of objects of class Counted (from Exercise 1) Call delete for the resulting pointer, without the square brackets Explain the results Thinking in C+ + www.BruceEckel.com 9 10 11 12 13 14 15 Create an object... 590 Thinking in C+ + www.BruceEckel.com objects in the program It’s an undesirable effect of using void pointers as the underlying representation and will be fixed in later chapters The second test opens the source code file and reads it one line at a time into another PStash Each line is read into a string using getline( ) then a new string is created from line to make an , independent copy of that line... particularly if they aren’t efficient enough Also, you can modify what happens when the heap runs out of storage Exercises Solutions to selected exercises can be found in the electronic document The Thinking in C+ + Annotated Solution Guide, available for a small fee from www.BruceEckel.com 1 2 3 4 5 6 7 8 610 Create a class Countedthat contains an int id and a static int count The default constructor... array chunk, the default constructor and destructor are called for each object in the array Constructor calls Considering that MyType* f = new MyType; calls new to allocate a MyType-sized piece of storage, then invokes the MyType constructor on that storage, what happens if the storage allocation in new fails? The constructor is not called in that case, so although you still have an unsuccessfully created... robust recovery You replace the new-handler by including new.h and then calling set_new_handler( ) with the address of the function you want installed: //: C1 3:NewHandler.cpp // Changing the new-handler #include #include #include using namespace std; int count = 0; void out_of_memory() { cerr . do with making sure that delete is actually called for each object pointer held in the 586 Thinking in C+ + www.BruceEckel.com container. The container cannot “own” the pointer because it holds. "PStash.h" #include " /require.h" #include <iostream> #include <cstring> // 'mem' functions 588 Thinking in C+ + www.BruceEckel.com using namespace std; int PStash::add(void*. removed using templates (which you’ll learn about in Chapter 16). 592 Thinking in C+ + www.BruceEckel.com new & delete for arrays In C+ +, you can create arrays of objects on the stack or on