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。

但是,如果在函数内部发生异常,则将撤销vector而不会释放数组。问题在于数组不是自动释放的。在new之后但在delete之前发生的异常使得数组没有被撤销。不管何时发生异常,都保证运行vector析构函数。

用类管理资源分配

对析构函数的运行导致一个重要的编程技术的出现:异常安全的---即使发生异常,程序也能正常工作。在这种情况下,“安全”来自于保证“如果发生异常,被分配的任何资源都适当地释放”!

通过定义一个类来封装资源的分配和释放,可以保证正确释放资源---“资源分配即初始化”(RAII)

具体:设计资源管理类,以便构造函数分配资源而析构函数释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域的时候释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象,作为展开定义对象的作用域的一部分。如:

class Resource
{
public:
    Resource(parms p):r(allocate(p)) {}
    ~Resource()
    {
        release(r);
    }

private:
    resource_type *r;
    resource_type *allocate(parms p);

    void release(resource_type *);
};

resource类是分配资源和回收资源的类型,它保存表示该资源的数据成员。Resource的构造函数分配资源,而析构函数释放它:

void fcn()
{
    Resource res(args); //申请资源
    //...

}//自动释放资源

如果函数正常终止,就在Resource对象超出作用域时释放资源;如果函数因异常而提早退出,编译器就运行Resource的析构函数作为异常处理过程的一部分。

【最佳实践】

可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源---使用资源管理类来分配和回收可以保证如果发生异常就释放资源。

//P591 习题17.7
//1
void exercise(int *b,int *e)
{
    vector<int> v(b,e);
    int *p = new int[v.size()];

    try
    {
        ifstream in("ints");
        //...
    }
    catch(...)
    {
        delete p;
        //...
    }
}
//2
template <typename resource_type>
class Resource
{
public:
    Resource(size_t sz):r(new resource_type[sz]) {}
    ~Resource()
    {
        release(r);
    }

private:
    resource_type *r;

    void release(resource_type *);
};

void exercise(int *b,int *e)
{
    vector<int> v(b,e);
    Resource<int> res(v.size());
    //...
    ifstream in("ints");
    //...
}

九、auto_ptr类

标准库auto_ptr类是上一节中介绍的异常安全的“资源分配即初始化”技术例子。auto_ptr类是接受一个类型形参的模板,它为动态分配的对象提供异常安全,auto_ptr类在头文件memory中定义。


auto_ptr类


auto_ptr<T>ap;


创建名为ap的未绑定的auto_ptr对象


auto_ptr<T>ap(p);


创建名为ap的auto_ptr对象,ap拥有指针p指向的对象。该构造函数为explicit。


auto_ptr<T>ap1(ap2);


创建名为ap1的auto_ptr对象,ap1保存原来存储在ap2中的指针。将所有权转给ap1,ap2成为未绑定的auto_ptr对象


ap1=
ap2


将所有权ap2转给ap1。删除ap1指向的对象并且使ap1指向ap2指向的对象,使ap2成为未绑定的


~ap


析构函数。删除ap指向的对象


*ap


返回对ap所绑定的对象的引用


ap->


返回ap保存的指针


ap.reset(p)


如果p与ap的值不同,则删除ap指向的对象并且将ap绑定到p


ap.release()


返回ap所保存的指针并且使ap成为未绑定的


ap.get()


返回ap保存的指针

【小心地雷】

auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组[会导致未定义的运行时行为]。

当auto_ptr被复制或复制时,有不寻常的行为,因此,不能将auto_ptr存储在标准库容器类型中。

每个auto_ptr对象绑定到一个对象或者指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。当auto_ptr对象超出作用域或者另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象。

class Text
{
public:
    Text()
    {
        cout << "Text" << endl;
    }
    ~Text()
    {
        cout << "~Text" << endl;
    }
};

int main()
{
    //对比!
    auto_ptr<Text> ap(new Text);
    Text *p = new Text;
}

1、为异常安全的内存分配使用auto_ptr

如果通过常规指针分配内存,而且在执行delete之前发生异常,就不会自动释放内存:

void f()
{
    int *ip = new int(42);
    //如果在此处抛出异常,并且不被局部捕获,则内存没法释放
    delete ip;
}

如果使用auto_ptr对象来代替,将会自动释放内存,即使提早退出这个块也是这样:

void f()
{
    auto_ptr<Text> ap(new Text);
    throw runtime_error("TEXT");
}

在这个例子中,编译器保证在展开超过了f之前运行ap的析构函数。

2、auto_ptr是可以保存在任何类型指针的模板

auto_ptr类是可以接受单个类型形参的模板:

    auto_ptr<string> strPtr(new string("Brontosaurus"));
    cout << *strPtr << endl;

3、将auto_ptr绑定到指针

在最常见的情况下,将auto_ptr对象初始化为由new表达式返回的对象的地址:

    auto_ptr<int> intPtr(new int(1024));

接受指针的构造函数为explicit构造函数,所以必须使用初始化的直接形式来创建auto_ptr对象:

    auto_ptr<int> iP = new int(1024);   //Error
    auto_ptr<int> pI(new int(1024));    //OK

iP所指的由 new表达式创建的对象在超出作用域时自动删除。如果iP是局部对象,iP所指对象在定义pi的块的末尾删除;如果发生异常,则iP也超出作用域,析构函数将自动运行iP的析构函数作为异常处理的一部分;如果iP是全局对象,就在程序末尾删除iP引用的对象。

4、使用auto_ptr对象

auto_ptr类定义了解引用操作符(*)和箭头操作符(->)的重载版本,因为auto_ptr定义了这些操作符,所以可以用类似于使用内置指针的方式使用auto_ptr对象:

    auto_ptr<string> strPtr(new string("HELLO!"));
    cout << *strPtr << endl;

    *strPtr = "TRex";
    string s = *strPtr;
    cout << s << endl;

    if (strPtr -> empty())
    {
        cout << "Empty!" << endl;
    }
    else
    {
        cout << "Not Empty!" << endl;
    }

auto_ptr的主要目的,在保证自动删除auto_ptr对象引用的对象的同时,支持普通指针式行为。正如我们所见,自动删除该对象这一事实导致在怎样复制和访问它们的地址值方面,auto_ptrs与普通指针明显不同。

时间: 2024-10-10 01:36:44

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

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 学习笔记_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 学习笔记_87_用于大型程序的工具 -错误处理

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

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 学习笔记_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子树.如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的. [小心地雷] 当一个类有多个基类的时候,通过对所有直接基类同时进行名字查