`
qqsunkist
  • 浏览: 33094 次
  • 性别: Icon_minigender_1
  • 来自: 大连
社区版块
存档分类
最新评论

有效创建一个类(四)

    博客分类:
  • C++
阅读更多
在前三篇中我说明了有效创建一个类的前4个考虑步骤,现在就差最后一步了,考虑创建与类定义有关的异常类。

异常的概述

用户调用某个函数,函数可以在运行时检测到错误,但是不知道如何处理;用户呢,实际上知道在遇到这种错误时,该如何处理;为了解决这类问题,提出了异常的概念。异常的基本思想是:当函数检测到自己无法处理的错误时抛出一个异常,以便调用者(用户)能够处理这个异常。用户如果希望处理这种异常可以使用catch捕获这个异常。

传统的错误处理方式
(1)终止程序
(2)返回一个表示“错误”的值
(3)返回一个合法值,让程序处于某种非法状态
(4)调用一个预先准备好,在出现“错误”的情况下用的函数
(1)的方式,在未捕获异常的情况,默认发生的事情,换句话说,跟没有异常一样。但是我们可以做的更好不是吗?(2)调用者需要检查错误值,这容易使程序的体积加大,而且能够正常返回错误值是前提,但有时并不如人所愿。(3)事实上,即使能返回一个合法值,但是调用者往往很难注意到程序已经处在非法状态中了。(4)看上去与异常处理相似,但是是不是出现这种错误时,就一定要执行这个预先准备好的函数呢?

一个异常就是某个用于表示异常发生类的一个对象。检查到一个错误的代码段throw一个对象。一个catch语句表明它要处理某个异常。一个throw的作用就表示堆栈的一系列回退,直到找到适当的catch。

异常与资源管理
对于一个资源管理类来说,一旦在获取资源的过程中,或者使用资源的过程中,捕获到异常,需要释放掉自身占有的异常。当然可以使用try, catch语句,但是C++提出的“资源申请即初始化”的方式更优雅,更安全。

异常与new
一旦在使用new时,捕获到异常,那么处理异常的代码中必须调用对应的delete。

异常与资源耗尽
当堆内存由于申请的空间过大,已经耗尽资源时;C++默认调用_new_handler函数指针。
void customized_new_handler(){
  ...
};

set_new_handler(&customize_new_handler); //std func

void f()
{
   void (*oldnh)() = set_new_handler(&customized_new_handler);
   try {
     // ...
   }
   catch(bad_alloc) {
     // ...
   }
   catch( ... ){
      set_new_handler(oldnh); //reset handler
      throw; //re-throw
   }
   
   set_new_handler(oldnh); //reset handler
}

上述是资源耗尽的一般情况,极端情况可能连new一个异常对象的空间也没有了,那怎么办?不用担心,C++语言已经帮我们想好了,那就是每一个C++程序实现都要求保留足够的存储,在资源耗尽的情况,仍然可以抛出bad_alloc。

异常与构造函数
因为构造函数其特殊性,无法返回一个独立的值供调用程序检查。异常处理机制允许从构造函数内部传出来错误信息。通常构造函数多与资源申请有关,所以建议采用“资源申请即初始化”的方式处理异常。

异常与成员初始化
将成员初始式包含在try,catch块内。
class X {
  Vector v;
  //
public:
  X(int);
  //
};

X::X(int s)
try
      :v(s)
{ 
   // ...
}
catch(Vector::Size){
  // ...
}


异常与析构函数
析构函数的调用存在两种情况
I.  正常销毁对象,调用
II. 因异常,在捕获异常的处理块中调用;
对于后一种情况,绝不能让析构函数里抛出异常。如果真是这样,那就是异常处理机制的一次失败,并调用std::terminate()。那么抛出异常退出析构函数也违背了标准库的要求。
如果析构函数必须要调用一个可能抛出的异常的函数,可以通过try, catch块保护自己;当然保护方式要么吞掉所有可能抛出的异常,要么终止程序。如果要求析构函数调用的这个可以抛出异常的函数必须做出运行时异常反应的话,那么请考虑使用一个普通函数。(参考《Effective C++》条款08)

异常的描述
void f() throw( x2, x3); //may throw only x2, x3 exceptions
void f()                 //can throw any exception
void f() throw();        //no exception thrown 


异常的映射
unexpected()的行为与std::bad_exception映射;
与此set_new_handler()类似,对unexpected()的响应由_unexpected_handler决定,它又是通过<exception>中的std::set_unexpected()设置的。

用户自定义的异常映射
void g() throw(Yerr);

g()被在网络分布式环境下被调用,由于g()对网络异常一无所知,自然调用unexpected()。如果不希望g()调用unexpected(),那么需要g()处理所有网络情况可能抛出的那些网络异常,那么就需要重写g()。如果g()由于种种限制不能重写,我们只能通过重新定义unexpected()完成。

一、首先采用“资源申请即初始化”方式为unexpected()函数定义一个类
//摘自《The C++ Programming Language》第14.6.3节

typedef void(*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler);

class STC { //store and reset class
  unexpected_handler old;
public:
  STC(unexpected_handler f){ old = set_unexpected(f);}
  ~STC(){ set_unexpected(old);}
};


二、定义一个函数,使它具有我们希望的unexpected()的意义
class Yunexpected:public Yerr{};
void throwY() throw (Yunexpected) { throw Yunexpected(); }


三、提供一个网络版g函数
void networked_g() throw (Yerr)
{
   STC xx(&throwY);
   g();
}

但是上述对于用户而言,只是知道因为调用g()产生一个unexpected异常,具体是什么网络异常并不知道,那么怎么办呢?
这时修改下Yunexpected的类定义和throwY()的定义,使之可以保存真正的异常信息即可。
class Yunexpected:public Yerr{
public:
  Network_exception * pne;
  Yunexpected(Network_exception *p) : pne(p?p->clone():0){}
  ~Yunexpected(){delete pne;}
};

void throwY() throw(Yunexpected){
  try{
    throw; //re-throw
  }
  catch(Network_exception& p){
    throw Yunexpected(&p);
  }
  catch( ... ){
    throw Yunexpected(0);
  }
}


未捕获异常
缺省情况下,如果抛出一个异常未被捕获,那就会调用函数std::terminate()。
uncaught_exception由_uncaught_handler决定,uncaught_handler由std::set_terminate()设置。

标准异常体系
标准异常体系的样子如下图所示

其中exception在文件<exception>里给出
class exception{
public:
  exception() throw ();
  exception(const exception&) throw();
  exception& operator=(const exception&) throw();
  virtual ~exception() throw();
  virtual const char* what() const throw();
private:
  // ...
};

所有标准异常都由exception派生,然后不是所有的异常都由exception派生,所以通过捕捉exception想捕获所有异常是错误的想法。

如何定义一个完善的异常类,并且继承于std::exception体系呢?
#include <string>
#include <exception>

class MyBaseException : public std::exception
{
public:
    //Constructor without inner exception
    xxBaseException(const std::string& what = std::string("xxBaseException"))
        : xx_BaseException(0), xx_What(what) {}      

    //Constructor with inner exception
    xxBaseException(const xxBaseException& innerException, const std::string& what = std::string("xxBaseException"))
        : xx_BaseException(innerException.clone()), xx_What(what) {}
  
    template <class T>  // valid for all subclasses of std::exception
    xxBaseException(const T& innerException, const std::string& what = std::string("xxBaseException"))
        : xx_BaseException(new T(innerException)), xx_What(what) {}

    virtual ~xxBaseException() throw()
        { if(xx_BaseException) { delete xx_BaseException; } } 
          //don't forget to free the copy of the inner exception
    const std::exception* base_exception() { return xx_BaseException; }
    virtual const char* what() const throw()
        { return xx_What.c_str(); } 
    //add formated output for your inner exception here
private:
    const std::exception* xx_BaseException;
    const std::string xx_What;
    virtual const std::exception* clone() const
        { return new xxBaseException(); } 
    // do what ever is necesary to copy yourselve
};

上述的自定义xxBaseException还是比较简单的,还可以自己添加文件名,行号等信息,stackTrace深度等信息。

虽然在C++里提供了这样完备的异常处理机制,(似乎每种语言的异常处理机制大致相同,比如JAVA,C#),但是想象一下如果一个函数或者一个类中布满了这样的try, catch块,总归显得代码十分丑陋与笨拙。丑陋的同时也大大降低了开发人员对代码稳定性以及执行效率的自信。

那么怎么才能在避免写很多try,catch块的前提下,又能写出异常安全类或者方法呢?
C++的实现者,Bjarne Stroustrup,和《Effective C++》的作者,Scott Meyers给了我们关于如何写异常安全类的建议:

实现”异常安全“类
说到异常安全类,那么其定义是必须要说的。(定义引自"Exception Safty:Concepts and Techniques", Bjarne Stroustrup, Advances in Exception Handling Techniques, Lecture Notes in Computer Science 2022. Springer-Verlag, 2001, 60--76, Springer-Verlag)

引用

An operation on an object is said to be exeception safe if that operation leaves the object in a valid state when the operation is terminated by throwing an exception.


这段定义中需要注意这么几个关键字:
1.valid state:合法状态,合法状态可以是一个需要清除工作的错误状态,但这个错误状态一定是被良好定义的。这里良好定义意味着对象拥有合理的错误处理代码。
2.object:为了合理地定义合法状态,对象应该拥有一个invariant,一旦它的构造函数们确立了这个invariant,接下来针对这个对象的所有操作都会维持这个invariant,直到析构函数完成最后的清除工作。这段话里涉及到了一个invariant关键字,在维基百科里关于invariant的定义给出了两类,一类称为class invariant, 另一类是object invariant。这两个概念对于我们更好地理解如何“合理”定义“合法”状态有十分大的帮助。
3.throwing an exception: 操作是因为有异常抛出而终止的,object state也是针对这种情境下而言的。

class invariant 和 object invariant的简单介绍

这里我给出原文的原因是至今我没有找到非常贴切的术语来描述class invariant,虽然我可能理解了这个定义。

class invariant: A class invariant is an invariant used to constrain objects of a class. Methods of the class should preserve the invariant. The class invariant constrains the state stored in the object.
(类不变量:类的约束条件,约束了存储在对象内的状态。)
Class invariants are established during construction and constantly maintained between calls to public methods.
(狭隘理解:这个类约束条件主要针对类管理的资源,无论是内存还是文件句柄或者数据库连接等等,只要资源在构造函数中被确立,那么后续的任何类对象操作,这个约束条件始终有效且保持不变。例如一个vector,类约束条件是在堆中申请下来)
object invariant: is a programming construct consisting of a set of invariant properties that remain uncompromised regardless of the state of the object. This ensures that the object will always meet predefined conditions, and that methods may, therefore, always reference the object without the risk of making inaccurate presumptions.
(对象不变量:是由一组无论对象状态如何都不妥协地保持不变的属性组成的编程概念。这确保了对象一直满足预定义的条件)

基于此,合法状态的定义对于异常安全类显得至关重要了;所以“合理状态”给出了三个保证
1. 基本保证:类不变量基本被保持,至少类在构造期确定下来的资源(内存,数据库连接,SOCKET,文件句柄等)不会有泄漏。
2. 强烈保证:类似于数据库的事务概念。针对类的关键操作,在基本保证的前提下要么操作完全成功,要么完全失败,无中间状态可言。
3. 不抛掷保证:在基本保证的前提下,对一些操作保证不抛掷异常。

那么实现异常安全类,基于不同的保证有一些编程技巧如下:
(1)try块
(2)资源申请即初始化
其中资源申请即初始化这个点子的关键之处就是使资源的拥有者赋予一个局域化的对象。那么,在大多数情况,通过这个局域化对象的构造函数构建这个资源,当这个局域化对象被销毁时,其管理的资源自然地通过析构函数销毁掉,无论析构函数的调用时正常调用还是因为抛出异常调用。这样就保证了资源不会泄漏。

//摘自《Effective C++》条款29
class PrettyMenu {
public:
  ...
  void changeBackground(std::istream& imgSrc);
  ...
private:
  Mutex mutex;
  Image* bgImage;
  int imageChanges;
};

void PrettyMenu::changeBackground(std:;istream& imgSrc){
  lock(&mutex);
  delete bgImage;
  ++imageChanges;
  bgImage = new Image(imgSrc);
  unlock(&mutex);
}


上述的代码显然有很多问题:
【1】资源泄漏:如果new Image(imgSrc)因为imgSrc给的源不正确而抛出异常,那么unlock语句不会被执行,于是锁永远不会被释放,造成资源泄漏;
【2】数据损坏:如果new Image(imgSrc)失败,抛出异常,那么bgImage就会指向一个已经被delete掉的对象,imageChanges也会被累加,而我们知道这违背事实。

所以上述代码可以这样改变下以解决上面两个问题

class PrettyMenu {
  ...
  std::tr1::shared_ptr<Image> bgImage;
  ...
};

void PrettyMenu::changeBackground(std::istream& imgSrc){
  Lock ml(&mutex);
  bgImage.reset(new Image(imgSrc));
  ++imageChanges;
}

上述代码中,mutex资源的拥有者赋予给了一个局域化Lock对象ml。这样通过Lock类的构造函数确立了资源的获取,即获得锁;如果函数成功执行完后,函数内局域变量ml被自动销毁。销毁时,通过Lock类的析构函数自动释放锁;即使抛出异常时,由于销毁函数,所以锁同样也会被释放。

std::tr1::shared_ptr<T>是一个智能指针,通过智能指针的reset函数,不再需要手动删除旧bgImage对象,因为智能指针内部已经处理掉了,并且处理就图像的动作永远依据于new Image(imgSrc)语句的结果。

上述的代码已经缩短了函数changeBackground的长度,看起来更精练些。似乎也提供了强烈保证;但是美中不足的是imgSrc这个参数。如果Image构造函数抛出异常,istream的读取记号已被移走,而这样就跟该函数执行前有不同的地方了。所以上述changBackground实际只给出了基本保证。那么怎么改,才能给出强烈保证呢?考虑可以采用有效创建一个类(三)中的copy and swap方式。这种方式的基本思想就是为打算修改的对象创建一个副本,然后在副本上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改动都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。

实现上通常是将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓实现对象(即副本,因为副本完成修改动作)。这种手法通常称pointer to implementation, pimpl idiom。 所以就有了如下的可以提供强烈保证的修正代码
struct PMImpl {
  std::tr1::shared_ptr<Image> bgImage;
  int imageChanges;
};

class PrettyMenu {
  ...
private: 
  Mutex mutex;
  std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& imgSrc){
  using std::swap;
  Lock ml(&mutex);
  std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //create copy 
  pNew->bgImage.reset(new Image(imgSrc));   //modify copy
  ++pNew->imageChanges;

  swap(pImpl, pNew); //swap 
}

  • 大小: 33.9 KB
0
0
分享到:
评论

相关推荐

    C++创建window服务

    文章强调了一个关键概念——使用一个特定的C++类来简化服务程序的开发过程。这种方法的核心优势在于,开发者只需要重写几个基本类中的虚拟函数就能实现自己的服务程序。此外,文中提供了三个示例源代码: - **NT...

    Eclipse中设置在创建新类时自动生成注释

    在模板编辑器中,可以看到一个名为`"${filecomment}"`的占位符,这是我们自定义文件级注释的关键位置。将光标置于该行后,可以插入自定义的注释模板。例如,参考提供的部分代码: ``` /** * @author E-mail: * @...

    声明一个类Point,然后利用它声明一个组合类Line,实现由用户输入两点确定直线的长度和斜率,编写程序实现测试

    在本篇内容中,我们将深入探讨如何在C++编程语言中声明并利用类来实现对几何对象的操作,具体是通过创建一个表示点的类`Point`和一个表示线的类`Line`,来计算由两个点确定的直线的长度和斜率。这个过程不仅涉及到...

    win32窗口程序的创建

    创建一个Windows窗口需要经过四个基本步骤: 1. **设计窗口类**:定义 `WNDCLASS` 结构体,设置窗口类的属性和窗口过程函数。 2. **注册窗口类**:通过调用 `RegisterClass()` 函数注册窗口类,使系统能够识别并...

    jsp编程技巧 创建表示层之类

    例如,创建一个Servlet或Filter来处理请求,然后通过RequestDispatcher将控制权传递给JSP页面,以展示结果。 五、使用EL表达式和JSTL Expression Language (EL)和JavaServer Pages Standard Tag Library (JSTL)是...

    2-3 创建自定义异常类 - EMOS小程序1

    在【标题】"2-3 创建自定义异常类 - EMOS小程序1"中,描述了如何为EMOS小程序创建一个自定义异常类`EmosException`。 首先,创建了一个名为`com.example.emos.wx.exception`的包,这是为了遵循良好的编程实践,将...

    如何使用CDC的四个派生类

    为了能够在这些设备上进行有效的图形绘制,我们需要通过创建DC来指定一个逻辑意义上的“画布”。DC不仅包含了物理设备的各种状态信息,还提供了对这些设备进行绘图操作的基础。 当使用Win32 API进行编程时,我们...

    用类的方法求四位数各位之和

    在`main()`方法中,我们创建了`Third`类的一个实例`sumss`,并调用了`setsum()`方法将四位数`1567`传递给`s`变量。接着,通过调用`sums()`方法计算四位数各位数字的和,并将结果存储在`sumt`变量中,最后使用`System...

    Scott Mitchell 的ASP_NET 2_0数据教程之二:创建一个业务逻辑层

    为了保持组织结构,创建两个子文件夹,一个用于DAL,一个用于BLL,将类型化数据集移动到DAL文件夹,并在BLL文件夹中创建四个类文件。 **方法实现** 每个BLL类将包含与TableAdapter对应的方法,但在此基础上增加...

    SQL实验:创建表并实施完整性,使用规则和缺省,更新表中数据.doc ) 您可以上传小于60MB的文件

    在SQL中,创建一个基本表的基本语法如下: ```sql CREATE TABLE 表名 ( 列名1 数据类型 约束, 列名2 数据类型 约束, ... ); ``` 其中,“表名”是您要创建的表的名称;“列名”表示表中的每一列的名称;“数据...

    Gif编码解码类,总共四个

    最后,`Encoder`类可能是一个通用的编码器基类,提供了编码图像的基本框架和接口,而具体的编码实现可能由`GifEncoder`继承或引用。它可能包括设置编码参数、打开输出流、写入文件头和数据等功能。 总的来说,这些...

    递归的方式创建二叉树

    1. 定义二叉树节点类:首先,我们需要定义一个表示二叉树节点的类,包含数据字段和指向左右子节点的引用。 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self....

    Lucene3.0创建索引

    在Lucene3.0中创建索引是一个关键功能,可以帮助用户快速地检索和管理大量的文本数据。本篇文章将详细介绍如何使用Lucene3.0来创建索引,并通过一个具体的例子来演示整个过程。 #### 一、Lucene3.0简介 Lucene是一...

    Accp6.0 使用Java实现面向对象编程 第二章

    继承是面向对象编程中的一个核心概念,它允许创建一个新的类(子类),这个新类继承了现有类(父类)的所有属性和方法。通过继承机制,可以有效地减少代码重复,提高代码的可读性和可维护性。此外,继承还使得类的...

    C#创建多线程应用程序

    - 使用Thread类的构造函数,传入一个委托(代表线程要执行的方法)。 ```csharp Thread thread = new Thread(new ThreadStart(MyMethod)); ``` - 使用Lambda表达式简化代码。 ```csharp Thread thread = new ...

    有关c ++的题目,关于构造函数和拷贝构造函数的知识点

    因此,在创建`guyuan`类的对象时,编译器会自动生成一个默认的构造函数,该构造函数将所有成员变量初始化为它们类型的默认值。同时,编译器还会提供一个默认的拷贝构造函数,用于复制`guyuan`类的对象。 ##### 2.2 ...

    java和kotlin的内部类静态嵌套类

    例如,你可以在一个Activity或Fragment中定义一个内部类来处理特定的点击事件,或者创建一个静态嵌套类作为Adapter的ViewHolder,以便更好地管理视图复用。 总的来说,理解Java和Kotlin的内部类和静态嵌套类是成为...

    在C#中创建数据库

    在C#中创建数据库主要涉及的是ADO.NET框架的使用,这是一个强大的数据访问技术,允许开发者在应用程序中连接、操作和管理数据库。以下是一系列详细的知识点: 1. **数据库基础知识**:首先,我们需要理解数据库的...

    sap HR职位创建

    通过以上步骤,您可以在SAP HR系统中成功创建一个新的职位,并为其添加职位空缺和详细的职位描述。这些步骤不仅适用于SAP HR系统的初学者,也适用于希望优化其组织结构和人员配置的专业人士。通过细致的操作指南,...

    使用Java创建任务管理应用程序 - 一个实战教程

    任务管理应用程序的核心目标是提供一个用户友好的界面,帮助用户有效地组织和跟踪任务。在这个Java项目中,我们将创建一个具备以下功能的应用程序: - 添加新任务:用户可以输入任务的标题、描述、截止日期和优先级...

Global site tag (gtag.js) - Google Analytics