C++ 动态分配资源的自动释放 – auto_ptr的实现原理

本文深入探讨了C++中的资源管理技术,重点介绍了RAII(Resource Allocation Is Initialization)的概念及其实现方式,并通过示例代码详细解释了如何利用auto_ptr进行资源管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://patmusing.blog.163.com/blog/static/13583496020101824142699/

动态分配资源的自动释放的英文是 Resource Allocation In Initialization,通常缩写成RAII

根据《C++ Primer》第4版:

During stack unwinding, the function containing the throw, and possibly other functions in the call chain, are exited prematurely. In general, these functions will have created local objects that ordinarily would be destroyed when the function exited. When a function is exited due to an exception, the compiler guarantees that the local objects are properly destroyed. As each function exits, its local storage is freed. … If the local object is of class type, the destructor for this object is called automatically. As usual, the compiler does not work to destroy an object of built-in type.

假定有一个类定义如下:

// 资源类

class Student

{

public:

         Student(const string name = "Andrew", string gender = "M"int age = 6)

         {

                   this->name = name;

                   this->gender = gender;

                   this->age = age;

         }

 

         void printStudentInfo()

         {

                   cout << "Name: " << name << " ";

                   cout << "Gender: " << gender << " ";

                   cout << "Age: " << age << endl;

                   //throw runtime_error("Throw an exception deliberately...");                          // (1)

         }

 

         ~Student()

         {

                   // 下面这条语句用来提示资源是否被释放了

                   cout << "The destructor of class Student is called" << endl;

         }

 

private:

         string name;

         string gender;

         int age;

};

那么类似下面代码中的方式使用Student这个类,就会造成内存泄漏:

// 测试代码

int main()

{

         Student *stu = new Student("Andrew""M", 6);

         stu->printStudentInfo();

 

         return 0;

}

因为在return之前,没有deletestu这个动态分配而来的资源(如果仅仅是这个程序,也没有内存泄漏之忧,因为整个应用都结束运行了,在此只是为了方便说明类似的使用方式是不可以的,即用了new动态分配了资源,而没有对应的delete去回收)。为了防止这样的方式造成的内存泄漏,上面的测试代码应该增加一行delete stu

// 测试代码

int main()

{

         Student *stu = new Student("Andrew""M", 6);                               // (2)

         stu->printStudentInfo();

delete stu;                                                                                            // (3)

 

         return 0;

}

这样就不会造成内存泄漏了。运行该应用,输出的结果是:

Name: Andrew Gender: M Age: 6

The destructor of class Student is called

输出的The destructor of class Student is called表明了Student类的析构函数得到了调用。

 

现在,如果在(2)(3)中间发生了异常,即将Student类中的(1)语句uncomment掉,就会出现下面这样的错误提示:

This application has requested the Runtime to terminate it in an unusual way. Please contact the application’s support team for more information.

 

这样一来语句(3)delete stu;就不会得到执行,因此也会造成内存泄漏。为了消除上面的错误,我们在前面的测试代码中,增加try-ctach来捕获异常,代码如下:

// 测试代码

int main()

{

         Student *stu = new Student("Andrew""M", 6);                              // (2)

         try

         {

                   stu->printStudentInfo();                                                            

         }

         catch(const exception &e)

         {

                   cout << e.what() << endl;

         }

         delete stu;                                                                                                      // (3)

 

         return 0;

}

 

输出结果:

Name: Andrew Gender: M Age: 6

Throw an exception deliberately…

The destructor of class Student is called

 

这就说明,如果增加了try-catch,后面的delete stu;就会将用new动态分配的资源正确的予以释放。

 

进一步地,如果在stu->printStudentInfo();中出现了异常,而且后面也没有delete stu;这样的语句,用new分配给stu的资源进行自动释放?办法是有的。为此,我们需要增加一个类,定义如下:

// 资源管理类

template<typename T>

class Resource

{

public:

         Resource(T* p)

         {

                   // 将新分配的到的资源的地址赋给res,现在res指向了新分配到的资源

                   res = p;

         }

 

         ~Resource()

         {

                   if(res)

                   {

                            // 如果res指向的地址中,资源还没有被释放,那么则释放之

                            delete res;

                            res = 0;

                            cout << "Resources are deallocated here." << endl;

                   }

         }

private:

         T *res;

};

 

把测试代码改为:

// 测试代码

int main()

{

         Student *stu = new Student("Andrew""M", 6);                     // (2)

         // stu绑定到Resource<Student>类型的res变量,res是一个local对象,当程序超出其可见范围时

         // 其析构函数会自动执行。而它的析构函数中,又会自动释放stu所指向的资源

         Resource<Student> res(stu);

         try

         {

                   stu->printStudentInfo();                                                            

         }

         catch(const runtime_error &e)

         {

                   cout << e.what() << endl;

         }

 

         return 0;

}

 

上面代码的运行结果:

Name: Andrew Gender: M Age: 6

Throw an exception deliberately…

The destructor of class Student is called

Resources are de-allocated here.

 

这说明即使没有delete stu;程序也正确地析构了相关动态非配的资源,从而实现了动态分配资源的自动释放。

 

在上面的代码中,我们用stu->printStudentInfo();来调用相关的函数,如果想用res直接调用,则需要重载Resource类的->操作符,代码如下:

// 资源管理类

template<typename T>

class Resource

{

public:

         Resource(T* p)

         {

                   // 将新分配的到的资源的地址赋给res,现在res指向了新分配到的资源

                   res = p;

         }

 

         ~Resource()

         {

                   if(res)

                   {

                            // 如果res指向的地址中,资源还没有被释放,那么则释放之

                            delete res;

                            res = 0;

                            cout << "Resources are deallocated here." << endl;

                   }

         }

 

         T* operator->() const

         {

                   if(res)

                            return res;

                   else

                            cout << "The underlying object is empty." << endl;

         }

 

private:

         T *res;

};

粗体字部分重载了->操作符,用来返回该类中的私有成员变量res

 

相应的,测试代码做如下修改:

// 测试代码

int main()

{

         Student *stu = new Student("Andrew""M", 6);                     // (2)

         // stu绑定到Resource<Student>类型的res变量,res是一个local对象,当程序超出其可见范围时

         // 其析构函数会自动执行。而它的析构函数中,又会自动释放stu所指向的资源

         Resource<Student> res(stu);

         try

         {

                   //stu->printStudentInfo();            // 这行被注释掉

                   res->printStudentInfo();            // 改用res来调用printStudentInfo

         }

         catch(const runtime_error &e)

         {

                   cout << e.what() << endl;

         }

 

         return 0;

}

运行结果和前面是一致的,也实现了动态分配资源的自动释放。事实上,这就是大名鼎鼎的auto_ptr的实现原理。

 

注意,代码开始处需要包含以下语句:

#include <iostream>

#include <string>

#include <stdexcept>                 // 处理异常时,必须包含此头文件

using namespace std;


http://patmusing.blog.163.com/blog/static/13583496020101824541270/


a. auto_ptr定义于头文件memory中;

 

b. auto_ptr只能用来管理单个动态创建的对象,而不能管理动态创建的数组;

 

c. 和其他copyassign不同,auto_ptrcopyassign会改变右边的操作数,assignment符号的两边的auto_ptr均为左值;There is a crucially important difference between how auto_ptr and built-in pointers treat copy and assignment. When we

copy an auto_ptr or assign its value to another auto_ptr, ownership of the underlying object is transferred from the original

to the copy. The original auto_ptr is reset to an unbound state

 

d. auto_ptr不能作为容器中的元素;

  auto_ptrcopyassign具有析构行为,这就是auto_ptr不能作为容器元素的原因,因为标准库中的容器有对元素的要求:经

copy或者assign后的两个对象,必须相等;

 

e. 在判断一个auto_ptr是否被绑定的时候,不能直接使用auto_ptr对象:

      auto_ptr<Student> stu1(new Student);

         if(stu1)

         {

                   cout << "stu1 is bound" << endl;

         }

         else

         {

                   cout << "stu1 is unbound" << endl;

         }

         这样做将会导致compile error,应该改为:

         auto_ptr<Student> stu1(new Student);

         if(stu1.get())               // get()获取的是underlying对象的指针,如果被绑定则非零,如果没有被绑定则为0

         {

                   cout << "stu1 is bound" << endl;

         }

         else

         {

                   cout << "stu1 is unbound" << endl;

         }

 

f. auto_ptr的构造函数是explicit的,消除了隐式的类型转换(在这里即,从指针类型到auto_ptr类型的转换),因此不能直接将一个

指针赋给一个auto_ptr对象。如下面这样的代码:

         auto_ptr<Student> stu5 = new Student;

         stu5->printStudentInfo();

  在编译的时候不会有问题,但会出现严重的runtime error。正确的做法应该是:

         auto_ptr<Student> stu5(new Student);

         stu5->printStudentInfo();

 

g. 不同用两个auto_ptr绑定到同一个对象。

      // stu6stu7绑定到了同一个对象,这将会导致该对象被析构两次,将会产生runtime error

         auto_ptr<Student> stu6(new Student("Evanligine""F", 8));

         auto_ptr<Student> stu7(stu6.get());

         后面一句,应该改为:

         auto_ptr<Student> stu7(stu6);

         这样stu6就将ownership转交给了stu7stu6则成为了unboundauto_ptr            

 

h. 不能用auto_ptr指向静态资源分配对象。如下面的代码,尽管可以通过编译,但将会产生runtime error

         int ix = 10;

         auto_ptr<int> pint1(&ix);

 

i. auto_ptr的重要操作

auto_ptr<T> ap;               创建一个未绑定的auto_ptr对象ap

auto_ptr<T> ap(p);           创建一个auto_ptr对象ap,它绑定了指针p所指向的对象。该构造函数是explicit

auto_ptr<T> ap1(ap2);      创建一个auto_ptr对象ap1,它绑定到原来被ap2绑定的对象,ap2则成为未绑定的auto_ptr

ap1 = ap2;                       ap1删除原来绑定的对象,ap2ownership移交给ap1ap2成为未绑定的auto_ptr

*ap                                  返回ap绑定的对象的引用。可以通过*给被绑定的内在对象赋值。如下面代码:

                                                        auto_ptr<int> pint(new int(3));

                                                        cout << *pint << endl;                           // 输出3

                                                        *pint = 100;

                                                        cout << *pint << endl;                           // 输出100

ap->                                返回被ap绑定的对象的指针

ap.reset(p)                       如果指针pap绑定的内存对象的指针不相同,那么ap删除被其绑定的内存对象,改而绑定p

         指向的对象.

ap.release()                     返回ap所绑定对象的指针,并删除该被绑定的对象

ap.get()                           返回ap所绑定对象的指针


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值