tag 标签: destructor

相关博文
  • 热度 8
    2011-3-10 11:10
    1644 次阅读|
    0 个评论
    Here's how delete-expressions interact with destructors and deallocation functions in C++. I wrote a couple of articles on the distinction between allocating objects and allocating raw storage. In essence, when a program allocates an object, it not only allocates storage for the object but also initializes that storage with a value appropriate for the type of object that will occupy that storage. When a program just allocates storage, it leaves that storage uninitialized. Calling storage "raw" is a common way to emphasize that it's not initialized. The distinction between storage and objects is readily apparent in the behavioral difference between the Standard C malloc function and a C++ new-expression. Specifically, a C (or C++) expression such as: pw = (widget *)malloc(sizeof(widget)); allocates raw storage. Indeed, the malloc call allocates storage that's big enough and suitably aligned to hold an object of type widget , but it leaves that storage uninitialized. In contrast, a C++ new-expression as in: pw = new widget (); creates a widget object with a coherent initial value. The distinction between storage and objects carries over when allocating array objects. A call to malloc as in: pw = (widget *)malloc(10 * sizeof(widget)); allocates raw storage for an array of 10 widget s. By contrast, the C++ array new-expression in: pw = new widget ; allocates an array of 10 properly initialized widget s. Each new-expression is conceptually, if not actually, a two-step process: (1) allocate storage for an object, and (2) initialize it. For objects of class types, initializing an object usually involves calling a constructor. An array new-expression is also a two-step process, but the second step is a loop that initializes every array element. It should come as no surprise that the distinction between objects and raw storage also comes into play when you deallocate them. Just as allocating an object in C++ may involve calling a constructor, deallocating an object may involve calling a destructor. This month I'll explain how delete-expressions interact with destructors and deallocation functions in C++. I'll also explain how C programmers can employ a style of memory deallocation that mimics the behavior of C++ delete-expressions. Delete-expressions and destructors In C++, a destructor is a special class member function that "destroys" objects of that class type. Generally, to destroy an object means to deallocate or release the resources used by that object. A destructor's function name is always the same as its class name, but starting with a ~ (tilde, or "squiggle" if you prefer) as in: class widget { public: widget(); // a constructor ~widget(); // a destructor // ... }; Just as constructors provide guaranteed initialization for class objects, destructors provide guaranteed destruction. Just as you don't write explicit calls to constructors—compilers generate them for you—you don't write explicit destructor calls either. (Actually, you can write explicit destructor calls, but it's something you should avoid doing if possible.) Whenever you define an object with a class type, the compiler automatically plants calls to the object's destructor at the point(s) in the program where that object's lifetime ends—typically at function return statements and at the end (the closing brace) of function bodies and statement blocks. For guaranteed destruction to really be guaranteed, the compiler must generate a call to a destructor at every point where the program destroys an object, including in delete-expressions. Thus, if pw is a pointer to a single widget , a delete-expression such as: delete pw; doesn't just deallocate storage for that widget ; it applies widget 's destructor to the object addressed by pw to release any resources that the widget may have in its possession. Default initialization for scalar types, such as int and double , does nothing. When you dynamically allocate an object of scalar type, as in: int *pi = new int; the allocated object remains uninitialized. Default destruction for scalar types does nothing as well. An object of scalar type consumes no resources beyond its own storage, so there's nothing for its "destructor" to do. Thus, a delete-expression such as: delete pi; just deallocates the storage for the int addressed by pi . As I explained in previous blog Destroying everything in path , a destructor doesn't deallocate the storage for an object. 1 It releases the resources managed by the object, some of which may be storage. The storage for the object being destroyed will be deallocated by some other runtime agent of the program. For example, for a mythical widget object declared local to a function, the destructor for that widget executes just before the function returns. The destructor releases the widget 's resources, and then the runtime system deallocates the widget 's storage by popping it off the stack along with the rest of the function's stack frame. Just as the array new-expression in: pw = new widget ; applies the default widget constructor to each element in the storage allocated for the array, an array delete-expression such as: delete pw; applies the widget destructor to each array element. Whereas an array new-expression applies the constructors to the array element in ascending order by subscript starting at the 0th element, an array delete-expression applies the destructors in the reverse order. Delete-expressions and operator delete Just as a new-expression allocates memory by calling a function named operator new (rather than malloc ), a delete-expression deallocates memory by calling a function named operator delete (rather than free ). Each C++ environment provides a default implementation for a global operator delete , declared in the standard header as: void operator delete(void *p) throw (); The empty exception specification : throw () at the end of the function heading indicates that operator delete isn't supposed to allow any exceptions to propagate from the function. That is, operator delete may throw exceptions of various types, but it will catch those other exceptions before they can escape to the calling environment. However, as Scott Meyers explains, an empty exception specification doesn't really guarantee that no exceptions will propagate. 2 If pw is a pointer to an object of class type widget , a delete-expression such as: delete pw; translates more-or-less into something like: if (pw != NULL) { pw-~widget(); operator delete(pw); }      
  • 热度 10
    2011-3-9 11:07
    1772 次阅读|
    0 个评论
    Although I know both C and C++ quite well, I much prefer programming in C++. Features such as classes and access control help me partition my programs into components that are typically simpler and easier to use than they would be in C, with little or no loss of efficiency. Features such as constructors and destructors eliminate most of the resource management problems I encounter when I program in C. Despite my preference, I understand and appreciate that many programmers still have to program in C for pragmatic reasons. As I've demonstrated in some of my columns over the years, conscientious C programmers can do a reasonably good job of mimicking classes with constructors .1, 2 It just takes more effort and discipline in C than it does in C++. Unfortunately, approximating classes with destructors can take a lot more discipline. Let me show you why. Suppose widget is a type for an object that, among other things, manages a POSIX-style file referenced via an integer-valued file descriptor. In C, the definition for the widget type might look something like: typedef struct widget widget; struct widget {     int fd;     // a file descriptor     // other data members }; A function f that uses a local widget object might look like: int f(void) {     widget w;     w.fd = open(tmpnam(), O_RDWR);     // do whatever else f does     close(w.fd);     return 0; } The statement immediately after w 's declaration opens a temporary read-write file and assigns the associated file descriptor to w 's fd member. The later call to close closes that temporary file just before the function returns, thus avoiding a resource leak. However, a leak could easily occur elsewhere in function f . For example, suppose a conditional such as: if ( something bad happened )     return -1; were buried in the body of f . A call to f that terminates via this return-statement will fail to close w 's file, causing a resource leak. Rewriting this conditional as: if ( something bad happened ) {     close(w.fd);     return -1; } eliminates the leak. It's not hard to write once you know you need to do it. Unfortunately, it's easy to overlook. It also forces you to duplicate code, which increases code size and invites future maintenance problems. Using destructors in C++ reduces the possibility of resource leaks. You can define widget as a class with a constructor and a destructor, as follows: class widget { public:     widget();     ~widget();     // other member functions private:     int fd;     // other data members }; The constructor opens the file: inline widget::widget() {     fd = open(tmpnam(), O_RDWR); } and the destructor closes it: inline widget::~widget() {     close(fd); } Note that a destructor doesn't deallocate the storage for an object. It releases the resources managed by the object, some of which may be storage. The storage for the object being destroyed will be deallocated by some other runtime agent of the program. For example, for a widget object declared local to a function, the destructor for that widget executes just before the program returns from that function. The destructor releases the widget 's resources, and then the runtime system deallocates the widget 's storage by popping it off the stack along with the rest of the function's stack frame. Using this widget class simplifies function f : int f(void) {     widget w;     // do whatever else f does     return 0; } The compiler generates a call to the widget constructor as part of w 's declaration. The compiler also generates a call to the widget destructor applied to w as part of the function return, as it would for any other return elsewhere in the function body. When adding an error return somewhere in the middle of the function, you need not worry about closing the file inside the widget . For example: int f(void) {     widget w;     // do some of what f does     if ( something bad happened )        return -1;     // do the rest of what f does     return 0; } No matter which return-statement terminates f , the widget destructor always executes. Of course, you can mimic this behavior in C, but it's much harder to be sure that you got it right. Again, you define widget as a struct: struct widget {     int fd;    // a file descriptor     // other data members }; and the widget constructor and destructor as functions that each accept a pointer to a widget : inline void widget_construct(widget *w); {     w-fd = open(tmpnam(), O_RDWR); } inline void widget_destroy(widget *w) {     close(w-fd); } Then you can write function f as: int f(void) {     widget w;     widget_construct(w);     // do some of what f does     if ( something bad happened )     {        widget_destroy(w);        return -1;     }     // do the rest of what f does     widget_destroy(w);     return 0; } If the function has multiple returns and several local objects, you may wish to avoid calling the destructors for all those objects before every return. You can do it, but you have to introduce an additional local variable and fiddle with the control flow: int f(void) {     int rv = 0;     widget w;     widget_construct(w);     // do some of what f does     if ( something bad happened )         rv = -1;     else         // do the rest of what f does     widget_destroy(w);     return rv; } You might even have to resort to using goto-statements. Gasp! Compiler-generated destructor calls are almost completely effortless and clearly less error-prone than doing it by hand. Thanks to Steve Dewhurst ( www.semantics.org ) for some helpful insights on this subject. Endnotes: 1 Saks, Dan, "Incomplete Types as Abstractions," Embedded Systems Programming , December 2003, p. 43. 2. Saks, Dan, "Allocating objects vs. allocating storage," Embedded Systems Design , September, 2008, p. 11.