文章目录
前言
智能指针(Smart Pointer)是存储指向动态分配(堆)对象的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确地销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数。每使用它一次,内部的引用计数加1,每析构一次,内部引用计数减1,减为0时,删除所指向的堆内存。
C++11 提供了 3 种智能指针:std::shared_ptr,std::uniq_ptr,std::weak_ptr,使用时需要引用 memory 头文件。
一、std::shared_ptr 共享的智能指针
std::shared_ptr使用引用计数,每一个 std::shared_ptr 的拷贝都指向相同的内存。在最后一个 std::shared_ptr 析构的时候,内存才会被释放。。
1.1 std::shared_ptr 的基本用法
1. 初始化
可以通过构造函数、std::make_shared 辅助函数和 reset 方法来初始化 shared_ptr,代码如下:
//=============== std::shared_ptr 的初始化 ==================
std::shared_ptr<int> p(new int(1));
std::shared_ptr<int> p2 = p;
std::shared_ptr<int> ptr;
ptr.reset(new int(1));
if (ptr) {
cout << "shared ptr is initialized.";
}
我们应该优先使用 std::make_shared 来构造智能指针,因为它更高效。
2. 获取原始指针
当需要获取原始指针时,可以通过 get 方法来返回原始指针,代码如下:
std::shared_ptr<int> ptr(new int(1));
int* p = ptr.get();
3. 指定删除器
智能指针初始化可以指定删除器,代码如下:
void DeleteIntPtr(int* p) {
delete p;
}
std::shared_ptr<int> p(new int, DeleteIntPtr);
当 p 的引用计数为 0 时,自动调用删除器 DeleteIntPtr 来释放对象的内存。删除器可以是一个 lambda表达式,因此,上面的写法还可以改为:
std::shared_ptr<int> p(new int, [](int* p){delete p;});
当我们用 shared_ptr 管理动态数组时,需要指定删除器,因为 std::shared_ptr 的默认删除器不支持数组对象,代码如下:
std::shared_ptr<int> p(new int[10], [](int* p){delete[] p;}); //指定 delete[]
也可以将 std::default_delate 作为删除器。default_delate 的内部是通过调用 delete 来实现功能的,代码如下:
std::shared_ptr<int> p(new int[10], std::default_delete<int[]>);
另外,还可以通过封装一个 make_shared_array 方法来让 shared_ptr 支持数组,代码如下:
template<typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
//test
std::shared_ptr<int> p = make_shared_array<int>(10);
1.2 使用 shared_ptr 需要注意的问题
1)不要用一个原始指针初始化多个 shared_ptr,例如下面这样就是错误的:
int *ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr); //logic error
2) 不要在函数实参中创建 shared_ptr,
function (shared_ptr<int>(new int), g()); //有缺陷
因为 C++ 的函数参数的计算顺序在不同的编译器上调用可能是不一样的。一般是从右到左,但也有可能是从左到右。所以可能的过程是先 new int,然后调用 g(),如果恰好 g() 发生异常,而 shared_ptr 还没有创建,则 int 内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int());
f(p, g());
3)不要将 this 指针通过 shared_ptr 返回出来,因为 this 本质上是一个裸指针,这样会导致重复析构。通过 shared_from_this 返回 this 指针。
struct A
{
shared_ptr<A>GetSelf()
{
return shared_ptr<S>(this); //don't do this;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
在这个例子中,由于用同一个指针(this)构造了两个智能指针 sp1 和 sp2,而它们之间是没有任何关系的,在离开作用域之后 this 将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回 this 的shared_ptr 的做法是:让目标类通过派生 std::enable_shared_from_this 类,然后使用基类的成员函数 shared_from_this 来返回 this 的 shared_ptr,示例如下:
class A : public std::enable_shared_from_this<A>
{
std::shared_ptr<A> GetSelf()
{
return shared_from_this();
}
};
std::shared_ptr<A> spy(new A);
std::shared_ptr<A> p = spy->GetSelf(); //OK
- 避免循环引用,智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄漏。
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted!" << endl;
}
};
struct B {
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted!" << endl;
}
};
void testPtr() {
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
测试结果是两个指针A 和B都不会被删除,存在内存泄漏。循环引用导致 ap 和 bp 的引用计数为 2,在离开作用域之后,ap 和 bp 的引用计数减为1,并不会为 0,导致两个指针都不会被析构,产生了内存泄漏。解决办法是把 A 和 B 任何一个成员变量改为 weak_ptr。
二、unique_ptr 独占的智能指针
1. unique_ptr 基本用法
1.1 unique_ptr 的移动
unique_ptr 是一个独占型的智能指针,它不允许其他的智能指针共享其内容的指针,不允许通过赋值将一个 unique_ptr 赋值给另外一个 unique_ptr。下面用法是错误的:
unique_ptr<T> myPtr(new T);
unique_ptr<T> myOtherPtr = myPtr; //错误,不能赋值
unique_ptr 不允许复制,但可以通过函数返回给其他的 unique_ptr,还可以通过 std::move 来转移到其他的 unique_ptr,这样它本身就不再拥有原来指针的所有权了,例如:
unique_ptr<T> myPtr(new T);
unique_ptr<T> myOtherPtr = std::move(myPtr); //OK
unique_ptr<T> ptr = myPtr; //错误,只能移动,不可复制
unique_ptr 不像 shared_ptr 可以通过 make_shared 方法来创建智能指针,C++11 目前还没有提供 make_unique 方法。
1.2 指定数组
unique_ptr 和 shared_ptr 相比,unique_ptr 除了独占这个特点之外,还可以指向一个数组:
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
//而下面的写法是不合法的
std::shared_ptr<int []> ptr(new int[10]); //不合法
1.3 指定删除器
unique_ptr 指定删除器和 shared_ptr 是有差别的,比如:
std::shared_ptr<int> ptr(new int(1), [](int* p){delete p;}); //正确
std::unique_ptr<int> ptr(new int(1), [](int* p){delete p;}); //错误
std::unique_ptr 指定删除器的时候需要确定删除器的类型,所以不能像 shared_ptr 那样直接指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [](int *p) {delete p;});
上面这种写法在 lambda 没有捕获变量的情况下是正确的,如果捕获了变量,则会编译报错:
std::unique_ptr<int, void(*)(int*)> ptr(new int(1), [&](int *p) {delete p;}); //错误,捕获了变量
这是因为 lambda 在没有捕获变量的情况下是可以直接转换为函数指针的,一旦捕获了就无法转换了。如果希望 unique_ptr 的删除器支持 lambda,可以这样写:
std::unique_ptr<int, std::function<void(int*)>> ptr(new int(1), [&](int *p){delete p;});
还可以自定义 unique_ptr 的删除器,比如下面的代码:
#include <memory>
#include <functional>
using namespace std;
struct MyDeleter
{
void operator()(int* p)
{
cout<<"delete"<<endl;
delete p;
}
};
int main()
{
std::unique_ptr<int, MyDeleter> p(new int(1));
return 0;
}
关于 shared_ptr 和 unique_ptr 的使用场景要根据实际应用需求来选择,如果希望只有一个智能指针管理资源或者管理数组就用 unique_ptr,如果希望多个智能指针管理同一个资源就用 shared_ptr。
三、weak_ptr 弱引用的智能指针
弱引用指针 weak_ptr 是用来监视 shared_ptr 的,不会使引用计数加1,它不管理 shared_ptr 内部的指针,主要是为了监视 shared_ptr 的生命周期,更像是 shared_ptr 的一个助手。weak_ptr 没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过 shared_ptr 获得资源的检测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视 shared_ptr 中管理的资源是否存在。weak_ptr 还可以用来返回 this 指针和解决循环引用的问题。
1. weak_ptr 基本用法
1)通过 use_count() 方法获得当前观测资源的引用计数,代码如下:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout<<wp.use_count()<<endl; //结果输出1
2)通过 expired() 方法判断所检测的资源是否已经被释放,代码如下:
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if (wp.expired())
{
std::cout << "weak_ptr 无效,所监测的智能指针已被释放\n";
}
else
{
std::cout << "weak_ptr 有效\n";
}
//结果输出: weak_ptr 有效
3)通过 lock 方法来获取所监视的 shared_ptr,代码如下:
std::weak_ptr<int> gw;
void f()
{
if (gw.expired())
{
std::cout << "gw is expired\n";
}
else
{
auto spt = gw.lock();
std::cout << *spt << "\n";
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
f();
}
f();
return 0;
}
//输出如下:
42
gw is expired
2. weak_ptr 返回 this 指针
上面提到不能将 this 指针返回为 shared_ptr,需要通过派生 std::enable_shared_from_this 类,并通过其方法 shared_from_this 来返回智能指针,原因是 std::enable_shared_from_this 类中有一个 weak_ptr,这个 weak_ptr 用来观测 this 智能指针,调用 shared_from_this() 方法时,会调用内部这个 weak_ptr 的 lock() 方法,将所观测的 shared_ptr 返回。再看一下前面的例子:
struct A : public std::enable_shared_from_this<A>
{
std::shared_ptr<A> GetSelf()
{
return shared_from_this();
}
~A()
{
cout << "A is deleted" << endl;
}
};
int main()
{
std::shared_ptr<A> spy(new A);
std::shared_ptr<A> p = spy->GetSelf();
return 0;
}
//输出结果: A is deleted
在外面创建 A 对象的智能指针和通过该对象返回 this 的智能指针都是安全的,因为 shared_from_this() 是内部的 weak_ptr 调用 lock() 方法之后返回的智能指针,在离开作用域后,spy 的引用计数减为0,A 对象会被析构,不会出现 A 对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数仅在 shared_ptr 的构造函数被调用之后才能使用,因为 enable_shared_from_this 内部的 weak_ptr 只有通过 shared_ptr 才能构造。
3. weak_ptr 解决循环引用的问题
上面提到因为智能指针的循环引用导致内存泄漏的问题,再看一下其示例:
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted!" << endl;
}
};
struct B {
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted!" << endl;
}
};
void testPtr() {
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
std::cout << "ap use count: " << ap.use_count() << endl;
std::cout << "bp use count: " << bp.use_count() << endl;
}
int main()
{
testPtr();
return 0;
}
//输出如下:
ap use count: 2
bp use count: 2
在这个例子中由于循环引用导致 ap 和 bp 的引用计数都为2,离开作用域之后引用计数减为1,不会去删除指针,导致内存泄漏。通过 weak_ptr 就可以解决这个问题,只要将 A 或 B 的任意一个成员变量改为 weak_ptr 即可。
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted!" << endl;
}
};
struct B {
//std::shared_ptr<A> aptr;
std::weak_ptr<A> aptr; //改为 weak_ptr
~B() {
cout << "B is deleted!" << endl;
}
};
void testPtr() {
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
std::cout << "ap use count: " << ap.use_count() << endl;
std::cout << "bp use count: " << bp.use_count() << endl;
}
//输出结果:
ap use count: 1
bp use count: 2
A is deleted!
B is deleted!
这样在对 B 的成员赋值时,即执行 bp->aptr = ap; 时, 由于 aptr 是 weak_ptr,它并不会增加引用计数,所以 ap 的引用计数仍然是1,在离开作用域之后,ap 的引用计数会减为0,A指针会被析构,析构后其内部的 bptr 的引用计数减为1,然后在离开作用域后 bp 引用计数又从1减为0,B对象也会被析构,不会发生内存泄露。