📄 mc3.htm
字号:
const string& imageFileName, Const string& audioClipFileName): theName(name), theAddress(address), theImage(0), theAudioClip(0){ if (imageFileName != "") { theImage = new Image(imageFileName); }if (audioClipFileName != "") {theAudioClip = new AudioClip(audioClipFileName); }}BookEntry::~BookEntry(){ delete theImage; delete theAudioClip;}The constructor initializes the pointers theImage and theAudioClip to null, then makes them point to real objects if the corresponding arguments are non-empty strings. The destructor deletes both pointers, thus ensuring that a BookEntry object doesn't give rise to a resource leak. Because C++ guarantees it's safe to delete null pointers, BookEntry's destructor need not check to see if the pointers actually point to something before deleting them.Everything looks fine here, and under normal conditions everything is fine, but under abnormal conditions under exceptional conditions things are not fine at all.Consider what will happen if an exception is thrown during execution of this part of the BookEntry constructor: if (audioClipFileName != "") { theAudioClip = new AudioClip(audioClipFileName);}An exception might arise because operator new (see Item 8) is unable to allocate enough memory for an AudioClip object. One might also arise because the AudioClip constructor itself throws an exception. Regardless of the cause of the exception, if one is thrown within the BookEntry constructor, it will be propagated to the site where the BookEntry object is being created.Now, if an exception is thrown during creation of the object theAudioClip is supposed to point to (thus transferring control out of the BookEntry constructor), who deletes the object that theImage already points to? The obvious answer is that BookEntry's destructor does, but the obvious answer is wrong. BookEntry's destructor will never be called. Never.C++ destroys only fully constructed objects, and an object isn't fully constructed until its constructor has run to completion. So if a BookEntry object b is created as a local object, void testBookEntryClass(){ BookEntry b("Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 01867");...}and an exception is thrown during construction of b, b's destructor will not be called. Furthermore, if you try to take matters into your own hands by allocating b on the heap and then calling delete if an exception is thrown, void testBookEntryClass(){ BookEntry *pb = 0; try { pb = new BookEntry("Addison-Wesley Publishing Company", "One Jacob Way, Reading, MA 01867"); ... } catch (...) { // catch all exceptions delete pb; // delete pb when an // exception is thrown throw; // propagate exception to } // caller delete pb; // delete pb normally}you'll find that the Image object allocated inside BookEntry's constructor is still lost, because no assignment is made to pb unless the new operation succeeds. If BookEntry's constructor throws an exception, pb will be the null pointer, so deleting it in the catch block does nothing except make you feel better about yourself. Using the smart pointer class auto_ptr<BookEntry> (see Item 9) instead of a raw BookEntry* won't do you any good either, because the assignment to pb still won't be made unless the new operation succeeds.There is a reason why C++ refuses to call destructors for objects that haven't been fully constructed, and it's not simply to make your life more difficult. It's because it would, in many cases, be a nonsensical thing possibly a harmful thing to do. If a destructor were invoked on an object that wasn't fully constructed, how would the destructor know what to do? The only way it could know would be if bits had been added to each object indicating how much of the constructor had been executed. Then the destructor could check the bits and (maybe) figure out what actions to take. Such bookkeeping would slow down constructors, and it would make each object larger, too. C++ avoids this overhead, but the price you pay is that partially constructed objects aren't automatically destroyed. (For an example of a similar trade-off involving efficiency and program behavior, turn to Item E13.)Because C++ won't clean up after objects that throw exceptions during construction, you must design your constructors so that they clean up after themselves. Often, this involves simply catching all possible exceptions, executing some cleanup code, then rethrowing the exception so it continues to propagate. This strategy can be incorporated into the BookEntry constructor like this: BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(0), theAudioClip(0){ try { // this try block is new if (imageFileName != "") { theImage = new Image(imageFileName); } if (audioClipFileName != "") { theAudioClip = new AudioClip(audioClipFileName); } } catch (...) { // catch any exception delete theImage; // perform necessary delete theAudioClip; // cleanup actions throw; // propagate the exception }}There is no need to worry about BookEntry's non-pointer data members. Data members are automatically initialized before a class's constructor is called, so if a BookEntry constructor body begins executing, the object's theName, theAddress, and thePhones data members have already been fully constructed. As fully constructed objects, these data members will be automatically destroyed when the BookEntry object containing them is, and there is no need for you to intervene. Of course, if these objects' constructors call functions that might throw exceptions, those constructors have to worry about catching the exceptions and performing any necessary cleanup before allowing them to propagate.You may have noticed that the statements in BookEntry's catch block are almost the same as those in BookEntry's destructor. Code duplication here is no more tolerable than it is anywhere else, so the best way to structure things is to move the common code into a private helper function and have both the constructor and the destructor call it: class BookEntry {public: ... // as beforeprivate: ... void cleanup(); // common cleanup statements};void BookEntry::cleanup(){ delete theImage; delete theAudioClip;}BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(0), theAudioClip(0){ try { ... // as before } catch (...) { cleanup(); // release resources throw; // propagate exception }}BookEntry::~BookEntry(){ cleanup();}This is nice, but it doesn't put the topic to rest. Let us suppose we design our BookEntry class slightly differently so that theImage and theAudioClip are constant pointers: class BookEntry {public: ... // as aboveprivate: ... Image * const theImage; // pointers are now AudioClip * const theAudioClip; // const};Such pointers must be initialized via the member initialization lists of BookEntry's constructors, because there is no other way to give const pointers a value (see Item E12). A common temptation is to initialize theImage and theAudioClip like this, // an implementation that may leak resources if an// exception is thrownBookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(imageFileName != "" ? new Image(imageFileName) : 0), theAudioClip(audioClipFileName != "" ? new AudioClip(audioClipFileName) : 0){}but this leads to the problem we originally wanted to eliminate: if an exception is thrown during initialization of theAudioClip, the object pointed to by theImage is never destroyed. Furthermore, we can't solve the problem by adding try and catch blocks to the constructor, because try and catch are statements, and member initialization lists allow only expressions. (That's why we had to use the ?: syntax instead of the if-then-else syntax in the initialization of theImage and theAudioClip.)Nevertheless, the only way to perform cleanup chores before exceptions propagate out of a constructor is to catch those exceptions, so if we can't put try and catch in a member initialization list, we'll have to put them somewhere else. One possibility is inside private member functions that return pointers with which theImage and theAudioClip should be initialized: class BookEntry {public: ... // as aboveprivate: ... // data members as aboveImage * initImage(const string& imageFileName); AudioClip * initAudioClip(const string& audioClipFileName);};BookEntry::BookEntry(const string& name, const string& address, const string& imageFileName, const string& audioClipFileName): theName(name), theAddress(address), theImage(initImage(imageFileName)), theAudioClip(initAudioClip(audioClipFileName)){}// theImage is initialized first, so there is no need to// worry about a resource leak if this initialization// fails. This function therefore handles no exceptionsImage * BookEntry::initImage(const string& imageFileName){ if (imageFileName != "") return new Image(imageFileName); else return 0;}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -