程序中需要管理各种各样的资源,为了避免内存泄露,必须确保在资源再也不需要时释放资源,智能指针提供了这一功能。C++标准提供了两大类智能指针,这里主要介绍shared_ptr。多个shared_ptr可以共享同一个资源,通过引用计数来计算资源被引用次数,并在最后一个拥有资源的shared_ptr被销毁时释放资源。
shared_ptr定义在中,支持下列操作:
函数 | 作用 |
---|---|
shared_ptr sp | 默认构造函数 |
shared_ptr sp(ptr) | 构造函数 |
shared_ptr sp(ptr, del) | 构造函数,指定其资源和deleter |
shared_ptr sp(ptr, del, ac) | 构造函数,指定资源、deleter和allocator |
shared_ptr sp(sp2) | 复制构造函数,sp和sp2共享资源 |
shared_ptr sp(move(sp2)) | 移动语义构造函数 |
shared_ptr sp(sp2, ptr) | alias构造函数,和sp2共享拥有权,但指向*ptr |
shared_ptr sp(wp) | wp是weak_pointer |
shared_ptr sp(move(up)) | up是unique_ptr |
shared_ptr sp(move(ap)) | ap是auto_ptr |
sp.~shared_ptr() | 析构函数,调用deleter |
sp = sp2 | 赋值操作,sp共享sp2的资源 |
sp = move(sp2) | 移动语义赋值操作,sp2把拥有权交给sp |
sp = move(up) | up是unique_ptr |
sp = move(ap) | ap是auto_ptr |
sp1.swap(sp2) | 交换sp1和sp2的pointer和deleter |
swap(sp1,sp2) | 交换sp1和sp2的pointer和deleter |
sp.reset() | 重置shared_ptr为空 |
sp.reset(ptr) | 重置shared_ptr,指定资源,使用默认deleter |
sp_reset(ptr, del) | 重置shared_ptr,指定资源和deleter |
sp_reset(ptr, del, ac) | 重置shared_ptr,指定资源、deleter和allocator |
make_shared() | 为资源构建一个shared_ptr |
sp.get() | 返回指向资源的指针 |
*sp | 返回资源 |
sp->… | 访问资源对象的成员 |
sp.use_count() | 返回资源的引用计数 |
sp.unique() | 判断资源是否只被引用一次 |
==、!=、<、>、<=、>= | 判断指向资源的pointer |
shared_ptr<string> sp_book(new string("C++"));
shared_ptr<string> sp_song = new string("i want something just like this"); //error
sp_book->append(" Standard");
cout << *sp_book << endl;
定义一个管理字符串的智能指针sp_book后,就可以像普通指针一样使用sp_book,上述代码分别使用 -> 操作符访问 string 成员函数 append ,和使用 * 操作符获取字符串对象。但是注意,接受单一指针作传参的构造函数被声明为explicit,所以shared_ptr不支持指针到shared_ptr的隐式类型转换,所以对 sp_song 的赋值会提示编译错误。
make_shared可以用来创建一个shared_ptr对象,但比构造函数更高效。因为构造函数需要申请两次内存,而make_shared只需要一次。
shared_ptr内部数据可以分为两部分:数据块和控制块。数据块就是指向资源的指针,控制块包括引用计数等数据。
shared_ptr的构造函数传参一般是动态申请内存、指向资源对象的指针,这是第一次内存分配,对应shared_ptr的数据块;构造函数内存再为其控制块申请内存,这是第二次内存分配。
make_shared是一个不定参函数模板,根据传参匹配资源对象类的构造函数(所以,make_shared的对象必须有公开的构造函数)。所以,使用make_shared不用先在外部为数据块申请内存,可以在其内部为数据块和控制块同时申请内存
。
class File
{
public:
File(string filename)
{
std::cout << "File(string)" << std::endl;
}
File()
{
std::cout << "File()" << std::endl;
}
};
shared_ptr<File> sp1 = make_shared<File>(); // 输出File()
shared_ptr<File> sp2 = make_shared<File>("main.cpp"); // 输出file(string)
string *str = new string("C++");
shared_ptr<string> sp1(str);
shared_ptr<string> sp2 = sp1; // ok
shared_ptr<string> sp3(sp1); // ok
shared_ptr<string> sp4(str); // error
智能指针之间共享资源时应该使用复制构造函数或者赋值操作符,它们之间共享控制块数据,也就是共享一个引用计数器;sp4的操作会产生两个引用计数器,造成资源释放两次。
前面提到,共享数据的智能指针是应该通过复制构造函数或赋值操作符产生的。所以,如果在被shared_ptr管理的类内部需要用到shared_ptr对象,不能直接用this指针构造一个shared_ptr,否则会导致重复释放资源对象。
正确做法是继承enable_shared_from_this类模板,调用成员函数shared_from_this。如果当前对象被shared_ptr引用,shared_from_this函数会构建一个shared_ptr对象并返回,引用计数加1;否则会抛出bad_weak_ptr异常。
class Z : public std::enable_shared_from_this<Z>
{
public:
void output()
{
shared_ptr<Z> sp = shared_from_this();
std::cout << sp.use_count() << endl;
}
~Z()
{
std::cout << "~Z()" << std::endl;
}
};
shared_ptr<Z> sp(new Z);
sp->output();
shared_str的默认deleter会对资源指针执行delete操作来释放资源,但是对于一些特殊的资源(比如数组需要执行delete []操作,文件需要执行close操作等),默认deleter并不适用,所以需要自行定义deleter。一些构造函数和reset成员函数可以指定deleter,deleter传参可以是函数、函数对象或者lamba表达式。
void arrayDelete(int *p)
{
cout << "arrayDelete" << endl;
delete[] p;
}
class arrayDeleter
{
public:
void operator () (int *p)
{
cout << "arrayDeleter" << endl;
delete[] p;
}
};
// 传函数
shared_ptr<int> sp(new int[10], arrayDelete);
// 传lambda表达式
shared_ptr<int> sp(new int[10], [](int *p) {
delete[] p;
cout << "arrayDeleter" << endl;
});
// 传函数对象
shared_ptr<int> sp(new int[10], arrayDeleter());
shared_ptr有一个特殊的构造函数 shared_ptr sp(sp2, ptr),称作alias构造函数,其语义是共享控制块不共享数据块,在 sp 和 sp2 的生命周期都结束时,sp2 引用的资源对象会被释放,ptr 指向的资源不会被释放。
weak_ptr是一种共享资源、但不拥有资源的智能指针, 所以weak_ptr不会引起资源引用计数的增减。
weak_ptr支持下列操作:
函数 | 作用 |
---|---|
weak_ptr wp | 默认构造函数 |
weak_ptr wp(sp) | 基于shared_ptr构造weak_ptr对象,和shared_ptr共享资源 |
weak_ptr wp(wp2) | 复制构造函数,和另一个weak_ptr共享资源 |
wp = wp2 | 赋值操作符, 和另一个weak_ptr共享资源 |
wp = sp | 用shared_ptr对weak_ptr对象赋值,和shared_ptr共享资源 |
wp.swap(wp2) | 交换两个weak_ptr的资源指针 |
swap(wp1, wp2) | 交换两个weak_ptr的资源指针 |
wp.reset() | 重置weak_ptr为空 |
wp.use_count() | 返回weak_ptr拥有的资源,被shared_ptr引用的次数 |
wp.expired() | 判断weak_ptr拥有的资源,被shared_ptr引用次数是否为0 |
wp.lock() | 返回一个和weak_ptr共享资源的shared_ptr,会引起引用计数加1 |
class Node
{
public:
shared_ptr<Node> sp_parent;
shared_ptr<Node> sp_child;
void setparent(shared_ptr<Node> p)
{
sp_parent = p;
}
void setchild(shared_ptr<Node> c)
{
sp_child = c;
}
~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
{
shared_ptr<Node> parent(new Node()); // sp1
shared_ptr<Node> child(new Node()); // sp2
parent->setchild(child);
child->setparent(parent);
}
}
代码中各个对象的关系如下图所示,parent和child是堆上两个node对象,分别用shared_ptr sp1和sp2管理,其内部也通过sp_child和sp_parent相互引用,这种成环引用的结果是parent和child的引用计数永远不会变成0,也就不会被delete。
flowchart LR
subgraph child
sp_parent
end
subgraph parent
sp_child
end
sp1 --> parent
sp2 --> child
sp_parent --> parent
sp_child --> child
我们需要把sp_child和sp_parent中的至少一个改用weak_ptr来破坏成环引用。假如sp_child改用weak_ptr wp_child,那么child只被sp2拥有:
1、当sp1、sp2被销毁时,parent引用计数变为1,child的引用计数变为0;
2、child被delete,所以sp_parent被销毁,parent引用计数变为0;
3、parent被delete。
weak_ptr共享shared_ptr的资源,但不拥有资源,所以使用前必须确认资源是否存在:
1、使用lock()
2、使用expired(),比use_count()效率更高
3、利用构造函数从weak_ptr对象构造一个shared_ptr对象,如果资源不存在,构造函数会抛出bad_weak_ptr异常