C++ Primer 学习笔记_90_用于大型程序的工具 -错误处理[续3]

用于大型程序的工具

--异常处理[续3]

九、auto_ptr类[接上]

5、auto_ptr对象的复制和赋值是破坏性操作

auto_ptr和内置指针对待复制和赋值有非常关键的区别。当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态。

    auto_ptr<string> strPtr1(new string("HELLO!"));
    auto_ptr<string> strPtr2(strPtr1);
    cout << *strPtr2 << endl;
    cout << *strPtr1 << endl;   //段错误

复制之后,strPtr1不再指向任何对象。

与其他复制或赋值操作不同,auto_ptr的复制和赋值改变右操作数,因此,赋值的左右操作数必须都是可修改的左值。

6、赋值删除左操作符指向的对象

除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象---假如两个对象不同。

    auto_ptr<string> strPtr1(new string("HELLO!"));
    auto_ptr<string> strPtr2;
    strPtr2 = strPtr1;
    cout << *strPtr2 << endl;
    cout << *strPtr1 << endl;   //段错误

将strPtr2赋值给strPtr1之后:

1)删除了strPtr1指向的对象。

2)将strPtr2置为指向strPtr1所指的对象。

3)strPtr1是未绑定的auto_ptr对象。

【小心地雷】

因为复制和赋值是破坏性操作,所以auto_ptrs不能将 auto_ptr对象存储在标准容器中。标准库的容器类要求在复制或赋值之后两个对象相等,auto_ptr不满足这一要求,如果将strPtr2赋给strPtr1,则在赋值之后strPtr1!=
strPtr2,复制也类似。

7、auto_ptr的默认构造函数

    auto_ptr<int> p_auto;   //对象是未绑定的,不指向任何对象

默认情况下,auto_ptr的内部指针值置为0。对未绑定的auto_ptr对象解引用,其效果与对未绑定的指针解引用相同——
程序出错并且没有定义会发生什么:

    *p_auto = 1024;
    cout << *p_auto << endl;    //Segmentation fault (core dumped)

8、测试auto_ptr对象

测试的效果是确定指针是否为0。但是,不能直接测试auto_ptr对象:

    if (p_auto) //Error:auto_ptr 类型没有定义到可用作条件的类型的转换
        *p_auto = 1024;

必须使用它的get成员:

    if (p_auto.get())	//返回包含在 auto_ptr 对象中的基础指针
    {
        *p_auto = 1024;
        cout << *p_auto << endl;
    }
    else
    {
        cout << "Unbound any object!" << endl;
    }

【小心地雷】

应该只使用get询问auto_ptr对象或者使用返回的指针值,不能用get作为创建其他auto_ptr对象的实参。

使用get成员初始化其他auto_ptr对象违反auto_ptr类设计原则:在任意时刻只有一个auto_ptrs对象保存给定指针,如果两个auto_ptrs对象保存相同的指针,该指针就会被delete两次。

9、reset操作

auto_ptr对象与内置指针的另一个区别是,不能直接将一个地址(或者其他指针)赋给auto_ptr对象:

    auto_ptr<int> p_auto = new int(1024);   //Error

相反,必须调用reset函数来改变指针:

    auto_ptr<int> p_auto;

    if (p_auto.get())
    {
        *p_auto = 1024;
    }
    else
    {
        p_auto.reset(new int(2048));
    }
    cout << *p_auto << endl;

要复位auto_ptr对象,可以将0传递给reset函数。

【小心地雷】

调用auto_ptr对象的 reset函数时,在将auto_ptr对象绑定到其他对象之前,会删除auto_ptr对象所指向的对象(如果存在)。但是,正如自身赋值是没有效果的一样,如果调用该 auto_ptr对象已经保存的同一指针的reset函数,也没有效果,不会删除对象。

【警告:auto_ptr的缺陷】

要正确的使用auto_ptr类,必须坚持该类型强加的下列限制:

1.不要使用auto_ptr对象保存指向静态分配对象的指针,否则,当 auto_ptr对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。

2.永远不要使用两个auto_ptrs对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者reset两个不同的auto_ptr对象。另一种导致这个错误的微妙方式可能是,使用一个auto_ptr对象的get函数的结果来初始化或者reset另一个auto_ptr对象。

3.不要使用auto_ptr对象保存指向动态分配数组的指针。当auto_ptr对象被删除的时候,它只释放一个对象——
它使用普通 delete操作符,而不用数组的delete[] 操作符。

4.不要将auto_ptr对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr类不满足这个要求。

十、异常说明

异常说明指定,如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从列出的异常中派生的类型。

1、定义异常说明:

异常说明跟在函数形参表之后。一个异常说明在关键字throw之后跟着一个(可能为空的)由圆括号括住的异常类型列表:

void recoup(int) throw (runtime_error);

说明:如果recoup抛出一个异常,该异常将是runtime_error或者是有runtime_error派生的类型的异常。

空说明列表指出函数不抛出任何异常:

void no_problem() throw ();

异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。

如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。

2、违反异常说明

只有在运行时才能检测是否违反函数异常说明。

如果函数抛出了没有在异常说明中列出的异常,就调用标准库函数unexpected。默认情况下,unexpected函数调用terminate函数,terminate函数一般会终止程序。

【小心地雷】

在编译的时候,编译器不能也不会试图验证异常说明。

即使对函数代码的偶然阅读指明,它可能抛出异常说明中没有的异常,编译器也不会给出提示:

//不会给出任何错误说明!
void f() throw ()
{
    throw exception();
}

相反,编译器会产生代码以便保证:如果抛出了一个违反异常说明的异常,就调用unexpected函数。

3、确定函数不抛出异常

【最佳实践】

异常说明有用的一种重要情况是:如果函数可以保证不会抛出任何异常。

确定函数将不抛出任何异常,对函数的用户和编译器都有所帮助:

1)知道函数不抛出异常会简化编写调用该函数的异常安全的代码的工作,我们可以知道在调用函数时不必担心异常,

2)如果编译器知道不会抛出异常,它就可以执行被可能抛出异常的代码所抑制的优化。

4、异常说明与成员函数

像非成员函数一样,成员函数声明的异常说明符跟在函数形参表之后。

class bad_alloc : public exception
{
public:
    bad_alloc() throw();
    bad_alloc(const bad_alloc &) throw();
    bad_alloc &operator=(const bad_alloc &) throw();
    virtual ~bad_alloc() throw();
    //异常说明符放在const限定符之后
    virtual const char *what() const throw();
};

5、异常说明与析构函数

isbn_mismatch类将析构函数定义为:

class isbn_mismatch : public std::logic_error
{
public:
    virtual ~isbn_mismatch() throw()
    {

    }
};

isbn_mismatch类从logic_error类继承而来,logic_error是一个标准异常类,该标准异常类的析构函数包含空throw(),它们承诺不抛出任何异常。当继承这两个类中的一个时,我们的析构函数也必须承诺不抛出任何异常。

class isbn_mismatch : public std::logic_error
{
public:
    virtual ~isbn_mismatch();	//Error
};

out_of_stock类没有成员,所以它的合成析构函数不做任何可能抛出异常的事情,因此,编译器可以知道合成析构函数将遵守不抛出异常的承诺。

isbn_mismatch类有两个string类成员,这意味着isbn_mismatch的合成析构函数调用 string析构函数。C++标准保证,string析构函数像任意其他标准库类析构函数一样,不抛出异常。但是,标准库的析构函数没有定义异常说明,在这种情况下,我们知道,但编译器不知道,string析构函数将不抛出异常。我们必须定义自己的析构函数来恢复析构函数不抛出异常的承诺。

6、异常说明与虚函数

基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明符不同。

但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。

这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常:

class Base
{
public:
    virtual double f1(double) throw();
    virtual int f2(int) throw (std::logic_error);
    virtual std::string f3() throw (std::logic_error,std::runtime_error);
};

class Derived : public Base
{
public:
    //派生类不能在异常说明符列表中增加异常
    double f1(double) throw (std::underflow_error); //Error
    int f2(int) throw (std::logic_error);   //OK
    std::string f3() throw ();  //OK
};

如果通过基类指针或引用进行函数调用,那么,这些类的用户所涉及的应该只是在基类中指定的异常。通过派生类抛出的异常限制为由基类所列出的那些,在编写代码时就可以知道必须处理哪些异常。代码可以依赖于这样一个事实:基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集。例如,当调用f3的时候,我们知道只需要处理logic_error或 runtime_error:

void compute(Base *pd) throw()
{
    try
    {
        pd -> f3();
    }
    catch (const logic_error &le)
    {
        //...
    }
    catch (const runtime_error &re)
    {
        //...
    }
}

在确定可能需要捕获什么异常的时候,compute函数使用基类中的异常说明。

十一、函数指针的异常说明

异常说明是函数类型的一部分。这样,也可以在函数指针的定义中提供异常说明:

void (*pf)(int) throw(runtime_error);	//该函数只能抛出runtime_error异常

如果不提供异常说明,该指针就可以指向能够抛出任意类型异常的具有匹配类型的函数。

在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。

void recoup(int) throw (runtime_error);

void (*pf1)(int) throw(runtime_error) = recoup;  //OK

void (*pf2)(int) throw(runtime_error,logic_error) = recoup; //OK

//recoup函数抛出的异常类型超出了pf3所指定的,因此会引发一个编译错误
//但是:有些编译器在此并不报错,比如g++!
void (*pf3)(int) throw() = recoup;  //Error

void (*pf4)(int) = recoup;  //OK
时间: 2024-10-10 01:36:42

C++ Primer 学习笔记_90_用于大型程序的工具 -错误处理[续3]的相关文章

C++ Primer 学习笔记_89_用于大型程序的工具 -错误处理[续2]

用于大型程序的工具 --异常处理[续2] 八.自动资源释放 考虑下面函数: void f() { vector<string> v; string s; while (cin >> s) { v.push_back(s); } string *p = new string[v.size()]; //... delete p; } 在正常情况下,数组和vector都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销vector. 但是,如果在函数内部发生异常,则将

C++ Primer 学习笔记_88_用于大型程序的工具 -错误处理[续1]

用于大型程序的工具 --异常处理[续1] 四.重新抛出 有可能单个catch不能完全处理一个异常.在进行了一些校正行动之后,catch可能确定该异常必须由函数调用链中更上层的函数来处理,catch可以重新抛出将异常传递给函数调用链中更上层的函数.重新抛出是后面不跟类型或表达式的一个throw: throw; 空throw语句将重新抛出异常对象,它只能出现在catch或从catch调用的函数中.如果在处理代码不活动时碰到空throw,就调用terminate函数. 虽然重新抛出不指定自己的异常,但

C++ Primer 学习笔记_90_用于大型程序的工具 --异常处理[续3]

用于大型程序的工具 --异常处理[续3] 九.auto_ptr类[接上] 5.auto_ptr对象的复制和赋值是破坏性操作 auto_ptr和内置指针对待复制和赋值有非常关键的区别.当复制auto_ptr对象或者将它的值赋给其他auto_ptr对象的时候,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定状态. auto_ptr<string> strPtr1(new string("HELLO!")); auto_ptr<

C++ Primer 学习笔记_87_用于大型程序的工具 -错误处理

用于大型程序的工具 --异常处理 引言: C++语言包含的一些特征在问题比较复杂,非个人所能管理时最为有用.如:异常处理.命名空间和多重继承. 相对于小的程序员团队所能开发的系统需求而言,大规模编程[往往涉及数千万行代码]对程序设计语言的要求更高.大规模应用程序往往具有下列特殊要求: 1.更严格的正常运转时间以及更健壮的错误检测和错误处理.错误处理经常必须跨越独立开发的多个子系统进行[异常处理]. 2.能够用各种库(可能包含独立开发的库)构造程序[命名空间]. 3.能够处理更复杂的应用概念[多重

C++ Primer 学习笔记_89_用于大型程序的工具 --异常处理[续2]

用于大型程序的工具 --异常处理[续2] 八.自动资源释放 考虑下面函数: void f() { vector<string> v; string s; while (cin >> s) { v.push_back(s); } string *p = new string[v.size()]; //... delete p; } 在正常情况下,数组和vector都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销vector. 但是,如果在函数内部发生异常,则将

C++ Primer 学习笔记_93_用于大型程序的工具 --命名空间[续2]

用于大型程序的工具 --命名空间[续2] 五.类.命名空间和作用域 名字的可见性穿过任意嵌套作用域,直到引入名字的块的末尾. 对命名空间内部使用的名字的查找遵循常规C++查找规则:当查找名字的时候,通过外围作用域外查找.对命名空间内部使用的名字而言,外围作用域可能是一个或多个嵌套的命名空间,最终以全包围的全局命名空间结束.只考虑已经在使用点之前声明的名字,而该使用仍在开放的块中: namespace A { int i; namespace B { int i; int j; int f1()

C++ Primer 学习笔记_94_用于大型程序的工具 --命名空间[续3]

用于大型程序的工具 --命名空间[续3] 六.重载与命名空间 正如我们所见,每个命名空间维持自己的作用域,因此,作为两个不同命名空间的成员的函数不能互相重载.但是,给定命名空间可以包含一组重载函数成员. 1.候选函数与命名空间 命名空间对函数匹配有两个影响.一个影响是明显的:using声明或using 指示可以将函数加到候选集合.另一个影响则微妙得多. 正如前节所见,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间.这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类

C++ Primer 学习笔记_95_用于大型程序的工具 --多重继承与虚继承

用于大型程序的工具 --多重继承与虚继承 引言: 大多数应用程序使用单个基类的公用继承,但是,在某些情况下,单继承是不够用的,因为可能无法为问题域建模,或者会对模型带来不必要的复杂性. 在这些情况下,多重继承可以更直接地为应用程序建模.多重继承是从多于一个直接基类派生类的能力,多重继承的派生类继承其所有父类的属性. 一.多重继承 1.定义多个类 为了支持多重继承,扩充派生列表: class Bear : public ZooAnimal { //... }; 以支持由逗号分隔的基类列表: cla

C++ Primer 学习笔记_96_用于大型程序的工具 --多重继承与虚继承[续1]

用于大型程序的工具 --多重继承与虚继承[续1] 四.多重继承下的类作用域 成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在本类中查找,然后依次查找每个基类.在多重继承下,查找同时检察所有的基类继承子树 -- 在我们的例子中,并行查找 Endangered子树和Bear/ZooAnimal子树.如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的. [小心地雷] 当一个类有多个基类的时候,通过对所有直接基类同时进行名字查