C++智能指针使用总结
C++提供了4种智能指针用于对分配的内存进行自动释放,这些智能指针如下:
auto_ptr、unique_ptr、shared_ptr、weak_ptr。其中auto_ptr在C++98标准引入,后三种在C++11标准中加入。而auto_ptr已经被C++11所摒弃,建议使用后三种智能指针,这4种智能指针使用模板(template)实现。在此总结下个人对这4种智能指针肤浅认识。
C++智能指针简介
-
C++智能指针是行为类似于指针的类对象。它使用设计模式中的代理模式,代理了原始“裸”指针的行为,为指针添加了更多更有用的特性。
C++引入异常机制后,智能指针由一种技巧升级为一种非常重要的技术,因为如果没有智能指针,程序员必须保证new对象能在正确的时机delete,四处编写异常捕获代码释放资源,而智能指针则可以在退出作用域时——不管是正常离开或是因异常离开——总调用delete来析构在堆栈上动态分配的对象。
因为C++异常处理的真正功能在于它具有为异常抛掷前构造的所有局部对象(那么智能指针对象也适用)自动调用析构函数的能力(C++异常机制不仅仅在于它能够处理各种不同类型的异常)。所以在异常退出智能指针对象作用域时,总能由C++异常机制调用析构函数释放在堆栈上动态分配的对象。
当然,正常退出对象(智能指针对象也属于此列)作用域也会自动调用析构函数释放在堆栈上动态分配的对象。
由此可知,将“裸”指针包装成智能指针对象可以实现动态分配的内存对象的自动释放。
而且C++智能指针对象可以像原指针那样直接使用运算符,如赋值运算符'=',指针运算符'->',解引用运算符'*'。这点可以从下面的”shared_ptr智能指针--shared_ptr模板类摘要“部分可以印证。
END
auto_ptr智能指针(C++98)
-
所属头文件:#include <memory>
所属命名空间及标识符:using std::shared_ptr;
所属版本:C++98
g++启用版本命令:g++ -std=c++98 -c -o
补充:
如果启用c++11及以上标准,即g++ -std=c++11 -c -o,编译时会有一个警告信息提示
warning:‘auto_ptr’ is deprecated (‘auto_ptr‘被反对使用)
-
存在很多种智能指针,其中最有名的应该是C++98标准中的“自动指针”std::auto_ptr,它部分解决了获取资源自动释放的问题,例如:
#include <memory>
#include <iostream>
#include <string>
using std::cin;
using std::cout;
using std::string;
using std::auto_ptr;
class Report
{
private:
std::string str;
public:
Report( const string s ):str(s) { cout << "Object created!\n"; }
~Report() { cout << "Object deleted!\n"; }
void comment(string owner) const { cout << owner << str << "\n"; }
};
int main(void)
{
auto_ptr<Report> ps (new Report("Using auto_ptr."));
ps->comment(string("ps:"));
auto_ptr<Report> p1;
p1 = ps; //赋值完毕后ps已经失去对内存对象的所有权,不可再使用
p1->comment(string("p1:"));
//ps->comment(string("after p1=ps:")); //error,Segmentation faul
}
/*Result:
Object created!
Using auto_ptr.
Object deleted!
*/
auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但因为重载了 operator*后operator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时(离开作用域或异 常),C++会保证auto_ptr对象销毁,调用auto_ptr的析构函数,进而使用delete操作符删除原始指针释放资源。
-
auto_ptr很好用,被包含在C++标准库中令它在世界范围内被广泛使用,使用智能指针的思想、用法深入人心。但标注库没有覆盖智能指针的全部领域,尤其最重要的引用计数型智能指针。
END
shared_ptr智能指针(C++11)
-
所属头文件:#include <memory>
所属命名空间及标识符:using std::shared_ptr;
所属版本:C++11
g++启用版本命令:g++ -std=c++11 -c -o
-
shared_ptr是一个最像智能指针的“智能指针”,是源自boost库,后被收录到C++11标准的TR1库中。“抱歉,我实在想不出什么更恰当的词汇来形容它在软件开发中的重要性。再强调一遍,shared_ptr非常有价值、非常重要、非常有用。”(摘自《Boost程序库完全开发指南:深入C++“准”标准库》 罗剑锋 著 P69)。
在C++历史上曾经出现过无数的引用计数型智能指针实现,但没有一个比得上shared_ptr,在过去、现在和将来,它都是最好的。
-
shared_ptr模板类摘要:
template<class T>
class shared_ptr
{
public:
typedef T element_type;
//
shared_ptr();
template<class Y> explicit shared_ptr(Y *p);
template<class Y, class D> shared_ptr(Y *p, D d);
~shared_ptr();
//
shared_ptr( shared_ptr const & r);
template<calss Y> explicit shared_ptr(std::auto_ptr<Y> & r);
//
shared_ptr &operator=(shared_ptr const & r);
template<class Y> shared_ptr &operator=(shared_ptr<Y> const &r);
template<class Y> shared_ptr &operator=(std::auto_ptr<Y> & r);
//
void reset( );
template<class Y> void reset(Y * p);
template<class Y, class D> void reset( Y * p, D d);
//
T & operator*( )const;
T * operator->( ) const;
T * get( ) const;
//
bool unqiue( ) const;
long use_count( ) const;
//
operator unspecified-bool-type( ) const;
void swap(shared_ptr & b);
}
-
操作函数:
shared_ptr是用于管理new动态分配对象的智能指针,它重载了*和->操作符以模仿原始指针的行为,提供隐式bool类型转换以判断指针的有效性,get()函数可以得到指针原始指针,并且没有提供指针算术操作。例如:
#include <memory>
#include <assert>
//
using std::shared_ptr;
using std::assert;
//
shared_ptr<int> spi (new int); //一个int的shared_ptr
assert(spi); //在bool语境中隐式转换为bool值
*spi = 253; //使用解引用操作符*
shared_ptr<string> sps(new string("smart")); //一个string的shared_ptr
assert(sps->size( ) == 5); //使用->运算符
-
shared_ptr可以被安全的共享——shared_ptr是一个“全功能”的类,有着正常的拷贝、赋值语义,也可以进行shared_ptr间的比较,是“最智能”的智能指针。
shared_ptr有多种形式的构造函数,应用于各种可能的情形:
▲ 无参的shared_ptr( )创建一个持有空指针的shared_ptr;
▲ shared_ptr(Y *p)获得指向类型T的指针p的管理权,同时引用计数置为1。
这个构造函数要求Y类型必须能够转换为T类型;
▲ shared_ptr(shared_ptr const & r)从另外一个shared_ptr获得指针的管理
权,同时引用计数加1,结果是两个shared_ptr共享一个指针的管理权;
▲ shared_ptr(std::auto_ptr<Y> & r)从一个auto_ptr获得指针的管理权,引用
计数置为1,同时auto_ptr自动失去管理权;
▲ operator=赋值操作符可以从另外一个shared_ptr或auto_ptr获得指针的
管理权,其行为同构造函数;
▲ shared_ptr( Y *p, D d)行为类似shared_ptr(Y * p),但使用参数d指定了
析构时的定制删除器,而不是简单的delete。
-
shared_ptr的reset( )函数的作用是将引用计数减1,停止对指针的共享,除非引用计数为0,否则不会发生删除操作。
shared_ptr有两个专门的函数检查引用计数。unique( )在shared_ptr是指针的唯一拥所有者时返回true。use_count( )返回当前指针的引用计数。
要小心,use_count( )应该仅仅用于测试或者调试,它不提供高效率的操作,而且有的时候可能是不可用的(极少数情形)。而unique( )则是可靠的,任何时候都可用,而且比use_count( ) == 1 速度更快。
-
shared_ptr还支持比较运算符,可以测试两个shared_ptr的相等或者不等,比较基于内部保存的指针,相当于a.get( ) == b.get( )。
shared_ptr还可以使用operator<比较大小,同样基于内部保存的指针,但不提供除operator<以外的比较操作符,这使得shared_ptr可以被用于标准关联容器(set 和 map):
typedef shared_ptr<string> sp_t; //shared_ptr类型定义
map<sp_t, int> m; //标准映射容器
sp_t sp(new string("one")); //一个shared_ptr对象
m[sp] = 111; //关联数组用法
-
shared_ptr还支持流输出操作符operator<<,输出内部的指针值,方便调试。
-
用法举例
A.示范shared_ptr基本用法的例子如下:
shared_ptr<int> sp (new int(10));
assert( sp.unique( )); //现在shared_ptr是指针的唯一持有者
shared_ptr<int> sp2; //生成一个空(NULL)的智能指针
sp2 = sp; //sp2和sp指向同一个对象,拷贝构造函数
//两个shared_ptr相等,指向同一个对象,引用计数为2
assert( sp == sp2 && sp.use_count( ) == 2 );
*sp2 = 100;
assert( *sp == 100 );
sp.reset( );
assert( !sp );
//
//
B.第二个示范shared_ptr复杂的应用:
class shared
{
private:
shared_ptr<int> p;
public:
shared (shared_ptr<int> p_):p(p_) { }
void print( ) { cout <<"count:"<<p.use_count() <<"v="<<*P << endl;}
};
void print_func(shared_ptr<int> p)
{
//同样输出shared_ptr的引用计数和指向的值
cout << "count:" << p.use_count() << "v =" << *P << endl;
}
int main( )
{
shared_ptr<int > p( new int(100) );
shared s1(p), s2(p);
//
s1.print( );
s2.print( );
//
*p = 20;
print_func(p);
//
s1.print( );
}
// 运行结果 //
cout:3 v=100
cout:3 v=100
cout:4 v=20
cout:3 v=20
//
在声明了shared_ptr和两个shared_ptr类实例后,指针被它们所共享,因此引用计数为3。print_func( )函数内部拷贝了一个shared_ptr对象,因此引用计数再增加1,但当退出函数时拷贝自动析构,引用计数又恢复为3。
END
unique_ptr智能指针(C++11)
-
所属头文件:#include <memory>
所属命名空间及标识符:using std::shared_ptr;
所属版本:C++11
g++启用版本命令:g++ -std=c++11 -c -o
-
unique_ptr为何优于auto_ptr
请看下面的语句:
auto_ptr<string> p1 (new string("auto")); //#1
auto_ptr<string> p2; //#2
p2 = p1; //#3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。这是件好事,可以防止p1和p2的析构函数试图删除同一个对象;但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。
下面来看看使用unique_ptr的情况:
unique_ptr<string> p3 (new string("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3; //#6
编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全(编译阶段错误比潜在的程序崩溃更安全)。
-
但有时候,将一个智能指针赋值给另一个并不会留下危险的悬挂指针(就是空指针,极有可能被误用)。假设有如下函数定义:
#include <iostream>
#include <memory>
#include <string>
using std::string;
using std::cout;
using std::unique_ptr;
class Report
{
private:
string str;
public:
Report( const string s):str(s) { cout << "Object created!\n"; }
~Report() { cout << "Object deleted!\n"; }
void comment(const string owner) const {
cout << owner << str << "\n";
}
};
unique_ptr<Report> demo(const char *s)
{
unique_ptr<Report> temp(new Report(s));
return temp;
}
int main(void)
{
unique_ptr<Report> ps;
ps = demo("Uniquely special point");
ps->comment(string("un_ptr:"));
return 0;
}
//
demo( )返回一个临时的unique_prt,然后ps接管了原本归返回的unique_ptr所有的对象,而返回的unique_ptr被销毁。这没有问题,因为ps拥有了Report对象的所有权。这里还有另一个好处是,demo()返回的临时unique_ptr很快被销毁(因为由函数调用而返回的临时对象在堆中使用完后会被销毁),没有机会使用它来访问无效的数据。换句话说,没有理由禁止这种赋值。神奇的是,编译器(GUN GCC g++编译器支持这种特性)确实允许这种赋值!
-
总之,程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果源unique_ptr将存在一段时间,编译器将禁止这样做:
using std::unique_ptr;
using std::string;
unique_ptr<string> pu1(new string("Hi ho!"));
unique_ptr<string> pu2;
pu2 = pu1; //#not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("Yo!")); //#allowed
语句#1将留下悬挂的unique_ptr(pu1),这句可能导致危害。语句#2不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权转让给pu后就被销毁。这种情况而异的行为表明,unique_ptr优于允许两种赋值的auto_ptr。
这也是禁止(只是一种建议,编译器并不禁止)在容器对象中使用auto_ptr,但允许使用unique_ptr的原因。如果容器算法视图对包含unique_ptr的容器执行类似于语句#1的操作,将导致编译错误;如果算法视图执行类似于语句#2的操作,则不会有任何问题。而对auto_ptr,类似于语句#1的操作可能导致不确定的行为和神秘崩溃。
当然,您可能确实想执行类似于语句#1的操作。仅当以非智能的方式使用遗弃的智能指针(如解除引用时),这种赋值才不安全。要安全地重用这种指针,可以给它赋新值。C++有一个标准库函数std::move( ),让您能够将一个unique_ptr赋给另一个。下面是一个使用前述demo( )函数的例子,该函数返回一个unique_ptr<string>对象:
using std::unique_ptr;
unique_ptr<string> ps1,ps2;
ps1 = demo("Uniquely special");
ps2 = std::move(ps1); //enable assignment,启用分配
ps1 = demo(" and more");
cout << *ps2 << *ps1 << endl;
-
您可能会问,unique_ptr如何能够区分安全和不安全的用法呢?答案是它使用了C++11新增的移动构造函数和右值引用,这将在18章(C++ primer 6th)讨论。
相比于auto_ptr,unique_ptr还有另一个优点,它有一个け用于数组的变体。别忘了,必须将delete和new别对,将delete[]和new[]配对。模板auto_ptr使用delete而不是delete[],因此只能与new一起使用,而不能与new[]一起使用。但unique_ptr有使用new[]和delete[]的版本:
std::unique_ptr<double[ ]> pda (new double[5]); //将使用delete[ ]
-
std::default_delete
—— 配合unique_ptr使用
----------------------------------------------------------------------------
non-specialized template <class T> class default_delete;
array specialization template <class T> class default_delete<T[]>;
模板参数:T
类型T为待删除对象的类型。
------------------------------
默认的删除器(Default deleter)
函数对象类,其功能像调用(invokation)并删除一个类型为T*对象。 无参数的版本只是简单的使用delete执行内存释放操作。 同样(likewise), 以运行时确定数组长度的数组参数版本使用delete[]执行内存释放操作 此类用于保存unique_ptr实例中类型删除器而特意设计,或者用于定义一个删除器对象传递给unique_ptr 和 shared_ptr的构造函数。
由于这是一个轻量级(lightweight)的无状态类,使用它作为unique_ptr对象的删除器,相对于C++内建指针(绝大多数库都实现了)它既不增加额外开销也不增加空间。
-------------------------------- Example ----------------------------------
// unique_ptr constructor example
#include <iostream>
#include <memory>
int main () {
std::default_delete<int> d;
std::unique_ptr<int> u4 (new int, d);
std::unique_ptr<int> u5 (new int, std::default_delete<int>());
std::cout << "u4: " << (u4?"not null":"null") << '\n';
std::cout << "u5: " << (u5?"not null":"null") << '\n'
return 0;
}
END