C++智能指针、shared_ptr、enable_shared_from_this
Reference:
1. 简介
由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程更太复杂,最终导致没有delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。
用智能指针便可以有效缓解这类问题。诸如:std::unique_ptr、std::shared_ptr、std::weak_ptr。
2. 具体使用
2.1 总括
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型。在栈对象生命期即将结束时,智能指针通过析构函数释放由它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
访问智能指针包含的裸指针则可以用 get()
函数。由于智能指针是一个对象,所以 if(my_smart_ptr)
永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if(my_smart_ptr.get())
。
智能指针包含了 reset()
方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
我们编写一个测试类来辅助分析:
class Simple {
public:
Simple(int param = 0)
{
number = param;
std::cout << "Simple: " << number << std::endl;
}
~Simple()
{
std::cout << "~Simple: " << number << std::endl;
}
void PrintSomething()
{
std::cout << "PrintSomething: " << info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
2.2 std::auto_ptr
这玩意在C++11开始,被unique_ptr替代了,因为unique_ptr更安全,这一部分可以忽略。
std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include 便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。
void TestAutoPtr()
{
std::auto_ptr<Simple> my_memory(new Simple(1)); // 创建对象,输出:Simple:1
if (my_memory.get())
{ // 判断智能指针是否为空
my_memory->PrintSomething();// 使用 operator-> 调用智能指针对象中的函数
my_memory.get()->info_extend = "Addition";// 使用 get() 返回裸指针,然后给内部对象赋值
my_memory->PrintSomething();// 再次打印,表明上述赋值成功
(*my_memory).info_extend += " other";// 使用 operator* 返回智能指针内部对象,然后用“.”调用智能指针对象中的函数
my_memory->PrintSomething();// 再次打印,表明上述赋值成功
}
}// my_memory 栈对象即将结束生命期,析构堆对象 Simple(1)
执行结果为:
Simple: 1
PrintSomething:
PrintSomething: Addition
PrintSomething: Addition other
~Simple: 1
上述为正常使用 std::auto_ptr 的代码,一切似乎都良好,无论如何不用我们显式的使用该死的 delete 了。
2.3 std::shared_ptr
如果在程序中使用 new 从堆(自由存储区)分配内存,等到不再需要时,应使用 delete 将其释放。C++ 引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL时)表明,需要有更精确的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr。
2.3.1 为什么要使用 shared_ptr
在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
智能指针shared_ptr 是存储动态创建对象的指针,其主要功能是管理动态创建对象的销毁,从而帮助彻底消除内存泄漏和悬空指针的问题。
2.3.2 shared_ptr 的原理和特点
基本原理:就是记录对象被引用的次数,当引用次数为
0
0
0 的时候,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。
特点:它所指向的资源具有共享性,即多个 shared_ptr 可以指向同一份资源,并在内部使用引用计数机制来实现这一点。
共享指针内存:每个 shared_ptr 对象在内部指向两个内存位置:
- 指向对象的指针;
- 用于控制引用计数数据的指针。
- 当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
- 当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。
2.3.3 shared_ptr的使用
2.3.3.1 构造函数创建
shared_ptr<T> ptr;//ptr 的意义就相当于一个 NULL 指针
shared_ptr<T> ptr1(new T());//从new操作符的返回值构造
shared_ptr<T> ptr2(ptr1);//使用拷贝构造函数的方法,会让引用计数加 1
//shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用拷贝构造函数。
/*假设B是A的子类*/
shared_ptr<B> ptrb(new B());
shared_ptr<A> ptra( dynamic_pointer_cast<A>(ptrb) );//从 shared_ptr 提供的类型转换 (cast) 函数的返回值构造
/* shared_ptr 的“赋值”*/
shared_ptr<T> a(new T());
shared_ptr<T> b(new T());
a = b; // 此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1
//shared_ptr 也可以直接赋值,但是必须是赋给相同类型的 shared_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。
//当共享指针 a 被赋值成 b 的时候,如果 a 原来是 NULL, 那么直接让 a 等于 b 并且让它们指向的东西的引用计数加 1;
// 如果 a 原来也指向某些东西的时候,如果 a 被赋值成 b, 那么原来 a 指向的东西的引用计数被减 1, 而新指向的对象的引用计数加 1。
/*已定义的共享指针指向新的new对象————reset()*/
shared_ptr<T> ptr(new T());
ptr.reset(new T()); // 原来所指的对象会被销毁
2.3.3.2 make_shared 辅助函数创建
std::shared_ptr<int> foo = std::make_shared<int> (10);
2.3.4 shared_ptr 常用函数
-
get()
: 返回当前存储的指针(就是被 shared_ptr 所管理的指针) 。
但是不建议使用 get() 函数获取 shared_ptr 关联的原始指针,因为如果在 shared_ptr 析构之前手动调用了delete函数,会导致错误。(生成的是指针不是智能指针)shared_ptr<T> ptr(new T()); T *p = ptr.get(); // 获得传统 C 指针
-
use_count()
: 表示当前引用计数shared_ptr<T> a(new T()); a.use_count(); //获取当前的引用计数
-
reset()
: 表示重置当前存储的指针。shared_ptr<T> a(new T()); a.reset(); // 此后 a 原先所指的对象会被销毁,并且 a 会变成 NULL
2.3.5 示例
示例1:shared_ptr的基础应用:
#include <iostream>
#include <memory> // 共享指针必须要包含的头文件
using namespace std;
int main()
{
// 最好使用make_shared创建共享指针,
shared_ptr<int> p1 = make_shared<int>();//make_shared 创建空对象,
*p1 = 10;
cout << "p1 = " << *p1 << endl; // 输出10
// 打印引用个数:1
cout << "p1 count = " << p1.use_count() << endl;
// 第2个 shared_ptr 对象指向同一个指针
std::shared_ptr<int> p2(p1);
// 输出2
cout << "p2 count = " << p2.use_count() << sendl;
cout << "p1 count = " << p1.use_count() << endl;
// 比较智能指针,p1 等于 p2
if (p1 == p2) {
std::cout<< "p1 and p2 are pointing to same pointer\n";
}
p1.reset();// 无参数调用reset,无关联指针,引用个数为0
cout << "p1 Count = " << p1.use_count() << endl;
p1.reset(new int(11));// 带参数调用reset,引用个数为1
cout << "p1 Count = " << p1.use_count() << endl;
p1 = nullptr;// 把对象重置为NULL,引用计数为0
cout << "p1 Reference Count = " << p1.use_count() << endl;
if (!p1) {
cout << "p1 is NULL" << endl; // 输出
}
return 0;
}
示例2:shared_ptr作返回值:
shared_ptr<string> factory(const char* p)
{
shared_ptr<string> p1 = make_shared<string>(p);
return p1;
}
void use_factory()
{
shared_ptr<string> p = factory("helloworld");
int num1 = p.use_count();
cout << *p << endl;//!离开作用域时,p引用的对象被销毁。
}
shared_ptr<string> return_share_ptr()
{
shared_ptr<string> p = factory("helloworld");
cout << *p << endl;
return p; //!返回p时,引用计数进行了递增操作。
} //!p离开了作用域,但他指向的内存不会被释放掉。
int main()
{
use_factory();
auto p = return_share_ptr();
cout << p.use_count() << endl;
system("pause");
return 0;
}
//可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。
//例如,当用一个shared_ptr去初始化另一个shared_ptr;当我们给shared_ptr赋予一个新的值或者是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。
//一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。
示例3:容器中的shared_ptr-记得用erase节省内存:
//对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,他就不会被释放掉。
//由于这个特性,保证shared_ptr在不用之后不再保留就非常重要了,
//通常这个过程能够自动执行而不需要人工干预,有一种例外就是我们将shared_ptr放在了容器中。
//所以永远不要忘记erase不用的shared_ptr。
#include <iostream>
using namespace std;
int main()
{
list<shared_ptr<string>>pstrList;
pstrList.push_back(make_shared<string>("1111"));
pstrList.push_back(make_shared<string>("2222"));
pstrList.push_back(make_shared<string>("3333"));
pstrList.push_back(make_shared<string>("4444"));
for(auto p:pstrList)
{
if(*p == "3333");
{
/*do some thing!*/
}
cout<<*p<<endl;
}
/*包含"3333"的数据我们已经使用完了!*/
for(list<shared_ptr<string>>::iterator itr = pstrList.begin();itr!=pstrList.end();++itr)
{
if(**itr == "3333"){
cout<<**itr<<endl;
pstrList.erase(itr);
}
}
cout<<"-------------after remove------------"<<endl;
for(auto p:pstrList)
{
cout<<*p<<endl;
}
while(1)
{
/*do somthing other works!*/
/*遍历 pstrList*/ //!这样不仅节约了大量内存,也为容器的使用增加了效率
}
}
示例4:shared_ptr:对象共享相同状态:
//使用shared_ptr在一个常见的原因是允许多个多个对象共享相同的状态,而非多个对象独立的拷贝!
#include <iostream>
using namespace std;
void copyCase()
{
list<string> v1({"1","b","d"});
list<string> v2 = v1; //!v1==v2占用两段内存
v1.push_back("cc"); //!v1!=v2
for(auto &p:v1){
cout<<p<<endl;
}
cout<<"--------void copyCase()---------"<<endl;
for(auto &p:v2){
cout<<p<<endl;
}
} //v1和v2分属两个不同的对象,一个改变不会影响的状态。
void shareCase()
{
shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb");
shared_ptr<list<string>> v2 = v1;
(*v1).push_back("c2c");
for(auto &p:*v1){
cout<<p<<endl;
}
cout<<"----------shareCase()--------"<<endl;
for(auto &p:*v2){
cout<<p<<endl;
}
} //v1和v2属于一个对象的两个引用,有引用计数为证,其内容的改变是统一的。
int main()
{
copyCase();
cout<<"++++++++++++++++"<<endl;
shareCase();
}
示例5:shared_ptr管理动态数组:
//默认情况下,shared_ptr指向的动态的内存是使用delete来删除的。
//这和我们手动去调用delete然后调用对象内部的析构函数是一样的。
//与unique_ptr不同,shared_ptr不直接管理动态数组。
//如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器来替代delete。
#include <iostream>
using namespace std;
class DelTest
{
public:
DelTest(){
j= 0;
cout<<" DelTest()"<<":"<<i++<<endl;
}
~DelTest(){
i = 0;
cout<<"~ DelTest()"<<":"<<i++<<endl;
}
static int i,j;
};
int DelTest::i = 0;
int DelTest::j = 0;
void noDefine()
{
cout<<"no_define start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10]);
}
void slefDefine()
{
cout<<"slefDefine start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});
} //!传入lambada表达式代替delete操作。
int main()
{
noDefine(); //!构造10次,析构1次。内存泄漏。
cout<<"----------------------"<<endl;
slefDefine(); //!构造次数==析构次数 无内存泄漏
}
2.3.6 常见错误
1. 不能使用原始指针初始化多个shared_ptr(注意这里说的是原始指针,使用智能指针初始化智能指针是没有问题的)
int* p11 = new int;
std::shared_ptr<int> p12(p11);
std::shared_ptr<int> p13(p11);
//由于p1和p2是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针,
//比如p1已经将aa delete了,这时候p2里边的aa就是空悬指针了
2. 不允许以暴露裸漏的指针进行赋值
//带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样
std::shared_ptr<int> p1 = new int();//不能隐式转换,类型不匹配
3. 不要用栈中的指针构造 shared_ptr 对象
int x = 12;
std::shared_ptr<int> ptr(&x);
shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。
4. 不要使用shared_ptr的get()初始化另一个shared_ptr
Base *a = new Base();
std::shared_ptr<Base> p1(a);
std::shared_ptr<Base> p2(p1.get());
//p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
5. 多线程中使用 shared_ptr
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
- 一个 shared_ptr 对象实体可被多个线程同时读取
- 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
- 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁
2.3.7 额外内容
当一个对象被 shared_ptr
管理,而同时存在 weak_ptr
引用时,对象不会被释放直到所有 shared_ptr
和 weak_ptr
都释放了对它的引用。
shared_ptr
和 weak_ptr
是C++中用于管理动态分配的资源(比如内存)的智能指针。它们的区别在于 shared_ptr
拥有对象的共享所有权,而 weak_ptr
则不拥有共享所有权,只是观察 shared_ptr
的状态。
当所有 shared_ptr
释放了对对象的引用时,对象会被销毁,不再存在于内存中。但 weak_ptr
可以在这之前被销毁,因为它不影响对象的生命周期,只是观察对象是否还存在。
因此,只有当所有 shared_ptr
和 weak_ptr
都释放了对对象的引用时,对象的内存才会被释放。
2.4 std::enable_shared_from_this
2.4.1 std::enable_shared_from_this 应用场景
在记录std::enable_shared_from_this之前,需要先看一下原始指针与shared_ptr在使用过程中一些问题。先看一代码段:
#include <iostream>
#include <memory>
class Point;
typedef std::shared_ptr<Point> ptr_point;
class Point
{
public:
Point(int _x, int _y) : x(_x), y(_y) {
}
~Point() {
std::cout << "~Point()" << std::endl;
}
ptr_point get_this() {
return ptr_point(this);
}
private:
int x;
int y;
};
int main(int argc, char **argv)
{
Point *raw_pointer = new Point(8, 9);
std::shared_ptr<Point> ptr(raw_pointer); //[line 1]
std::shared_ptr<Point> ptr2(raw_pointer); //[line 2]
return 0;
}
这就是2.3.6节中提到的一个问题,运行该程序会出现以下错误:
上边代码[line 1] [line 2] 是使用原始指针(raw pointer)来创建了两个shared_ptr对象,这样做会造成一个严重问题那就是那一资源会被free两次。为啥会这样?如下图可以看看内存情况:ptr与ptr2指向同一资源(0x555555768e70),但是两shared_ptr的引用计数器不是同一个,意味着同一资源要被释放两次,这绝对是严重错误。
但是我们确实有这样一种应用场景:就如类Point被shared_ptr管理。且在该类的某成员函数中要把当前类对象作为参数传递给其他函数时,但又要避免上述问题出现。我们就可以使用std::enable_shared_from_this<T>
。
在某些情况下,我们使用以下方式即可:
int main(int argc, char **argv)
{
Point *raw_pointer = new Point(8, 9);
std::shared_ptr<Point> ptr(raw_pointer);//[line 1]
std::shared_ptr<Point> ptr2(ptr); //[line 2]
return 0;
}
但是,我们经常需要将指针传入一个 API 内,这种方法就不太适合了。
2.4.2 std::enable_shared_from_this 使用方式
使用 std::enable_shared_from_this<T>
需要以下两个步骤:
- 让类A继承
std::enable_shared_from_this<T>
,同时将该类类名作为模板参数; - 在类成员函数中使用
shared_from_this()
传递该类对象。(这里很重要!!!前面做了继承,使用还是在这里)
更新上面代码:
#include <iostream>
#include <memory>
class Point;
typedef std::shared_ptr<Point> ptr_point;
class Point : public std::enable_shared_from_this<Point>
{
public:
Point(int _x, int _y) : x(_x), y(_y) {
}
~Point() {
std::cout << "~Point()" << std::endl;
}
ptr_point get_this() {
return shared_from_this();
}
private:
int x;
int y;
};
int main(int argc, char **argv)
{
Point *raw_pointer = new Point(8, 9);
std::shared_ptr<Point> ptr(raw_pointer); //[line 1]
std::shared_ptr<Point> ptr2(raw_pointer->get_this()); //[line 2]
return 0;
}
问题解决。
2.5 weak_ptr
weak_ptr
是 C++11 引入的一种智能指针,用于解决 shared_ptr
的循环引用问题,以及避免悬空指针的情况。它通常与 shared_ptr 一起使用,但是 weak_ptr 不会增加引用计数,也不会拥有对象的所有权。
主要用途有两个:
- 解决循环引用问题:当多个对象之间相互持有
shared_ptr
形成了循环引用时,可能会导致内存泄漏,因为它们的引用计数永远不会变为 0。通过将其中一个或多个指向的shared_ptr
转换为weak_ptr
,可以破坏循环引用,当没有shared_ptr
引用对象时,它会自动销毁。 - 避免悬空指针问题:可以通过调用
lock()
方法来将weak_ptr
转换为一个临时的shared_ptr
,如果对象仍然存在,则可以安全地使用该shared_ptr
。
悬空指针(dangling pointer)
:一个指针指向的内存地址已经被释放或者不再有效,但是该指针仍然保留着该地址的引用。在访问悬空指针时,可能会导致未定义的行为,包括程序崩溃、数据损坏或者安全漏洞。C++ 野指针与悬空指针的区别
悬空指针通常是由于以下几种情况导致的:- 释放了内存但未将指针置空:当使用
delete
或free
释放了动态内存分配的内存,但没有将指向该内存的指针设置为nullptr
或NULL
,那么这些指针就变成了悬空指针; - 指向栈上或局部变量的指针:如果一个指针指向了一个局部变量或者栈上的内存,当函数返回时,这些内存就会被释放,这样指针就会变成悬空指针;
- 指针过期:当指针指向的对象在其生命周期内被释放或销毁,但是指针仍然被使用时,就会导致指针变成悬空指针。
- 释放了内存但未将指针置空:当使用
以下是 weak_ptr
的基本用法:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> shared = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weak = shared;
if (auto sharedPtr = weak.lock()) {
// Use sharedPtr safely
std::cout << "Using weak_ptr: Object is still alive." << std::endl;
} else {
std::cout << "Using weak_ptr: Object has been deleted." << std::endl;
}
shared.reset(); // Destroy the shared_ptr
if (auto sharedPtr = weak.lock()) {
std::cout << "Using weak_ptr: Object is still alive." << std::endl;
} else {
std::cout << "Using weak_ptr: Object has been deleted." << std::endl;
}
return 0;
}
输出为:
MyClass constructor
Using weak_ptr: Object is still alive.
MyClass destructor
Using weak_ptr: Object has been deleted.
在这个示例中,shared_ptr
和 weak_ptr
共同持有 MyClass 的实例。当 shared_ptr
被销毁后,weak_ptr
也能感知到,并且调用 lock()
方法将返回一个空的 shared_ptr。
请注意,weak_ptr
不能直接访问对象的成员,因为它不拥有对象的所有权,必须通过调用 lock()
方法获得一个有效的 shared_ptr
来访问对象。同时,为了确保安全性,应在使用 lock()
后检查 shared_ptr
是否为非空,因为在某些情况下对象可能已经被销毁。
2.5.1 循环引用
循环引用问题通常发生在两个或多个对象彼此持有对方的 shared_ptr
时,导致这些对象无法被正确释放,从而造成内存泄漏。weak_ptr
能够解决这个问题的关键在于它不会增加引用计数,从而打破了循环引用的关系。
具体来说,当两个或多个对象相互持有对方的 shared_ptr
时,它们的引用计数永远不会降为
0
0
0,因为彼此都持有对方的引用。这样一来,这些对象就永远不会被销毁,因为它们的引用计数永远不会为
0
0
0。
通过使用 weak_ptr
,对象可以持有对另一个对象的弱引用,而不是持有对其 shared_ptr
的强引用。因此,即使循环引用存在,其中的 weak_ptr
并不会阻止对象的销毁。当所有 shared_ptr
都释放了对对象的所有权时,对象就会被销毁,即使有 weak_ptr
存在也不会阻止这一过程。
因此,weak_ptr的存在使得对象之间可以相互引用,而不会导致循环引用问题和内存泄漏。通过使用lock()方法,我们可以检查weak_ptr是否有效,并尝试获取对被观察对象的shared_ptr,从而安全地使用它。
比如有下面的循环引用示例:
#include <iostream>
#include <memory>
class B; // 提前声明类B
class A {
public:
std::shared_ptr<B> bPtr;
A() {
std::cout << "A constructor\n";
}
~A() {
std::cout << "A destructor\n";
}
};
class B {
public:
std::shared_ptr<A> aPtr;
B() {
std::cout << "B constructor\n";
}
~B() {
std::cout << "B destructor\n";
}
};
int main() {
std::shared_ptr<A> aPtr = std::make_shared<A>();
std::shared_ptr<B> bPtr = std::make_shared<B>();
// 建立循环引用
aPtr->bPtr = bPtr;
bPtr->aPtr = aPtr;
// 打印引用计数,会发现无法析构
std::cout << "aPtr use_count: " << aPtr.use_count() << std::endl;
std::cout << "bPtr use_count: " << bPtr.use_count() << std::endl;
return 0;
}
输出为:
A constructor
B constructor
aPtr use_count: 2
bPtr use_count: 2
在这个例子中,类A 和 类B 互相持有对方的 shared_ptr
,形成了循环引用。这会导致在 main() 函数结束时,A 和 B 对象的析构函数都不会被调用,因为它们的引用计数永远不会降为
0
0
0。
要解决这个问题,我们可以使用 weak_ptr
来打破循环引用。下面是修改后的示例:
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> bPtr;
A() {
std::cout << "A constructor\n";
}
~A() {
std::cout << "A destructor\n";
}
};
class B {
public:
std::weak_ptr<A> aWeakPtr;
B() {
std::cout << "B constructor\n";
}
~B() {
std::cout << "B destructor\n";
}
};
int main() {
std::shared_ptr<A> aPtr = std::make_shared<A>();
std::shared_ptr<B> bPtr = std::make_shared<B>();
std::cout << "Step1, aPtr use_count: " << aPtr.use_count() << std::endl;
std::cout << "Step1, bPtr use_count: " << bPtr.use_count() << std::endl;
// 建立循环引用
aPtr->bPtr = bPtr;
bPtr->aWeakPtr = aPtr;
// 由于aWeakPtr是weak_ptr,不会增加引用计数
// aPtr的引用计数为1,bPtr的引用计数为2
// 当aPtr和bPtr超出作用域时,它们会被正确地销毁
std::cout << "Step2, aPtr use_count: " << aPtr.use_count() << std::endl;
std::cout << "Step2, bPtr use_count: " << bPtr.use_count() << std::endl;
return 0;
}
输出为:
A constructor
B constructor
Step1, aPtr use_count: 1
Step1, bPtr use_count: 1
Step2, aPtr use_count: 1
Step2, bPtr use_count: 2
A destructor
B destructor
在这个修改后的示例中,类B 持有 A 对象的 weak_ptr
,而不是 shared_ptr
。这样做可以防止形成循环引用,从而保证在 main() 函数结束时,A 和 B对象的析构函数可以正确地被调用。
2.5.2 weak_ptr 成员变量
-
swap
:void swap (weak_ptr& x) noexcept;
两个weak_ptr
间交换内容。int main () { std::shared_ptr<int> sp1 (new int(10)); std::shared_ptr<int> sp2 (new int(20)); std::weak_ptr<int> wp1(sp1); std::weak_ptr<int> wp2(sp2); wp1.swap(wp2); std::cout << "sp1 -> " << *sp1 << '\n'; std::cout << "sp2 -> " << *sp2 << '\n'; std::cout << "wp1 -> " << *wp1.lock() << '\n'; std::cout << "wp2 -> " << *wp2.lock() << '\n'; return 0; }
-
reset
:void reset() noexcept;
重置weak_ptr
指针,让该对象为空(重置的是weak_ptr
这个指针,而不会重置里面的shared_ptr
)。int main () { std::shared_ptr<int> sp (new int(10)); std::weak_ptr<int> wp(sp); std::cout << "1. wp " << (wp.expired()?"is":"is not") << " expired\n"; wp.reset(); std::cout << "2. wp " << (wp.expired()?"is":"is not") << " expired\n"; if(sp){ std::cout << "sp is not null:" << *sp << "\n"; } return 0; }
输出:
1. wp is not expired 2. wp is expired sp is not null:10
-
use_count
:long int use_count() const noexcept;
该函数返回与该对象共享指向相同指针的shared_ptr
对象数量。请注意,拥有组中的weak_ptr
对象不会被计算在内。如果这是一个空的weak_ptr
,则该函数返回零。库实现不需要保持对任何特定所有者集合的计数,因此调用此函数可能不是有效的。要特别检查use_count
是否为0,可以使用成员expired
,这可能更快。class B; class A { public: std::shared_ptr<B> bPtr; A() { std::cout << "A constructor\n"; } ~A() { std::cout << "A destructor\n"; } }; class B { public: std::weak_ptr<A> aWeakPtr; B() { std::cout << "B constructor\n"; } ~B() { std::cout << "B destructor\n"; } }; class C { public: std::shared_ptr<A> aPtr; C() { std::cout << "C constructor\n"; } ~C() { std::cout << "C destructor\n"; } }; int main() { std::shared_ptr<A> aPtr = std::make_shared<A>(); std::shared_ptr<B> bPtr = std::make_shared<B>(); std::cout << "Step1, aPtr use_count: " << aPtr.use_count() << std::endl; std::cout << "Step1, bPtr use_count: " << bPtr.use_count() << std::endl; // 建立循环引用 aPtr->bPtr = bPtr; bPtr->aWeakPtr = aPtr; // 由于aWeakPtr是weak_ptr,不会增加引用计数 // aPtr的引用计数为1,bPtr的引用计数为2 // 当aPtr和bPtr超出作用域时,它们会被正确地销毁 std::cout << "Step2, aPtr use_count: " << aPtr.use_count() << std::endl; std::cout << "Step2, bPtr use_count: " << bPtr.use_count() << std::endl; std::shared_ptr<C> cPtr = std::make_shared<C>(); cPtr->aPtr = aPtr; std::cout << "Step3, aPtr use_count: " << aPtr.use_count() << std::endl; std::cout << "Step3, aWeakPtr use_count: " << bPtr->aWeakPtr.use_count() << std::endl; return 0; }
输出:
A constructor B constructor Step1, aPtr use_count: 1 Step1, bPtr use_count: 1 Step2, aPtr use_count: 1 Step2, bPtr use_count: 2 C constructor Step3, aPtr use_count: 2 Step3, aWeakPtr use_count: 2 C destructor A destructor B destructor
-
expired
:bool expired() const noexcept;
检查是否过期。该函数返回weak_ptr
对象是否为空,或者它所属的拥有组中没有更多的shared_ptr
。当锁定时,已过期的指针行为类似于空的weak_ptr
对象,因此无法再用来恢复拥有的shared_ptr
。该函数应返回与use_count()==0
相同的值,尽管可能以更高效的方式实现。 -
lock
:shared_ptr<element_type> lock() const noexcept;
如果weak_ptr
对象未过期,则返回一个保留了weak_ptr
对象信息的shared_ptr
。如果weak_ptr
对象已过期(包括为空的情况),该函数返回一个空的shared_ptr
(就像默认构造的那样)。由于shared_ptr
对象被视为所有者,该函数锁定了所拥有的指针,防止它被释放(至少在返回的对象不释放它之前)。该操作是原子执行的。
2.5.3 额外问题
shared_ptr 在 weak_ptr 还有引用的时候会被释放么?
→
\rightarrow
→ 当一个对象被 shared_ptr
管理,而同时存在 weak_ptr
引用时,对象不会被释放直到所有 shared_ptr
和 weak_ptr
都释放了对它的引用。shared_ptr
和 weak_ptr
是C++中用于管理动态分配的资源(比如内存)的智能指针。它们的区别在于 shared_ptr
拥有对象的共享所有权,而 weak_ptr
则不拥有共享所有权,只是观察 shared_ptr
的状态。当所有 shared_ptr
释放了对对象的引用时,对象会被销毁,不再存在于内存中。但 weak_ptr
可以在这之前被销毁,因为它不影响对象的生命周期,只是观察对象是否还存在。因此,只有当所有 shared_ptr
和 weak_ptr
都释放了对对象的引用时,对象的内存才会被释放。
shared_ptr
在内部维护了一个引用计数(reference count)
,用于跟踪有多少个 shared_ptr
指向相同的对象。每当创建一个新的 shared_ptr
对象指向该对象时,引用计数就会增加;当 shared_ptr
对象被销毁或者不再指向该对象时,引用计数就会减少。而 weak_ptr
则不直接影响引用计数,而是通过一个额外的 弱引用计数(weak reference count)
来跟踪对应的 shared_ptr 是否还存在。当创建一个 weak_ptr
时,它会与相应的 shared_ptr
共享同一个控制块(control block)
,控制块中记录了强引用计数(即被 shared_ptr
维护的引用计数)和弱引用计数。当所有的 shared_ptr
和 weak_ptr
都不再指向该对象时,引用计数和弱引用计数都会变为 0,对象就会被销毁。因此,shared_ptr
不会直接知道有多少个 weak_ptr
引用了相同的对象,而是通过控制块中的弱引用计数来判断是否还有 weak_ptr
引用,从而确定对象是否应该被销毁。