(1) C++异常 一、 综述 我很少敢为自己写的东西弄个详解的标题,之所以这次敢于这样,自然还算是有点底气的。并且也以此为动力,督促自己好好的将这两个东西研究透。 当年刚开始工作的时候,第一个工作就是学习breakpad的源代码,然后了解其原理,为公司写一个ExceptionHandle的库,以处理服务器及客户端的未处理异常(unhandle exception),并打下dump,以便事后分析,当年这个功能在有breakpad的示例在前时,实现难度并不大,无非就是调用了SetUnhandledExceptionFilter等函数,让windows在出现未处理异常时让自己的回调函数接管操作,然后利用其struct _EXCEPTION_POINTERS* ExceptionInfo的指针,通过MiniDumpWriteDump API将Dump写下来。但是仍记得,那时看到《Windows 核心编程》第五部分关于结构化异常处理的描述时那种因为得到新鲜知识时的兴奋感,那是我第一次这样接近Windows系统的底层机制,如同以前很多次说过的,那以后我很投入的捧着读完了《Windows 核心编程》,至今受益匪浅。当时也有一系列一边看源代码一边写下心得的时候,想想,都已经一年以前的事情了。 《读windows核心编程,结构化异常部分,理解摘要》 《Breakpad在进程中完成dump的流程描述》 《Breakpad 使用方法理解文档》 直到最近,为了控制服务器在出现异常时不崩溃,(以前是崩溃的时候打Dump),对SEH(windows结构化异常)又进行了进一步的学习,做到了在服务器出现了异常情况(例如空指针的访问)时,服务器打下Dump,并继续运行,并不崩溃,结合以前也是我写的监控系统,通知监控客户端报警,然后就可以去服务器上取回dump,并分析错误,这对服务器的稳定性有很大的帮助,不管我们对服务器的稳定性进行了多少工作,作为C++程序,偶尔的空指针访问,几乎没有办法避免。。。。。。但是,这个工作,对这样的情况起到了很好的缓冲作用。在这上面工作许久,有点心得,写下来,供大家分享,同时也是给很久以后的自己分享。 二、 为什么需要异常 《Windows核心编程》第4版第13章开头部分描述了一个美好世界,即所编写的代码永远不会执行失败,总是有足够的内存,不存在无效的指针。。。。但是,那是不存在的世界,于是,我们需要有一种异常的处理措施,在C语言中最常用的(其实C++中目前最常用的还是)是利用错误代码(Error Code)的形式。 这里也为了更好的说明,也展示一下Error Code的示例代码: Error Code常用方式: 1.最常用的就是通过返回值判断了: 比如C Runtime Library中的fopen接口,一旦返回NULL,Win32 API中的CreateFiley一旦返回INVALID_HANDLE_VALUE,就表示执行失败了。 2.当返回值不够用(或者携带具体错误信息不够的)时候,C语言中也常常通过一个全局的错误变量来表示错误。 比如C Runtime Library中的errno 全局变量,Win32 API中的GetLastError,WinSock中的WSAGetLastError函数就是这种实现。 既然Error Code在这么久的时间中都是可用的,好用的,为什么我们还需要其他东西呢? 这里可以参考一篇比较浅的文章。《错误处理和异常处理,你用哪一个》,然后本人比较钦佩的pongba还有一篇比较深的文章:《错误处理(Error-Handling):为何、何时、如何(rev#2)》,看了后你一定会大有收获。当pongba列出了16条使用异常的好处后,我都感觉不到我还有必要再去告诉你为什么我们要使用异常了。 但是,这里在其无法使用异常的意外情况下,(实际是《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》一书中所写) 一,用异常没有带来明显的好处的时候:比如所有的错 误都会在立即调用端解决掉或者在非常接近立即调用端的地方解决掉。 二,在实际作了测定之后发现异常的抛出和捕获导致了显著的时间开销:这通常只有两种情 况,要么是在内层循环里面,要么是因为被抛出的异常根本不对应于一个错误。 很明显,文中列举的都是完全理论上理想的情况,受制于国内的开发环境,无论多么好的东西也不一定实用,你能说国内多少地方真的用上了敏捷开发的实践经验?这里作为现实考虑,补充几个没有办法使用异常的情况: 一.所在的项目组中没有合理的使用RAII的习惯及其机制,比如无法使用足够多的smart_ptr时,最好不要使用异常,因为异常和RAII的用异常不用RAII就像吃菜不放盐一样。这一点在后面论述一下。 二.当项目组中没有使用并捕获异常的习惯时,当项目组中认为使用异常是奇技淫巧时不要使用异常。不然,你自认为很好的代码,会在别人眼里不可理解并且作为异类,接受现实。 三、 基础篇 先回顾一下标准C++的异常用法 1.C++标准异常 只有一种语法,格式类似: try { } catch() { } 经常简写为try-catch,当然,也许还要算上throw。格式足够的简单。 以下是一个完整的例子: MyException: #include < <string> #include <iostream> using namespace std; class MyException : public exception { public: MyException(const char* astrDesc) { mstrDesc = astrDesc; } string mstrDesc; }; int _tmain(int argc, _TCHAR* argv[]) { try { throw MyException("A My Exception"); } catch(MyException e) { cout <<e.mstrDesc <<endl; } return 0; } 这里可以体现几个异常的优势,比如自己的异常继承体系,携带足够多的信息等等。另外,虽然在基础篇,这里也讲讲C++中异常的语义, 如下例子中, throwException: #include <string> #include <iostream> using namespace std; class MyException : public exception { public: MyException(const char* astrDesc) { mstrDesc = astrDesc; } MyException(const MyException& aoOrig) { cout <<"Copy Constructor MyException" <<endl; mstrDesc = aoOrig.mstrDesc; } MyException& operator=(const MyException& aoOrig) { cout <<"Copy Operator MyException" <<endl; if(&aoOrig == this) { return *this; } mstrDesc = aoOrig.mstrDesc; return *this; } ~MyException() { cout <<"~MyException" <<endl; } string mstrDesc; }; void exceptionFun() { try { throw MyException("A My Exception"); } catch(MyException e) { cout <<e.mstrDesc <<" In exceptionFun." <<endl; e.mstrDesc = "Changed exception."; throw; } } int _tmain(int argc, _TCHAR* argv[]) { try { exceptionFun(); } catch(MyException e) { cout <<e.mstrDesc <<" Out exceptionFun." <<endl; throw; } return 0; } 输出: Copy Constructor MyException A My Exception In exceptionFun. ~MyException Copy Constructor MyException A My Exception Out exceptionFun. ~MyException 可以看出当抛出C++异常的copy语义,抛出异常后调用了Copy Constructor,用新建的异常对象传入catch中处理,所以在函数中改变了此异常对象后,再次抛出原异常,并不改变原有异常。 这里我们经过一点小小的更改,看看会发生什么: throwAnotherException #include <string> #include <iostream> using namespace std; class MyException : public exception { public: MyException(const char* astrDesc) { mstrDesc = astrDesc; } MyException(const MyException& aoOrig) { cout <<"Copy Constructor MyException" <<endl; mstrDesc = aoOrig.mstrDesc; } MyException& operator=(const MyException& aoOrig) { cout <<"Copy Operator MyException" <<endl; if(&aoOrig == this) { return *this; } mstrDesc = aoOrig.mstrDesc; return *this; } ~MyException() { cout <<"~MyException" <<endl; } string mstrDesc; }; void exceptionFun() { try { throw MyException("A My Exception"); } catch(MyException e) { cout <<e.mstrDesc <<" In exceptionFun." <<endl; e.mstrDesc = "Changed exception."; throw e; } } int _tmain(int argc, _TCHAR* argv[]) { try { exceptionFun(); } catch(MyException e) { cout <<e.mstrDesc <<" Out exceptionFun." <<endl; throw; } return 0; } 这里和throwException程序的唯一区别就在于不是抛出原有异常,而是抛出改变后的异常,输出如下: Copy Constructor MyException A My Exception In exceptionFun. Copy Constructor MyException Copy Constructor MyException ~MyException ~MyException Changed exception. Out exceptionFun. ~MyException 你会发现连续的两次Copy Constructor都是改变后的异常对象,这点很不可理解。。。。。。。因为事实上一次就够了。但是理解C++的Copy异常处理语义就好理解了,一次是用于传入下一次的catch语句中的,还有一次是留下来,当在外层catch再次throw时,已经抛出的是改变过的异常对象了,我用以下例子来验证这点: throwTwiceException #include <string> #include <iostream> using namespace std; class MyException : public exception { public: MyException(const char* astrDesc) { mstrDesc = astrDesc; } MyException(const MyException& aoOrig) { cout <<"Copy Constructor MyException" <<endl; mstrDesc = aoOrig.mstrDesc; } MyException& operator=(const MyException& aoOrig) { cout <<"Copy Operator MyException" <<endl; if(&aoOrig == this) { return *this; } mstrDesc = aoOrig.mstrDesc; return *this; } ~MyException() { cout <<"~MyException" <<endl; } string mstrDesc; }; void exceptionFun() { try { throw MyException("A My Exception"); } catch(MyException e) { cout <<e.mstrDesc <<" In exceptionFun." <<endl; e.mstrDesc = "Changed exception."; throw e; } } void exceptionFun2() { try { exceptionFun(); } catch(MyException e) { cout <<e.mstrDesc <<" In exceptionFun2." <<endl; throw; } } int _tmain(int argc, _TCHAR* argv[]) { try { exceptionFun2(); } catch(MyException e) { cout <<e.mstrDesc <<" Out exceptionFuns." <<endl; throw; } return 0; } 输出如下,印证了我上面的说明。 Copy Constructor MyException A My Exception In exceptionFun. Copy Constructor MyException Copy Constructor MyException ~MyException ~MyException Changed exception. In exceptionFun2. ~MyException Copy Constructor MyException Changed exception. Out exceptionFuns. 上面像语言律师一样的讨论着C++本来已经足够简单的异常语法,其实简而言之,C++总是保持着一个上次抛出的异常用于用户再次抛出,并copy一份在catch中给用户使用。 但是,实际上,会发现,其实原有的异常对象是一直向上传递的,只要你不再次抛出其他异常,真正发生复制的地方在于你catch异常的时候,这样,当catch时使用引用方式,那么就可以避免这样的复制。 referenceCatch #include <string> #include <iostream> using namespace std; class MyException : public exception { public: MyException(const char* astrDesc) { mstrDesc = astrDesc; } MyException(const MyException& aoOrig) { cout <<"Copy Constructor MyException: " <<aoOrig.mstrDesc <<endl; mstrDesc = aoOrig.mstrDesc; } MyException& operator=(const MyException& aoOrig) { cout <<"Copy Operator MyException:" <<aoOrig.mstrDesc <<endl; if(&aoOrig == this) { return *this; } mstrDesc = aoOrig.mstrDesc; return *this; } ~MyException() { cout <<"~MyException" <<endl; } string mstrDesc; }; void exceptionFun() { try { throw MyException("A My Exception"); } catch(MyException& e) { cout <<e.mstrDesc <<" In exceptionFun." <<endl; e.mstrDesc = "Changed exception."; throw; } } void exceptionFun2() { try { exceptionFun(); } catch(MyException e) { cout <<e.mstrDesc <<" In exceptionFun2." <<endl; throw; } } int _tmain(int argc, _TCHAR* argv[]) { try { exceptionFun2(); } catch(MyException e) { cout <<e.mstrDesc <<" Out exceptionFuns." <<endl; throw; } return 0; } 上例中,使用引用方式来捕获异常,输出如下: A My Exception In exceptionFun. Copy Constructor MyException: Changed exception. Changed exception. In exceptionFun2. ~MyException Copy Constructor MyException: Changed exception. Changed exception. Out exceptionFuns. ~MyException 完全符合C++的引用语义。 基本可以发现,做了很多无用功,因为try-catch无非是一层迷雾,其实这里复制和引用都还是遵循着原来的C++简单的复制,引用语义,仅仅这一层迷雾,让我们看不清楚原来的东西。所以,很容易理解一个地方throw一个对象,另外一个地方catch一个对象一定是同一个对象,其实不然,是否是原来那个对象在于你传递的方式,这就像这是个参数,通过catch函数传递进来一样,你用的是传值方式,自然是通过了复制,通过传址方式,自然是原有对象,仅此而已。 另外,最终总结一下,《C++ Coding Standards》73条建议Throw by value,catch by reference就是因为本文描述的C++的异常特性如此,所以才有此建议,并且,其补上了一句,重复提交异常的时候用throw; 四、 参考资料 1. Windows核心编程(Programming Applications for Microsoft Windows),第4版,Jeffrey Richter著,黄陇,李虎译,机械工业出版社 2. MSDN—Visual Studio 2005 附带版,Microsoft 3. 错误处理和异常处理,你用哪一个,apollolegend 4. 错误处理(Error-Handling):为何、何时、如何(rev#2),刘未鹏 (2) 智能指针与C++异常 一、 综述 《异常处理与MiniDump详解(1) C++异常》稍微回顾了下C++异常的语法及其类似于函数参数传递的抛出异常对象的copy,引用语义,但是有个问题没有详细讲,那就是C++异常的绝佳搭档,智能指针。在没有智能指针的时候会感觉C++的异常少了一个用于释放资源的finally语法,但是C++没有这样的语法是有理由的,因为C++的智能指针。假如不用智能指针仅仅使用异常,那就像是吃一道没有放肉的辣椒炒肉一样。。。。。。。。。。。 智能指针对于C++的重要性很多人可能并没有认识到,看看C++相关的书籍吧,几乎每本都有其痕迹,从《C++ Primer》,TCPL到《C++沉思录》《C++编程艺术》《C++Templates》,Meyes的《Effective C++》中讲过,《More Effective C++》再讲,无论是概念,用法,实现都是一本一本反复提起,这样反复提起,反复讲解的知识,自然是有其作用的,其作用也很简单,为了弥补C++对于内存管理的不足。众所周知,C++对于内存管理的方式称为手动管理,说白了就是C++本身不管理,都由程序员管理,事实上导致的问题一片一片,不记得多少次反复的在公司的服务器程序中去查内存泄露了,几乎随着每次大规模功能的更新,都会有新的内存泄露出现。。。。。C++相信程序员的原则告诉我们,程序员总是对的,那么即使是内存泄露了,也是有他的理由。。。。。。他的理由就是下一次可以进行内存泄露检查的工作,以浪费一天又一天的时间。随着BoundsChecker这样的工具出现,虽然简化了一部分明显的内存泄露检查,但是实际上复杂情况下的内存泄露还是只能靠自己完成,BoundsChecker误报的本事太强了,并且根本无法正常运行公司的地图服务器并退出,可能因为随后的内存泄露正常报告+误报超出了BoundsChecker的整数上限,总是会将BoundsChecker拉入无响应的状态。 事实上,我工作中也用的相对较少,毕竟C++标准库中的auto_ptr是个比较扭曲的东西,不仅使用语义奇怪,碰到稍微复杂的应用就根本胜任不了了,这是很无奈的事情。这一点可以参考我很久以前的文章《C++可怜的内存管理机制漫谈及奇怪补救auto_ptr介绍》,文中可以看到,在加入了类,异常机制,并且延续着C语言中手动管理内存方式的C++中,auto_ptr其实最多也就算种奇异的补丁机制。 但是我们可以求助于boost的智能指针库,那里丰富的资源改变了很多事情,但是我工作中是不允许使用boost库的。。。。又一次的无奈。 二、 智能指针 1. 什么是智能指针 要知道什么是智能指针,首先了解什么称为 “资源分配即初始化”这个翻译的异常扭曲的名词。RAII—Resource Acquisition Is Initialization,外国人也真有意思,用一个完整的句子来表示一个应该用名词表示的概念,我们有更有意思了,直接翻译过来,相当扭曲。。。。。 在《C++ Primer》这样解释的,“通过定义一个类来封装资源的分配和释放,可以保证正确释放资源” 而智能指针就是这种技术的实现,《More Effective C++》中这样描述的:“Smart pointers are objects that are designed to look,act,and feel like build-in pointers,but to offer greater functionality.They have a variety of applications, including resource management.” 《Effective C++》给出的关键特点是: 1. 资源分配后立即由资源管理对象接管。 2. 资源管理对象用析构函数来确保资源被释放。 基本上,这就是智能指针的核心概念了,至于智能指针实现上的特点,比如所有权转移,所有权独占,引用计数等,都是次要的东西了。 目前我见过关于各种智能指针分类,介绍,使用方法说明最详细的应该是《Beyond the C++ Standard Library: An Introduction to Boost》一书,此书第一章第一个专题库就是关于智能指针的,除了对标准库中已有的auto_ptr没有介绍(因为本书是讲Boost的嘛),对Boost库中的智能指针进行了较为详细的描述,推荐想了解的都去看看。 文中论及的智能指针包括 scoped_ptr,scoped_array:所有权限制实现 shared_ptr,shared_array:引用计数实现 intrusive_ptr:引用计数插入式实现 weak_ptr:无所有权实现 关于智能指针的实现及原理,本人看过最详细的介绍是在More Effective C++ Items 28,29 这里仅仅介绍最广泛使用的智能指针shared_ptr,加上以前写过的auto_ptr(《C++可怜的内存管理机制漫谈及奇怪补救auto_ptr介绍》)给出智能指针的一些用法示例,其他的智能指针因为实现上的区别导致使用上也有一些区别,但是核心概念是一样的,都是上面提及的两条关键特点。 2. shared_ptr介绍 shared_ptr是通过引用计数计数实现的智能指针,应用也最为广泛,也是早在TR1就已经确认会进入下一版C++标准的东西,现在我还会因为标准库中没有,boost库不准用而遗憾,过几年,总有一天,我们就能自由使用类似shared_ptr的指针了。 原型如下: namespace boost { template<typename T> class shared_ptr { public: template <class Y> explicit shared_ptr(Y* p); template <class Y,class D> shared_ptr(Y* p,D d); ~shared_ptr(); shared_ptr(const shared_ptr & r); template <class Y> explicit shared_ptr(const weak_ptr<Y>& r); template <class Y> explicit shared_ptr(std::auto_ptr<Y>& r); shared_ptr& operator=(const shared_ptr& r); void reset(); T& operator*() const; T* operator->() const; T* get() const; bool unique() const; long use_count() const; operator unspecified_bool_type() const; void swap(shared_ptr<T>& b); }; template <class T,class U> shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r); } 首先,为了查看资源分配方便,引入一个方便查看资源转移,拷贝情况的类: #include <string> #include <iostream> template<class T> class CResourceObserver { public: CResourceObserver() { cout <<typeid(T).name() <<" Construct." <<endl; } CResourceObserver(const CResourceObserver& orig) { cout <<typeid(T).name() <<" Copy Construct." <<endl; } operator=(const CResourceObserver& orig) { cout <<typeid(T).name() <<" operator = " <<endl; } virtual ~CResourceObserver(void) { cout <<typeid(T).name() <<" Deconstruct." <<endl; } }; 这个类,利用了运行时类型识别及模板,这样发生与资源有关的操作时,都能通过输出恰当的反映出来。 shared_ptr的最简单应用 这里看个最简单的shared_ptr使用的例子,顺面看看CResourceObserver的使用。在最简单的一次性使用上,shared_ptr几乎没有区别。 例一: #include <boost/smart_ptr.hpp> #include "ResourceObserver.h" using namespace std; using namespace boost; class MyClass : public CResourceObserver<MyClass> { }; void Fun() { shared_ptr<MyClass> sp(new MyClass); } int _tmain(int argc, _TCHAR* argv[]) { cout <<"Fun called." <<endl; Fun(); cout <<"Fun ended." <<endl; return 0; } 输出如下: Fun called. class MyClass Construct. class MyClass Deconstruct. Fun ended. 我们只new,并没有显式的delete,但是MyClass很显然也是析构了的。 这里将shared_ptr替换成auto_ptr也是完全可以的,效果也一样。 shared_ptr的与auto_ptr的区别 shared_ptr与auto_ptr的区别在于所有权的控制上。如下例: 例二: #include <boost/smart_ptr.hpp> #include <memory> #include "ResourceObserver.h" using namespace std; using namespace boost; class MyClass : public CResourceObserver<MyClass> { public: MyClass() : CResourceObserver<MyClass>() { mstr = typeid(MyClass).name(); } void print() { cout <<mstr <<" print" <<endl; } std::string mstr; }; typedef shared_ptr<MyClass> spclass_t; //typedef auto_ptr<MyClass> spclass_t; void Fun2(spclass_t& asp) { spclass_t sp3(asp); cout <<asp.use_count() <<endl; asp->print(); return; } void Fun() { spclass_t sp(new MyClass); cout <<sp.use_count() <<endl; spclass_t sp2(sp); cout <<sp.use_count() <<endl; Fun2(sp); cout <<sp.use_count() <<endl; } int _tmain(int argc, _TCHAR* argv[]) { cout <<"Fun called." <<endl; Fun(); cout <<"Fun ended." <<endl; return 0; } 输出: Fun called. class MyClass Construct. 1 2 3 class MyClass print 2 class MyClass Deconstruct. Fun ended. 此例中将shared_ptr分配的资源复制了3份(实际是管理权的复制,资源明显没有复制),每一个shared_ptr结束其生命周期时释放一份管理权。每一个都有同等的使用权限。输出的引用计数数量显式了这一切。在这里,可以尝试替换shared_ptr到auto_ptr,这个程序没有办法正确运行。 shared_ptr的引用计数共享所有权 因为没有拷贝构造及operator=的操作,我们可以知道,对象没有被复制,为了证实其使用的都是同一个资源,这里再用一个例子证明一下: 例3: #include <boost/smart_ptr.hpp> #include <memory> #include "ResourceObserver.h" using namespace std; using namespace boost; class MyClass : public CResourceObserver<MyClass> { public: MyClass() : CResourceObserver<MyClass>() { mstr = typeid(MyClass).name(); } void set(const char* asz) { mstr = asz; } void print() { cout <<mstr <<" print" <<endl; } std::string mstr; }; typedef shared_ptr<MyClass> spclass_t; void Fun2(spclass_t& asp) { spclass_t sp3(asp); sp3->set("New Name"); return; } void Fun() { spclass_t sp(new MyClass); spclass_t sp2(sp); Fun2(sp); sp->print(); sp2->print(); } int _tmain(int argc, _TCHAR* argv[]) { cout <<"Fun called." <<endl; Fun(); cout <<"Fun ended." <<endl; return 0; } 输出: Fun called. class MyClass Construct. New Name print New Name print class MyClass Deconstruct. Fun ended. shared_ptr与标准库容器 在标准库容器中存入普通指针来实现某个动态绑定的实现是很普遍的事情,但是实际上每次都得记住资源的释放,这也是BoundsChecker误报的最多的地方。 例4: #include <boost/smart_ptr.hpp> #include <vector> #include "ResourceObserver.h" using namespace std; using namespace boost; class MyClass : public CResourceObserver<MyClass> { public: MyClass() : CResourceObserver<MyClass>() { mstr = typeid(MyClass).name(); } void set(const char* asz) { mstr = asz; } void print() { cout <<mstr <<" print" <<endl; } std::string mstr; }; typedef shared_ptr<MyClass> spclass_t; typedef vector< shared_ptr<MyClass> > spclassVec_t; void Fun() { spclassVec_t spVec; spclass_t sp(new MyClass); spclass_t sp2(sp); cout <<sp.use_count() <<endl; cout <<sp2.use_count() <<endl; spVec.push_back(sp); spVec.push_back(sp2); cout <<sp.use_count() <<endl; cout <<sp2.use_count() <<endl; sp2->set("New Name"); sp->print(); sp2->print(); spVec.pop_back(); cout <<sp.use_count() <<endl; cout <<sp2.use_count() <<endl; sp->print(); sp2->print(); } int _tmain(int argc, _TCHAR* argv[]) { cout <<"Fun called." <<endl; Fun(); cout <<"Fun ended." <<endl; return 0; } 输出: Fun called. class MyClass Construct. 2 2 4 4 New Name print New Name print 3 3 New Name print New Name print class MyClass Deconstruct. Fun ended. 当在标准库容器中保存的是shared_ptr时,几乎就可以不考虑资源释放的问题了,该释放的时候自然就释放了,当一个资源从一个容器辗转传递几个地方的时候,常常会搞不清楚在哪个地方统一释放合适,用了shared_ptr后,这个问题就可以不管了,每次的容器Item的添加增加计数,容器Item的减少就减少计数,恰当的时候,就释放了。。。。方便不可言喻。 3. 智能指针的高级应用: 已经说的够多了,再说下去几乎就要脱离讲解智能指针与异常的本意了,一些很有用的应用就留待大家自己去查看资料吧。 1. 定制删除器,shared_ptr允许通过定制删除器的方式将其用于其它资源的管理,几乎只要是通过分配,释放形式分配的资源都可以纳入shared_ptr的管理范围,比如文件的打开关闭,目录的打开关闭等自然不在话下,甚至连临界区,互斥对象这样的复杂对象,一样可以纳入shared_ptr的管理。 2. 从this创建shared_ptr 。 以上两点内容在《Beyond the C++ Standard Library: An Introduction to Boost》智能指针的专题中讲解了一些,但是稍感不够详细,但是我也没有看到更为详细的资料,聊胜于无吧。 3. Pimpl: 《Beyond the C++ Standard Library: An Introduction to Boost》中将智能指针的时候有提及,在《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》 第43 条Pimpl judiciously中对其使用的好处,坏处,方式等都有较为详细的讲解,大家可以去参考一下,我就不在此继续献丑了。 但是,事实上,很多时候并不是只能使用智能指针(比如pimpl),只是,假如真的你合理的使用了智能指针,那么将会更加安全,更加简洁。 以下是《Beyond the C++ Standard Library: An Introduction to Boost》对shared_ptr使用的建议: 1.当有多个使用者使用同一个对象,而没有一个明显的拥有者时 2.当要把指针存入标准库容器时 3.当要传送对象到库或从库获取对象,而没有明确的所有权时 4.当管理一些需要特殊清除方式的资源时 三、 智能指针与C++异常 因为考虑到大家可能对智能指针不够熟悉,所以讲到这里的时候对智能指针进行了较多的讲解,几乎就脱离主题了,在这里开始进入正题。 一开始我就讲了智能指针对于C++的异常机制非常重要,到底重要在哪里呢?这里看两个C++异常的例子,一个使用了没有使用智能指针,一个使用了智能指针。 void Fun() { MyClass* lp1 = NULL; MyClass* lp2 = NULL; MyClass* lp3 = NULL; lp1 = new MyClass; try { lp2 = new MyClass; } catch(bad_alloc) { delete lp1; } try { lp3 = new MyClass; } catch(bad_alloc) { delete lp1; delete lp2; } // Do Something..... delete lp1; delete lp2; delete lp3; } void Fun2() { try { spclass_t sp1(new MyClass); spclass_t sp2(new MyClass); spclass_t sp3(new MyClass); } catch(bad_alloc) { // No need to delete anything } // Do Something // No need to delete anything } 区别,好处,明眼人不用看第二眼。这里为了简单,用内存的作为示例,虽然现在内存分配的情况很少见了,但是其他资源原理上是一样的,个人经验最深的地方实在Python的C API使用上,哪怕一个简单的C++,Python函数相互调用,都会有N个PyObject*出来,一个一个又一个,直到头昏脑胀,使用了智能指针后,简化的不止一半代码。 其实从本质上来讲,异常属于增加了程序从函数退出的路径,而C++原来的内存管理机制要求每个分支都需要手动的释放每个分配了的资源,这是本质的复杂度,在用于普通return返回的时候,还有一些hack技巧,见《do...while(0)的妙用》,但是异常发生的时候,能够依赖的就只有手动和智能指针两种选择了。 在没有智能指针的光使用异常的时候,甚至会抱怨因为异常增加了函数的出口,导致代码的膨胀,说智能指针是C++异常处理的绝佳搭档就在于其弥补的此缺点。 另外,其实很多语言还有个finally的异常语法,JAVA,Python都有,SEH也有,其与使用了智能指针的C++异常比较在刘未鹏关于异常处理的文章《错误处理(Error-Handling):为何、何时、如何(rev#2)》中也有详细描述,我就不在此多费口舌了,将来讲SEH的时候自然还会碰到。个人感觉是,有也不错。。。。毕竟,不是人人都有机会在每个地方都用上智能指针。 四、 参考资料 1.C++ Primer,中文版第4版,Stanley B.Lippman, Josee lajoie, Barbara E.Moo著 人民邮电出版社 2.Effective C++,Third Edition 英文版,Chapter 3 Resource Management, Scott Meyes著,电子工业出版社 3.More Effective C++(英文版),Scott Meyes著,Items 28,29,机械工业出版社 4.Beyond the C++ Standard Library: An Introduction to Boost,By Bj?rn Karlsson著,Part 1,Library 1,Addison Wesley Professional 5.C++ Coding Standards: 101 Rules, Guidelines, and Best Practices Herb Sutter, Andrei Alexandrescu著, Addison Wesley Professional (3) SEH(Structured Exception Handling) 一、 综述 SEH--Structured Exception Handling,是Windows操作系统使用的异常处理方式。 对于SEH,有点需要说明的是,SEH是属于操作系统的特性,不为特定语言设计,但是实际上,作为操作系统的特性,几乎就等同与面向C语言设计,这点很好理解,就像Win32 API,Linux下的系统调用,都是操作系统的特性吧,实际还是为C做的。但是,作为为C语言设计的东西,实际上可调用的方式又多了,汇编,C++对于调用C语言的接口都是比较方便的。 二、 基础篇 还是简单介绍一下SEH的使用,但是不准备太详细的介绍了,具体的详细介绍见参考中提及的书目。关于SEH的基本应用,《Windows核心编程》绝对是最佳读物(其实个人一直认为《Windows核心编程》是Windows编程领域必看的第二本书,第一本是《Programming Windows》。关于SEH更深入的一点的知识可能就要参考一些能用汇编讲解的书籍了,《Windows用户态程序高效排错》算是其中讲的不错的一本。 首先,SEH也有像C++异常一样的语法,及类try-catch语法,在SEH中为__try-except语法,抛出异常从throw改为RaiseException,在MSDN中的语法描述为: __try { // guarded code } __except ( expression ) { // exception handler code } 见一个实际使用的例子: 例1: #include <iostream> #include <windows.h> using namespace std; int main() { __try { RaiseException(0, 0, 0, NULL); } __except(EXCEPTION_EXECUTE_HANDLER) { cout <<"Exception Raised." <<endl; } cout <<"Continue running" <<endl; } 这可能是最简单的SEH的例子了,输出如下: Exception Raised. Continue running 这个例子和普通C++异常的try-catch类似,也很好理解。只不过catch换成了except。 因为C语言没有智能指针,那么就不能缺少finally的异常语法,与JAVA,Python等语言中的也类似,(这是C++中没有的)finally语法的含义就是无论如何(不管是正常还是异常),此句总是会执行,常用于资源释放。 例2: #include <iostream> #include <windows.h> using namespace std; int main() { __try { __try { RaiseException(0, 0, 0, NULL); } __finally { cout <<"finally here." <<endl; } } __except(1) { } __try { __try { int i; } __finally { cout <<"finally here." <<endl; } } __except(1) { } cout <<"Continue running" <<endl; getchar(); } 这个实例看起来过于奇怪,因为没有将各个try-finally放入独立的模块之中,但是说明了问题: 1. finally的语句总是会执行,无论是否异常finally here总是会输出。 2. finally仅仅是一条保证finally语句执行的块,并不是异常处理的handle语句(与except不同),所以,假如光是有finally语句块的话,实际效果就是异常会继续向上抛出。(异常处理过程也还是继续) 3. finally执行后还可以用except继续处理异常,但是SEH奇怪的语法在于finally与except无法同时使用,不然会报编译错误。 如下例: __try { RaiseException(0, 0, 0, NULL); } __except(1) { } __finally { cout <<"finally here." <<endl; } VS2005会报告 error C3274: __finally 没有匹配的try 这点其实很奇怪,难道因为SEH设计过于老了?-_-!因为在现在的语言中finally都是允许与except(或类似的块,比如catch)同时使用的。C#,JAVA,Python都是如此,甚至在MS为C++做的托管扩展中都是允许的。如下例:(来自MSDN中对finally keyword [C++]的描述) using namespace System; ref class MyException: public System::Exception{}; void ThrowMyException() { throw gcnew MyException; } int main() { try { ThrowMyException(); } catch ( MyException^ e ) { Console::WriteLine( "in catch" ); Console::WriteLine( e->GetType() ); } finally { Console::WriteLine( "in finally" ); } } 当你不习惯使用智能指针的时候常常会觉得这样会很好用。关于finally异常语法和智能指针的使用可以说是各有长短,这里提供刘未鹏的一种解释,(见参考5的RAII部分,文中比较的虽然是JAVA,C#,但是实际SEH也是类似JAVA的)大家参考参考。 SEH中还提供了一个比较特别的关键字,__leave,MSDN中解释如下 Allows for immediate termination of the __try block without causing abnormal termination and its performance penalty. 简而言之就是类似goto语句的抛出异常方式,所谓的没有性能损失是什么意思呢?看看下面的例子: #include <iostream> #include <windows.h> using namespace std; int main() { int i = 0; __try { __leave; i = 1; } __finally { cout <<"i: " <<i <<" finally here." <<endl; } cout <<"Continue running" <<endl; getchar(); } 输出: i: 0 finally here. Continue running 实际就是类似Goto语句,没有性能损失指什么?一般的异常抛出也是没有性能损失的。 MSDN解释如下: The __leave keyword The __leave keyword is valid within a try-finally statement block. The effect of __leave is to jump to the end of the try-finally block. The termination handler is immediately executed. Although a goto statement can be used to accomplish the same result, a goto statement causes stack unwinding. The __leave statement is more efficient because it does not involve stack unwinding. 意思就是没有stack unwinding,问题是。。。。。。如下例,实际会导致编译错误,所以实在不清楚到__leave到底干啥的,我实际中也从来没有用过此关键字。 #include <iostream> #include <windows.h> using namespace std; void fun() { __leave; } int main() { __try { fun(); } __finally { cout <<" finally here." <<endl; } cout <<"Continue running" <<endl; getchar(); } 三、 提高篇 1. SEH的优点 1) 一个很大的优点就是其对异常进程的完全控制,这一点是C++异常所没有的,因为其遵循的是所谓的终止设定。 这一点是通过except中的表达式来控制的(在前面的例子中我都是用1表示,实际也就是使用了EXCEPTION_EXECUTE_HANDLER方式。 EXCEPTION_CONTINUE_EXECUTION (–1) 表示在异常发生的地方继续执行,表示处理过后,程序可以继续执行下去。 C++中没有此语义。 EXCEPTION_CONTINUE_SEARCH (0) 异常没有处理,继续向上抛出。类似C++的throw; EXCEPTION_EXECUTE_HANDLER (1) 异常被处理,从异常处理这一层开始继续执行。 类似C++处理异常后不再抛出。 2) 操作系统特性,不仅仅意味着你可以在更多场合使用SEH(甚至在汇编语言中使用),实际对异常处理的功能也更加强大,甚至是程序的严重错误也能恢复(不仅仅是一般的异常),比如,除0错误,访问非法地址(包括空指针的使用)等。这里可以用一个例子来说明: #include <iostream> #include <windows.h> using namespace std; int main() { __try { int *p = NULL; *p = 0; } __except(1) { cout <<"catch that" <<endl; } cout <<"Continue running" <<endl; getchar(); } 输出: catch that Continue running 在C++中这样的情况会导致程序直接崩溃的,这一点好好利用,可以使得你的程序稳定性大增,以弥补C++中很多的不足。但是,问题又来了,假如异常都被这样处理了,甚至没有声息,非常不符合发生错误时死的壮烈的错误处理原则。。。。。。。很可能导致程序一堆错误,你甚至不知道为什么,这样不利于发现错误。 但是,SEH与MS提供的另外的特性MiniDump可以完美的配合在一起,使得错误得到控制,但是错误情况也能捕获到,稍微的缓解了这种难处(其实也说不上完美解决)。 这一点需要使用者自己权衡,看看到底开发进入了哪个阶段,哪个更加重要,假如是服务器程序,那么在正式跑着的时候,每崩溃一次就是实际的损失。。。所以在后期可以考虑用这种方式。 关于这方面的信息,在下一次在详细讲解。 2. SEH的缺点 其实还是有的,因为是为操作系统设计的,实际类似为C设计,那么,根本就不知道C++中类/对象的概念,所以,实际上不能识别并且正确的与C++类/对象共存,这一点使用C++的需要特别注意,比如下例的程序根本不能通过编译。 例一: int main() { CMyClass o; __try { } __except(1) { cout <<"catch that" <<endl; } cout <<"Continue running" <<endl; getchar(); } 例二: int main() { __try { CMyClass o; } __except(1) { cout <<"catch that" <<endl; } cout <<"Continue running" <<endl; getchar(); } 错误信息都为: warning C4509: 使用了非标准扩展:“main”使用SEH,并且“o”有析构函数 error C2712: 无法在要求对象展开的函数中使用__try 这点比较遗憾,但是我们还是有折衷的办法的,那就是利用函数的特性,这样可以避开SEH的不足。 比如,希望使用类的使用可以这样: 这个类利用了上节的CResourceObserver类, class CMyClass : public CResourceObserver<CMyClass> { }; void fun() { CMyClass o; } #include <iostream> #include <windows.h> using namespace std; int main() { __try { fun(); } __except(1) { cout <<"catch that" <<endl; } cout <<"Continue running" <<endl; getchar(); } 输出: class CMyClass Construct. class CMyClass Deconstruct. Continue running 可以看到正常的析构,简而言之就是将实际类/对象的使用全部放进函数中,利用函数对对象生命周期的控制,来避开SEH的不足。 四、 参考资料 1. Windows核心编程(Programming Applications for Microsoft Windows),第4版,Jeffrey Richter著,黄陇,李虎译,机械工业出版社 2.MSDN—Visual Studio 2005 附带版,Microsoft 3.加密与解密,段钢编著,电子工业出版社 4.Windows用户态程序高效排错,熊力著,电子工业出版社 5. 错误处理(Error-Handling):为何、何时、如何(rev#2),刘未鹏(pongba)著 |