当前位置: 首页 > news >正文

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

文章目录

  • unique_ptr
    • 所有权转移
    • 所有权释放
    • make_unique 和直接(new)unique_ptr
      • 区别一 :需要一次性处理多个资源分配的地方make_unique比unique_ptr更安全
      • 区别二 :直接new支持自定义删除器
  • shared_ptr
    • shared_ptr智能指针指向同一个对象的不同成员
    • 循环引用问题
    • 为什么weak_ptr能解决shared_ptr的循环引用问题
    • make_shared 和 直接 new 一个shared_ptr的区别
      • 区别一:make_shared资源分配更安全
      • 区别二:直接new支持自定义删除器
      • 区别三:内存分配方式不同
      • 为什么make_unique和直接new unique_ptr都是一次分配内存
      • shared_ptr合并分配的缺点
        • shared_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 销毁后,内存会被正确释放。

相关文章:

  • LLaMA Factory微调后的大模型在vLLM框架中对齐对话模版
  • 【LVLMs】LVLMs和OVD结合的一些想法
  • Spring AI Alibaba 对话记忆使用
  • Java基础-26-多态-认识多态
  • 第十九章:Python-pyttsx3 库实现文本转语音功能
  • OpenCV 图形API(5)API参考:数学运算用于执行图像或矩阵加法操作的函数add()
  • mapreduce的工作原理
  • Codeforces Round 1014 (Div. 2)
  • Jetson 设备卸载 OpenCV 4.5.4 并编译安装 OpenCV 4.2.0
  • 电商---part01 项目整体
  • Keil5工程中.uvoptx和.uvprojx后缀名什么意思?
  • 【设计模式】深入解析设计模式:门面模式(外观模式)的定义、优点和代码实现
  • 383. 赎金信
  • 【Git】-- 处理 Git 提交到错误分支的问题
  • 深入理解哈希优化策略与TypeScript实现
  • 【LeetCode Solutions】LeetCode 111 ~ 115 题解
  • 快速构建个人本地知识库管理系统与实现RAG问答
  • JVM面试专题
  • JavaScript 事件流与事件委托
  • VMware Workstation下载,母盘安装,启动的设置,克隆,其他(详细图文)
  • 上海电商公司排名/安卓内核级优化神器
  • 党务公开网站建设汇报/百度导航是哪个国家的
  • 东城网站开发公司/长沙seo培训班
  • 简历上作品展示网站链接怎么做/培训行业seo整站优化
  • 政府网站建设以什么为宗旨/百度竞价收费标准
  • 没有经验可以做网站编辑吗/电商运营方案