时间:2013.03.15
地点:基地二楼
——————————————————————————————
一、问题的引出
假设开发一个通信薄软件,该软件可放置人名、地址、电话号码等字段,还有一张照片和一段声音。我们的设计如下:
class Image{ //图像数据类 public: Image(const string& image_data_filename); ... };
class AudioClip{ //音频数据类 public: AudioClip(const string& audio_data_filename); ... };
class PhoneNumber{ ...}; //封装电话号码的类
class BookEntry{ //封装一条通信薄记录 public: BookEntry(const string& name, const string& address="", const stirng& image_filename="", const string& audio_clip_filename=”“); ~BookEntry(), void AddPhoneNumber(const PhoneNumber& number); //添加电话号码 ... private: string the_name_; string the_address_; list<PhoneNumber> the_phones_; Image* the_image_; AudioClip* the_audio_clip_; };该类的设计原则是没有提供默认构造函数,且name字段是必须提供的。它的构造函数可实现如下:
BookEntry::BookEntry(const string& name, const string& address, const string& image_file_name, const string& audio_clip_filename) :the_name_name_,the_address_(address),the_image_(0),the_audio_clip_(0){ if(image_file_name!=""){ the_image_=new Image(image_filename); } if(audio_clip_filename!=""){ the_audio_clip_=new AudioClip(audio_clip_filename); } } BookEntry::~BookEntry(){ delete the_image; delete the audio_clip; }在这里我们用构造函数将指针the_image_和the_audio_clip都初始化为null了,如果对应的自变量不是空字符串再让它指向真正的对象。析构函数则负责删除指针,C++中删除null指针也是安全的。
现在看这种方式可能存在的问题
当程序执行到
if(audio_clip_filename!=""){ the_audio_clip_=new AudioClip(audio_clip_filename); }时,the_image_对象已经构造完成,要是在构造the_audio_clip所指对象抛出异常,比如operator new无法分配足够的内存给AudioClip对象使用,或者AudioClip的构造函数抛出异常,但不管怎么样,只要异常发生在BookEntry构造函数内,此时一部分成员已经构造完成,一部分正在构造,异常将被传播至构造BookEntry对象的那一端,控制权转出BookEntry构造函数之外,那么现在来接管已经构造好了的部分成员呢。在C++中析构函数只会析构已经构造好的对象,此时是不会被调用的。
于是我们可能想到这样的办法,在new对象时进行异常处理,比如:
void TestBookEntrClass(){ BookEntry* pb=0; try{ pb=new BookEntry("Addison-Wesley Publishing Company","One Jacob Way,Reading,Ma 01867"); ... } catch(...){ //捕获所有异常 delete pb; //有异常抛出时,删除pb throw; //将异常传给调用者 } delete pb; //正常情况删除异常 }首先,这种到处delete的代码并不优雅,前面谈RAII技术时已经提及,其次,它根本没有解决问题。只有在new成功时,pb才将被赋值,而你在new对象时抛出异常,并不成功,所以pb还是个null指针,在catch块中做的删除是徒劳的,只是感觉上让你很爽。
智能指针的解决方案在此处也不管用了,因为这里涉及到new出来的对象赋值问题,在构造时发生了异常,new不会成功返回值给pb,此时的pb还是个空架子。
那么C++的设计者为什么不让析构函数可以析构尚未构造好的对象呢,如果可以,对于析构函数来说,因为对象尚未构造好,它如何知道自己改做什么事这是一个大问题,除非被加载到构造对象上的数据附带某些信息指示对象构造达到了什么样的程度,这样析构函数通过不断检测这些信息采取相应的对策。显然这样会达到降低构造对象的速度加重类的任务和开销。如此必须付出未完成构造的对象不能被自动析构的代价,在效率和程序行为之间做出取舍。
——————————————————————————————
问题出来了,你可能想到办法也出来了,并给出了这种不可行性。正确的方法时必须自己设计构造函数处理这种对象部分构造完成的场合。如下:
BookEntr::BookEntry(const string& name, const string& address, const string& image_file_name, const string& audio_clip_filename) :the_name_(name),the_address_(address),the_image_(0),the_audio_clip_(0){ try{ if(image_filename!=""){ theimage=new Image(image_filename); } if(audio_clip_filename!=""){ the_audio_clip=new AudioClip(audio_clip_filename); } } catch(...){ //捕捉所有异常 delete the_image_; //执行必要的清除工作 delete the_audio_clip; throw; //继续传播这个异常 } }需要注意的是对象的数据成员时在构造函数开始之前通过初始化列表就已经完全构造好了,这些数据成员会被自动销毁无需顾虑,在这里catch块内的动作和析构函数的动作一样。我们希望共享代码,使得程序变得美观,我们可以把这段代码抽出来,放在一个private辅助函数内,让构造函数和析构函数去调用它既可,如下:
void BookEntry::ClearUp(){
delete the_image_;
delete the_audio_clip;
}
类中相应部分做恰当处理,这样看起来就舒服多了。
——————————————————————————————
尽管上述是一个不错的方法,但我觉得最好的方法莫过于使用RAII技术,将资源封装为类,转换为局部对象来管理资源。如下:
class BookEntry{ public: ... private: ... const auto_ptr<Image> the_image_; const auto_ptr<AudioClip> the_audio_clip_; };这样我们甚至可在初始化列表中初始化成员,如:
BookEntry::BookEntry(const string& name, const string& address, const string& image_file_name, const string& audio_clip_filename) :the_name_(name),the_address_(address), the_image_(image_filename!=""?new Image(image_filename):0), the_aduio_clip_(audio_clip_filename!=""?new AudioClip(audio_clip_filename:0){}这样,我们在初始化期间若有任何异常出现,由于此时the_image_已经是一个完整构造好的对象,且是一个局部类成员,它会离开作用域被自动销毁,就像the_name_,the-address_等成员变量一样,不再需要手动删除所指对象。auto_ptr的功能即封装一个指针成为一个对象,由于该对象现在时一个简单的成员变量,当异常出现时会自动销毁,而auto_ptr对象在销毁时会自动调用析构将封装的指针delete。