目录
C++中的智能指针(auto_ptr
) 可以实现堆内存对象的自动释放,相比直接用 new/delete
操作符来分配与操作内存,它在有些场景下更为安全。
这里对 auto_ptr
的应用场景、实现原理、使用注意事项、使用方式给出一个相对完整的介绍,更多内容可参考后面的参考资料。
1 使用 delete
的问题
动态内存使用最多的是在C++应用程序的代码中。有过编程经验的程序员虽然都知道 new
操作符的使用一定要与 delete
匹配,在某些场合仍然可能有内存溢出。当异常被掷出时,程序的正常控制流程被改变,因此导致潜在的内存溢出。
例如:
void g() //可能掷出 { if (some_condition == false) throw X(); } void func() { string * pstr = new string; g(); //如果 g 掷出一个异常,内存溢出 delete pstr; //如果 g 掷出一个异常,则此行为不能达到的代码行。 } int main() { try { func(); } catch(...) {} }
在上面的代码中,当 g
掷出一个异常,异常处理机制展开堆栈: g()
退出,同时控制被转移到 main()
的 catch(...)
代码块。这时,无论怎样, func()
中的 delete
语句都不会被执行,由此导致 pstr
的内存溢出。
如果使用局部自动串变量,而不是使用动态分配-内存溢出就不会出现了:
string str; //局部自动对象 g(); //没有内存溢出
但是,许多数据重要的结构以及应用,象链表,STL容器,串,数据库系统以及交互式应用都必须使用动态内存分配,因此仍然需要冒着万一发生异常导致内存溢出的风险去使用堆来进行动态内存的分配。C++标准化委员会意识到了这个漏洞并在标准库中添加了一个特殊的类模板,它就是 std::auto_ptr
,其目的是促使动态内存和异常之前进行平滑的交互。 Auto_ptr
保证当异常掷出时分配的对象(即: new
操作符分配的对象)能被自动销毁,内存能被自动释放。下面我们就来讨论使用动态内存时,如何正确和有效地使用 auto_ptr
来避免资源溢出。这个技术适用于文件,线程,锁定以及与此类似的资源。
1.1 auto_ptr
的使用
auto_ptr
的定义可以在 <memory.h>
中找到。与标准库中其它的成员一样,它被声明在命名空间 std::
中。当你实例化 auto_ptr
对象时,对它进行初始化的方法是用一个指针指向动态分配的对象,下面是实例化和初始化 auto_ptr
对象的例子:
include <memory> #include <string> using namespace std; void func() { auto_ptr<string> pstr (new string); /* 创建并初始化auto_ptr */ }
auto_ptr
后面的尖括弧里指定 auto_ptr
指针的类型,在这个例子中是 string
。然后 auto_ptr
句柄的名字,在这个例子中是 pstr
。最后是用动态分配的对象指针初始化这个实例。
注意你只能使用 auto_ptr
构造器的拷贝,也就是说,下面的代码是非法的:
auto_ptr pstr = new string; //编译出错
Auto_ptr
是一个模板,因此它是完全通用的。它可以指向任何类型的对象,包括基本的数据类型:
auto_ptr pi (new int);
一旦你实例化一个 auto_ptr
,并用动态分配的对象地址对它进行了初始化,就可以将它当作普通的对象指针使用,例如:
*pstr = "hello world"; //赋值 pstr->size(); //调用成员函数
之所以能这样做是因为 auto_ptr
重载了操作符 &
, *
和 ->
。不要被语法误导,记住 pstr
是一个对象,不是一个指针。
2 auto_ptr
的实现原理
使用 auto_ptr
与 delete
都是删除 new
的存储区,那两者使用上有什么不同呢?
2.1 首先,我们看 auto_ptr
是如何解决前面内存溢出问题的
auto_ptr
的析构函数自动摧毁它绑定的动态分配对象。换句话说,当 pstr
的析构函数执行时,它删除构造 pstr
期间创建的串指针。你绝不能删除 auto_ptr
,因为它是一个本地对象,它的析构函数是被自动调用的。让我们看一下函数 func()
的修订版本,这次使用了 auto_ptr:
void func() { auto_ptr pstr (new string); g(); //如果g()掷出异常,pstr 被自动摧毁 }
C++保证在堆栈展开过程中,自动存储类型的对象被自动摧毁。因此,如果 g()
掷出异常, pstr
的析构函数将会在控制被转移到 catch(...)
块之前执行。因为 pstr
的析构函数删除其绑定的串指针,所以不会有内存溢出发生。这样我们在使用动态分配对象时,利用 auto_ptr
就实现了自动和安全的本地对象。
2.2 然后,我们看一下 auto_ptr
的具体实现
auto_ptr
类:
template class auto_ptr { public: typedef T element_type; explicit auto_ptr(T *p = 0) throw(); auto_ptr(const auto_ptr& rhs) throw(); auto_ptr& operator=(auto_ptr& rhs) throw(); ~auto_ptr(); T& operator*() const throw(); T *operator->() const throw(); T *get() const throw(); T *release() const throw(); };
可见, auto_ptr
创建的是一个类对象,当这个对象消失时,会自动调用析构函数 ~auto_ptr();
再来看看 ~auto_ptr();
函数的实现:
~auto_ptr() { if (_Owns) delete _Ptr; }
从这里可看到,它调用了 delete
,而调用该函数的时机是对象消失,所以对象消失会调用 delete
。
综上可知,两者的不同就是:
auto_ptr
创建的是一个对象,而new
与delete
只是操作符(函数)!- 使用
auto_ptr
会在对象析够的时候自动调用delete
, 从而避免因忘记调用delete
导致内存溢出。
3 使用 auto_ptr
的注意事项
auto_ptr
并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免这些缺陷。
3.1 不能用于标准库容器对象
不要将 auto_ptr
对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果。
3.2 不能用于数组
auto_ptr
的另一个缺陷是将数组作为 auto_ptr
的参数:
auto_ptr pstr (new char[12] ); //数组;为定义
因为,不管什么时候使用数组的 new
操作时,都必须要用 delete[]
来摧毁数组。而 auto_ptr
的析构函数只对非数组类型起作用,所以数组将不能被正确析构的话,最终程序的行为是不明确的。
总之, auto_ptr
控制一个由 new
分配的单对象指针,仅此而已。
4 完整举例
这里给出一个实际的完整例子:
/*功能:简单介绍c++中auto_ptr智能指针的使用。 *使用auto_ptr(智能指针)可以自动管理内存,防止手工管理时候内存泄露的问题。它实际就是一个类型。 *auto_ptr是一个模板类,这个类型的对象内部的析构函数完成对堆内存的释放,所以不要对这个对象的内存进行delete了。 *auto_ptr对象通过一个new出来的内存,以及指定好一个类型来初始化,而使用方法和指针一样。 *auto_ptr对象不要用于stl以及数组,它只适合管理堆内存中的单个元素。 */ #include <iostream> #include <memory> using std::cout; using std::endl; using std::auto_ptr; void my_func(void); int main(int argc, char *argv[]) { my_func(); return 0; } void my_func(void) { //demo for manager heap memory by auto_ptr. auto_ptr<int> pInt(new int(0)); *pInt = 2; cout<<*pInt<<endl; //demo for manager heap memory by hand. int *pInt2 = new int(2); cout<<*pInt2<<endl; delete pInt2; }
这里例子中,在 my_func()
中分别用智能指针(auto_ptr
) 与 delete
方式管理 new
出来的对象,前者看起来更为方便(但是也不对称了)。