九、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