C++ unique_ptr、shared_ptr、weak_ptr全面解析

unique_ptr

所有权转移

unique_ptr不能被拷贝和用于赋值,因为unique_ptr删掉了这两个函数
在这里插入图片描述
但是底层源码重载了传右值的拷贝构造
所以可以通过std::move来通过转移所有权

unique_ptr<Data> p6(new Data());
//不可复制构造和赋值复制
//unique_ptr<Data>p7 = p6; 错误

//p6释放所有权 转移到p7
unique_ptr<Data>p7 = move(p6);

unique_ptr<Data>p8(new Data());
p7 = move(p8);//重新移动赋值,原有的p6会被释放掉
//重置空间,原空间清理
p7.reset(new Data());

所有权释放

注意!当unique_ptr释放所有权以后智能指针就不会再管理这块空间,需要自己手动释放空间!

//释放所有权
unique_ptr<Data>p9(new Data());
auto ptr9 = p9.release();//注意,release释放所有权以后要自己清理空间
delete ptr9;//!!!!!!

make_unique 和直接(new)unique_ptr

区别一 :需要一次性处理多个资源分配的地方make_unique比unique_ptr更安全

void process_data(
    std::unique_ptr<Data> p1, 
    std::unique_ptr<Data> p2
);

process_data(
    std::unique_ptr<Data>(new Data("A")),  // 分配资源 A
    std::unique_ptr<Data>(new Data("B"))   // 分配资源 B
);

编译器在构造函数参数时,​执行顺序是不确定的。可能的执行顺序例如:

  1. new Data(“A”) → 成功,得到一个裸指针 A
  2. new Data(“B”) → 成功,得到一个裸指针 B*
  3. 构造unique_ptr 接管 A*
  4. 构造 unique_ptr 接管 B*

如果中间发生异常:

  1. new Data(“A”) → 成功,得到 A*
  2. new Data(“B”) → ​抛出异常(例如内存不足)​ ​此时 A* 尚未被 unique_ptr 接管!​ 异常被抛出后,裸指针 A* 无法被自动释放 → ​内存泄漏。

如果选用make_unique

process_data(
    std::make_unique<Data>("A"),  // 直接构造并接管资源 A
    std::make_unique<Data>("B")   // 直接构造并接管资源 B
);

此时每一步的执行:

  1. make_unique(“A”) → ​立即构造对象并封装到 unique_ptr,无裸指针暴露
  2. make_unique(“B”) → 同上 如果 make_unique(“B”) 抛出异常:
    make_unique(“A”) 已经返回的 unique_ptr 会正常析构 → 资源 A 被自动释放没有泄漏!

区别二 :直接new支持自定义删除器

new支持在构造 unique_ptr 时指定自定义删除器

std::unique_ptr<MyClass, Deleter> p(new MyClass, custom_deleter);

但是make_unique不支持,只能使用默认的 delete 操作符。

shared_ptr

shared_ptr智能指针指向同一个对象的不同成员

sc2和sc3 分别 指向sc1的index1成员和index2成员,使sc1的引用计数+2

class Data
{
public:
	Data() {

		cout<< "Begin Data" << endl;
	}
	~Data() { cout<< "End Data" << endl; }
	int index1 = 0;
	int index2 = 0;
};

{
	shared_ptr<Data>sc1(new Data);
	//打印引用计数 = 1
	cout << "sc1.use_count() = " << sc1.use_count() << endl;

	shared_ptr<int>sc2(sc1,&sc1->index1);//引用计数+1
	shared_ptr<int>sc3(sc1, &sc1->index2);//引用计数+1
	//打印引用计数 = 3
	cout << "sc1.use_count() = " << sc1.use_count() << endl;
}

在这里插入图片描述

循环引用问题

当两个或多个对象通过 shared_ptr ​互相持有对方时,它们的引用计数永远不会归零,导致内存无法释放。

	class A
	{
	public:
		A() { cout << "Create A" << endl; }
		~A() { cout << " Drop A " << endl; }

		void Do()
		{
			cout << "Do b2.use_count() = " << b2.use_count() << endl;
			auto b = b2.lock(); //复制一个shared_ptr 引用计数加一
			cout << "Do b2.use_count() = " << b2.use_count() << endl;

		}

		shared_ptr<B> b1;//强智能指针
		weak_ptr<B> b2;  //弱智能指针
	};
	class B
	{
	public:
		B() { cout << "Create B" << endl; }
		~B(){ cout << " Drop B " << endl; }
		shared_ptr<A> a1;//强智能指针
		weak_ptr<A> a2;  //弱智能指针

	};

	{
		auto a = make_shared<A>();//a引用计数+1
		auto b = make_shared<B>();//b引用计数+1

		a->b1 = b;//b引用计数+1
		//出作用域前,引用计数为2
		cout << "a->b1 = b;b.use_count()=" << b.use_count() << endl;

		b->a1 = a;//a引用计数+1
		//出作用域前,引用计数为2
		cout << "b->a1 = a;a.use_count()=" << a.use_count() << endl;

	}

由于调用问题,导致出作用域以后a和b的引用计数都还是1,所以空间没有被释放

在这里插入图片描述

改成使用weak_ptr

	{
		auto a = make_shared<A>();
		auto b = make_shared<B>();

		a->b2 = b;//weak_ptr 引用计数不加一
		a->Do();//Do函数里引用计数加一,出Do函数作用域减一
		cout << "a->b2 = b;b.use_count()=" << b.use_count() << endl;

		b->a2 = a;//引用计数不加一
		cout << "b->a2 = a;a.use_count()=" << a.use_count() << endl;

	}//不会产生循环引用问题

为什么weak_ptr能解决shared_ptr的循环引用问题

weak_ptr 本身不拥有资源所有权
它只是观察 shared_ptr 管理的对象,不会增加引用计数。

所以a->b2 = b;这句不会增加b的引用计数
同理b->a2 = a;这句也不会增加a的引用计数

但是由于weak_ptr只是一个观察者,无法访问任何资源,仅“观察”资源,不拥有所有权如果想要访问资源该怎么办呢?

如同a->Do();里做的那样
只要原 shared_ptr(即 a)未释放资源,就可以通过 lock() 获取有效的 shared_ptr 并访问。

void Do()
{
	cout << "Do b2.use_count() = " << b2.use_count() << endl;
	auto b = b2.lock(); //返回一个shared_ptr 引用计数加一
	
	//这里还可以做其他的访问shared_ptr资源的操作

	cout << "Do b2.use_count() = " << b2.use_count() << endl;

}//出作用域加上的那个引用计数自动-1

通过weak_ptr.lock()返回 一个 shared_ptr 并将引用计数加一

(注意!只有当原shared_ptr对象还存在的时候才会返回shared_ptr,否者返回nullptr)

除了放函数里,还能放判断条件里

    if (auto a_shared = b->a_weak.lock()) {
        // 返回nullptr 说明shared_ptr被释放
        //不会执行此处
    } else {
 		//引用计数+1
        std::cout << "A is already destroyed!" << std::endl;  // 输出此句
    }//引用计数-1

通过这种方式,weak_ptr 可以安全地观察资源,而不会导致循环引用或内存泄漏

make_shared 和 直接 new 一个shared_ptr的区别

区别一:make_shared资源分配更安全

原因和make_unique一样,这里就不再赘述了

区别二:直接new支持自定义删除器

区别三:内存分配方式不同

由于shared_ptr除了维护对象本身的内存以外还要维护一个控制块

  • ​强引用计数​(use_count):记录有多少个 shared_ptr 共享对象
  • ​弱引用计数​(weak_count):记录有多少个weak_ptr 观察对象
  • 自定义删除器​(如果存在)。 ​
  • 对象指针​(指向实际分配的对象)。

make_shared 在底层会 ​一次性分配一块连续内存,既存储对象本身,也存储控制块。

但是new shared_ptr会有两次分配

std::shared_ptr<MyClass> p(new MyClass);
  1. 第一次分配:new MyClass 分配对象内存。
  2. ​第二次分配:shared_ptr 构造函数内部为控制块分配内存。

为什么make_unique和直接new unique_ptr都是一次分配内存

因为unique_ptr 不需要维护引用计数,因此 ​没有控制块。无论通过 make_unique 还是直接 new,都只需分配对象内存

shared_ptr合并分配的缺点

对象和控制块内存绑定,即使所有 shared_ptr 销毁,若仍有 weak_ptr 存在,对象内存仍然需等待控制块释放(但析构函数会被及时调用)

shared_ptr控制块内存的延迟释放是内存泄漏吗?

由于make_shared 是一次性分配一块连续内存,同时存储 ​对象实例 和 ​控制块​(包含引用计数、弱引用计数等)

所以当没有weak_ptr存在时

通过make_shared建立的shared_ptr:

  • 对象析构函数立即被调用。
  • 整块内存(对象 + 控制块)立即释放。

通过new建立的shared_ptr:

  • 对象内存立即释放。
  • 控制块内存也立即释放。

当有weak_ptr存在时

通过make_shared建立的shared_ptr:

  • 对象析构函数被调用(资源清理)。 ​
  • 对象内存和控制块内存暂时保留​(直到所有 weak_ptr 也被销毁)。

通过new建立的shared_ptr:

  • 对象内存立即释放(仅保留控制块内存)。

由于make_shared对象和控制块内存是连续的,无法单独释放对象内存。所以必须等待所有 weak_ptr 销毁后,​整块内存才能一起释放。

那这是内存泄漏吗?

不是!​ 内存泄漏的定义是:​无法再访问且未释放的内存。
而在此时:
对象析构函数已被调用(资源已清理)。
内存仍被 weak_ptr 的控制块管理,虽然未释放,但程序仍能通过 weak_ptr 的机制感知到内存状态。
当所有 weak_ptr 销毁后,内存会被正确释放。

enable_shared_from_this 和 shared_from_this()

解决场景:

当一个对象已被 shared_ptr 管理时,若在成员函数中直接用 this 创建新的 shared_ptr,会导致 多个独立的引用计数 。当某一 shared_ptr 析构时,对象可能被提前销毁,引发悬空指针或双重释放。

问题场景示例1

class BackgroundWorker {
public:
    void startWork() {
        // 错误:裸指针 this 跨线程传递,主线程可能提前释放对象
        std::thread([this]() {
            doWork(); // 可能访问已销毁对象!
        }).detach();
    }

    void doWork() { /* 自己实现的操作 */ }
};

int main() {
    auto worker = std::make_shared<BackgroundWorker>();
    worker->startWork();
    worker.reset(); // 主线程释放对象
    // 子线程仍在执行 doWork(),this 已失效!
}

enable_shared_from_this 内部保存一个 weak_ptr,指向与对象关联的 shared_ptr 控制块。通过 shared_from_this() 方法生成与已有 shared_ptr 共享所有权的 shared_ptr 确保引用计数一致

解决示例
通过获取与 shared_ptr 共享所有权的智能指针,延长对象生命周期

class BackgroundWorker : 
public std::enable_shared_from_this<BackgroundWorker> {
public:
    void startWork() {
        // 正确:通过 shared_from_this() 传递所有权
        std::thread([self = shared_from_this()]() {
            self->doWork(); // 安全:self 确保对象存活
        }).detach();
    }
    // ...
};

问题场景示例2:

class TreeNode {
public:
    void addChild() {
        // 创建一个子节点 child,用 shared_ptr 管理
        auto child = std::make_shared<TreeNode>();
        
        // 错误:直接用 this 创建新的 shared_ptr,赋值给 child->parent
        //child->parent引用计数也是 1
        child->parent = std::shared_ptr<TreeNode>(this); // 危险!
        
        // 将 child 添加到当前节点的 children 列表中
        children.push_back(child);
    }

    std::shared_ptr<TreeNode> parent;  // 父节点用 shared_ptr 管理
    std::vector<std::shared_ptr<TreeNode>> children; // 子节点列表
};

int main() {
    // 创建一个根节点 root,用 shared_ptr 管理
    auto root = std::make_shared<TreeNode>();//root引用计数是 1
    root->addChild(); // 调用 addChild,导致双重释放!
}

这段代码的关键问题出现在 child->parent = std::shared_ptr(this)
this 是当前 TreeNode 对象的裸指针(即 root 指向的对象)。
直接用 this 创建了一个新的 shared_ptr,赋值给 child->parent。这个新的 shared_ptr 与 root 是 ​完全独立 的,它不知道 root 的存在,因此它的引用计数 “也是 ​1”。

当 main 函数结束时,root 的引用计数减为 ​0,会释放它指向的 TreeNode 对象。

但此时 child->parent 也指向同一个 TreeNode 对象,且它的引用计数仍然是 ​1​(如果 child 未被销毁)。当 child 析构时,child->parent 的引用计数减为 ​0,会再次尝试释放同一个 TreeNode 对象,导致** ​双重释放**。

解决示例:

class TreeNode : public std::enable_shared_from_this<TreeNode> {
public:
    void addChild() {
        auto child = std::make_shared<TreeNode>();
        
        // 通过 shared_from_this() 获取与 root 共享所有权的 shared_ptr
        child->parent = shared_from_this(); // 引用计数 +1
        
        children.push_back(child);
    }

    std::shared_ptr<TreeNode> parent;
    std::vector<std::shared_ptr<TreeNode>> children;
};

int main() {
    auto root = std::make_shared<TreeNode>();
    root->addChild(); // 安全:root 的引用计数变为 2
}

shared_ptr的线程安全问题

陈硕大佬的博客有超详细介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值