文章目录
- std::shared_ptr
- std::weak_ptr
- std::unique_ptr
- 智能指针多线程安全问题
在实际的 c++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
针对以上这些情况,很多程序员认为 c++ 应该提供更友好的内存管理机制,这样就可以将精力集中于开发项目的各个功能上。事实上,早在 1959 年前后,就有人提出了“垃圾自动回收”机制。所谓垃圾,指的是那些不再使用或者没有任何指针指向的内存空间,而“回收”则指的是将这些“垃圾”收集起来以便再次利用。
如今,垃圾回收机制已经大行其道,得到了诸多编程语言的支持,例如 Java、Python、C#、PHP 等。而 c++ 虽然从来没有公开得支持过垃圾回收机制,但 c++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;c++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。
实际上,每种智能指针都是以类模板的方式实现的,例如 shared_ptr<T>
(其中 T 表示指针指向的具体数据类型)的定义位于 <memory>
头文件,
并位于 std 命名空间中。所以使用智能指针,需要导入头文件 <memory>
并显示标明 std 命名空间。
智能指针对象本身是存放在栈区空间,而智能指针指向的对象是存放在堆区空间,比如:
shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory"); }
// buf 是在栈上
// make_shared<Buffer>("auto free memory"); } 是在堆上分配的
智能指针使用场景
- 使用智能指针可以自动释放占用的内存
shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory"); // Buffer对象分配在堆上,但能自动释放 Buffer *buf = new Buffer("auto free memory"); // Buffer对象分配在堆上,但需要手动 delete释放
- 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题
std::shared_ptr
shared_ptr 共享智能指针特点:
- 多个 shared_ptr 智能指针可以共同使用同一块堆内存。
- 由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针,只有引用计数为 0 时,堆内存才会被自动释放。
也就是说 shared_ptr 共享对象的所有权。
shared_ptr 共享智能指针的内存模型如下:
简单来说,shared_ptr 实现包含了两部分(包含两个指针):
- 一个指向对象(T Object),即指向堆上创建的对象的裸指针(raw_ptr);
- 一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据。即指向内部隐藏的、共享的管理对象(share_count_object)。
shared_ptr 共享智能指针的创建:
(1)通过如下 2 种方式,可以构造出 shared_ptr<T>
类型的空智能指针(注意,空的 shared_ptr 指针,其初始引用计数为 0,而不是 1):
std::shared_ptr<int> p1; //不传入任何实参
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
(2)在构建 shared_ptr 智能指针,也可以明确其指向,同样有两种方式。例如:
// 其指向一块存有 10 这个 int 类型数据的堆内存空间。
std::shared_ptr<int> p3(new int(10));
// 调用std::make_shared<T> 模板函数:c++11 中提供了 std::make_shared<T> ,其可以用于初始化 shared_ptr 智能指针
std::shared_ptr<int> p3 = std::make_shared<int>(10);
我们应该优先使用 make_shared 来构造智能指针,因为他更高效。
(3)除此之外,shared_ptr<T>
模板还提供有相应的拷贝构造函数和移动构造函数,例如:
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);
//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4));
//或者 std::shared_ptr<int> p5 = std::move(p4);
(4)默认情况下,shared_ptr 指针采用 std::default_delete<T>
方法释放堆内存。当然,在初始化 shared_ptr 智能指针时,也可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
// 删除器可以是一个lambda表达式,上面的写法可以改为
std::shared_ptr<int> p8(new int(1), [](int *p) {
cout << "call lambda delete p" << endl;
delete p;});
当我们用 shared_ptr 管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象,代码如下所示:
std::shared_ptr<int> p(new int[10], [](int *p) { delete [] p;});
shared_ptr 共享智能指针的成员方法
【注意】
- p.get() 函数:返回 shared_ptr 中保存的裸指针,即指向堆上创建的对象的裸指针;
- p.reset() 函数:重置shared_ptr;
- 当智能指针调用了 p.reset() 函数的时候,就不会再指向这个对象了。
- 如果没有参数,智能指针会置为空;
- 如果有参数,智能指针会指向新对象(参数);
- 如果还有其它智能指针指向这个对象,那么其他的智能指针的引用计数会减1。如果没有其它智能指针指向这个对象,这个对象将会被释放。
- 当智能指针调用了 p.reset() 函数的时候,就不会再指向这个对象了。
- p.use_count() 函数:返回 shared_ptr 的强引用计数;
- p.unique() 函数:若 p.use_count() 为1,返回true,否则返回false。
#include <iostream>
#include <memory>
int main() {
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
std::cout << *p2 << std::endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) { // 智能指针可以通过重载的 bool 类型操作符来判断
std::cout << "p1 不为空" << std::endl;
}
else {
std::cout << "p1 为空" << std::endl;
}
//以上操作,并不会影响 p2
std::cout << *p2 << std::endl;
//判断当前和 p2 同指向的智能指针有多少个
std::cout << p2.use_count() << std::endl;
return 0;
}
// 执行结果:
// 10
// p1 为空
// 10
// 1
关于 shared_ptr 共享智能指针有几点需要注意:
- 对于 p.get() 函数获取裸指针,谨慎使用其返回值,如果不知道其危险性则永远不要调用 p.get()函数,因为:
- 保存 p.get() 的返回值 ,无论是保存为裸指针还是 shared_ptr 都是错误的:保存为裸指针不知什么时候就会变成空悬指针,保存为 shared_ptr 则产生了独立指针;
- 不要 delete p.get() 的返回值 ,会导致对一块内存 delete 两次的错误
std::shared_ptr<int> ptr(new int(1)); int *p = ptr.get(); delete p; // 一不小心
- 同一裸指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常而崩溃:
int* ptr = new int; std::shared_ptr<int> p1(ptr); std::shared_ptr<int> p2(ptr); //错误
- 不要在函数实参中创建 shared_ptr,因为 c++ 的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,正确的写法应该是先创建智能指针:
// 错误方式: function(shared_ptr<int>(new int), g()); // 正确方式: shared_ptr<int> p(new int); function(p, g());
- 通过
shared_from_this()
可以返回 this 指针,不要把 this 指针作为 shared_ptr 返回出来,因为 this 指针本质就是裸指针,通过 this 返回可能会导致重复析构,不能把 this 指针交给智能指针管理。// 先继承 std::enable_shared_from_this<T> 类,再使用 shared_from_this() class A :std::enable_shared_from_this<A> // 注意:继承 { shared_ptr<A> GetSelf() { return shared_from_this(); // 正确 // return shared_ptr<A>(this); 错误,会导致double free } }
- 尽量使用 make_shared,少用 new。
- 不是 new 出来的空间要自定义删除器。
- 不能将一个原始指针直接赋值给一个智能指针,shared_ptr 不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。例如,下面这种方法是错误的:
std::shared_ptr<int> p = new int(1);
- 要避免循环引用,循环引用导致内存永远不会被释放,造成内存泄漏。
std::weak_ptr
为什么需要 weak_ptr:
share_ptr 虽然已经很好用了,但是有一点 share_ptr 智能指针还是有内存泄露的情况,当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏,如下所示:
using namespace std;
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() { cout << "A delete" << endl; }
};
struct B {
std::shared_ptr<A> aptr;
~B() { cout << "B delete" << endl; }
};
int main() {
auto aaptr = std::make_shared<A>();
auto bbptr = std::make_shared<B>();
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
return 0;
}
上面代码,产生了循环引用,导致 aptr 和 bptr 的引用计数为 2,离开作用域后 aptr 和 bptr 的引用计数 1,但是永远不会为 0,导致指针永远不会析构,产生了内存泄漏,如何解决这种问题呢,答案是使用 weak_ptr。
weak_ptr 特点:
c++11 标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。
当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数,weak_ptr 是用来监视 shared_ptr 的生命周期,它不管理 shared_ptr 内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视 shared_ptr 中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
除此之外,weak_ptr<T>
模板类中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() { cout << "A delete" << endl; }
void Print() { cout << "A" << endl; }
};
struct B {
std::weak_ptr<A> aptr; // 这里改成weak_ptr
~B() { cout << "B delete" << endl; }
void PrintA() {
if (!aptr.expired()) { // 监视shared_ptr的生命周期
auto ptr = aptr.lock();
ptr->Print();
}
}
};
int main() {
auto aaptr = std::make_shared<A>();
auto bbptr = std::make_shared<B>();
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
bbptr->PrintA();
return 0;
}
// 输出:
// A
// A delete
// B delete
weak_ptr 创建:
1、可以创建一个空 weak_ptr 指针,例如:
std::weak_ptr<int> wp1;
2、凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针,例如:
std::weak_ptr<int> wp2 (wp1);
若 wp1 为空指针,则 wp2 也为空指针;反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,则 wp2 也指向该块存储空间(可以访问,但无所有权)。
3、weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,因为在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。例如:
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp);
weak_ptr 模板类提供的成员方法:
【注意】
- 通过 use_count() 方法获取当前观察资源的引用计数。
- 通过 expired() 方法判断所观察资源是否已经释放。
- 通过 lock() 方法获取监视的 shared_ptr。在实际使用中,一定是先调用 lock() 函数锁好资源,再调用 expired() 函数判断资源是否存在 shared_ptr。因为我们需要先通过 lock 锁住资源,那么此时可能存在两种情况,一种是资源存在,那么上锁成功,,此时通过 expired 判断资源存在就可以去使用;一种是资源在上锁前已经被释放或者被其他地方锁住,此时通过 expired 判断资源不存在。
std::weak_ptr<int> gw; auto spt = gw.lock(); // 锁好资源再去判断是否有 std::this_thread::sleep_for(std::chrono::seconds(2)); if (gw.expired()) { cout << "gw Invalid, resource released\n"; } else { cout << "gw Valid, *spt = " << *spt << endl; }
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp1(new int(10));
std::shared_ptr<int> sp2(sp1);
std::weak_ptr<int> wp(sp2);
//输出和 wp 同指向的 shared_ptr 类型指针的数量
std::cout << wp.use_count() << std::endl;
//释放 sp2
sp2.reset();
std::cout << wp.use_count() << std::endl;
//借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
std::cout << *(wp.lock()) << std::endl;
return 0;
}
// 程序执行结果为:
// 2
// 1
// 10
关于 weak_ptr 智能指针有几点需要注意:
(1)weak_ptr 返回 this 指针
shared_ptr 章节中提到不能直接将 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 返回,再看前面的范例
(2)weak_ptr在使用前需要检查合法性。
weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); // sp.use_count()==1
wp = sp; // wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); // wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); // sp_null .use_count()==0;
上述代码中 sp 和 sp_ok 离开了作用域,其容纳的对象已经被释放了。得到了一个容纳 NULL 指针的 sp_null 对象。在使用 wp 前需要调用 wp.expired() 函数判断一下。因为 wp 还仍旧存在,虽然引用计数等于 0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后一个 weak_ptr 对象被析构,这块“堆”存储块才能被回收。否则 weak_ptr 无法直到自己所容纳的那个指针资源的当前状态。
std::unique_ptr
unique_ptr 独占智能指针的特点
和 shared_ptr 指针最大的不同之处在于,unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个 unique_ptr 指针都独自拥有对其所指堆内存空间的所有权,不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr。
unique_ptr 独占智能指针的创建
1、通过以下 2 种方式,可以创建出空的 unique_ptr 指针:
std::unique_ptr<int> p1();
std::unique_ptr<int> p2(nullptr);
2、创建 unique_ptr 指针的同时,也可以明确其指向。例如:
std::unique_ptr<int> p3(new int);
和可以用 make_shared<T>()
模板函数初始化 shared_ptr 指针不同,c++11 标准中并没有为 unique_ptr 类型指针添加类似的模板函数。到 c++14 中才出现 make_unique<T>()
模板函数用于初始化 unique_ptr。
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
3、基于 unique_ptr 类型指针不共享各自拥有的堆内存,因此 c++11 标准中的 unique_ptr 模板类没有提供拷贝构造函数,unique_ptr不允许复制,只提供了移动构造函数。例如:
std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(p4);//错误,堆内存不共享
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数
unique_ptr<int> p6 = p4; // 报错,不能复制
对于调用移动构造函数的 p4 和 p5 来说,p5 将获取 p4 所指堆空间的所有权,而 p4 将变成空指针(nullptr)。
4、默认情况下,unique_ptr 指针采用 std::default_delete<T>
方法释放堆内存。当然,我们也可以自定义符合实际场景的释放规则。值得一提的是,和 shared_ptr 指针不同,为 unique_ptr 自定义释放规则,只能采用函数对象的方式。例如:
//自定义的释放规则
struct myDel {
void operator()(int *p) {
delete p;
}
};
std::unique_ptr<int, myDel> p6(new int);
//std::unique_ptr<int, myDel> p6(new int, myDel());
【注意】shared_ptr 与 unique_ptr 不能够混合使用
unique_ptr<A> my_ptr(new A);
shared_ptr<A> my_other_ptr = my_ptr; // 报错
unique_ptr 模板类提供的成员方法
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p5(new int);
*p5 = 10;
// p 接收 p5 释放的堆内存
int *p = p5.release();
std::cout << *p << std::endl;
//判断 p5 是否为空指针
if (p5) {
std::cout << "p5 is not nullptr" << std::endl;
} else {
std::cout << "p5 is nullptr" << std::endl;
}
std::unique_ptr<int> p6;
// p6 获取 p 的所有权
p6.reset(p);
std::cout << *p6 << std::endl;
return 0;
}
// 运行结果
// 10
// p5 is nullptr
// 10
unique_ptr 独占智能指针与 share_ptr 共享智能指针的区别
除了 unique_ptr 的独占性, unique_ptr 和 shared_ptr 还有一些区别:
1、unique_ptr 可以指向一个数组,代码如下所示
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
2、unique_ptr 指定删除器和 shared_ptr 有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); // 正确
unique_ptr 需要确定删除器的类型,所以不能像 shared_ptr 那样直接指定删除器,只能采用函数对象的方式。
3、使用场景区别
关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。
如果希望只有一个智能指针管理资源或者管理数组就用 unique_ptr,如果希望多个智能指针管理同一个资源就用 shared_ptr。
智能指针多线程安全问题
智能指针的引用计数本身是安全的,因为引用计数采用原子(atomic)计数,原子计数是线程安全的;至于智能指针是否安全需要结合实际使用分情况讨论:
情况1:多线程代码操作的是同一个 shared_ptr 的对象,此时是不安全的。比如 std::thread 的回调函数,是一个lambda表达式,其中引用捕获了一个 shared_ptr:std::thread td([&sp1]()){....});
;又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用:
void fn(shared_ptr<A>&sp) {
...
}
..
std::thread td(fn, sp1);
情况2:多线程代码操作的不是同一个 shared_ptr 的对象。这里指的是管理的数据是同一份,而 shared_ptr 不是同一个对象。比如多线程回调的 lambda 的是按值捕获的对象:std::thread td([sp1]()){....});
;另个线程传递的shared_ptr是值传递,而非引用:
void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);
这时候每个线程内看到的 sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改 sp 指向的操作的时候,是不会出现非预期的异常行为的。也就是说,如下操作是安全的:
void fn(shared_ptr<A> sp) {
...
if (..) {
sp = other_sp;
} else {
sp = other_sp2;
}
}
【注意】所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3 智能指针实际都是指向对象A, 三个线程同时操作对象 A,那对象的数据安全必然是需要对象 A 自己去保证。