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

shared_ptr创建方式以及循环引用问题

概念

shared_ptr 是 C++ 标准库提供的智能指针,用于管理动态分配的对象,通过引用计数实现资源的自动释放。它是现代 C++ 中避免内存泄漏的核心工具之一。

核心特点

  • 引用计数:多个 shared_ptr 可共享同一对象,每复制 / 赋值一次计数 + 1,销毁时计数 - 1,计数为 0 时自动释放资源。
  • 线程安全:引用计数的操作是原子的(但对象访问需自行同步)。
  • 自定义删除器:支持自定义资源释放逻辑(如关闭文件、释放句柄等)。

创建方式

方式语法示例特点
std::make_sharedauto ptr = std::make_shared<int>(42);推荐,一次内存分配,效率高;无法自定义删除器。
构造函数std::shared_ptr<int> ptr(new int(42));两次内存分配;支持自定义删除器(如 [](int* p) { delete p; })。
从 unique_ptr 转移std::unique_ptr<int> uptr(new int(42));
std::shared_ptr<int> sptr = std::move(uptr);
unique_ptr 放弃所有权,转为 shared_ptr 管理。
从原始指针转换int* raw = new int(42);
std::shared_ptr<int> sptr(raw);
不推荐,易导致多次释放(避免多个 shared_ptr 管理同一

操作

#include <memory>std::shared_ptr<int> ptr = std::make_shared<int>(42);// 获取引用计数
ptr.use_count();  // 返回当前共享对象的智能指针数量// 重置(释放当前对象,可指定新对象)
ptr.reset();              // 释放对象,ptr 为空
ptr.reset(new int(100));  // 释放原对象,指向新对象// 显式获取原始指针(谨慎使用)
int* raw = ptr.get();     // 返回原始指针,不增加引用计数// 判断是否独占对象
if (ptr.unique()) { /* ... */ }  // 等价于 use_count() == 1

make_shared与直接构造对比

特性std::make_shared直接构造 (std::shared_ptr<T>(new T))
内存分配次数1 次(对象 + 引用计数控制块一起分配)2 次(先分配对象,再分配控制块)
内存布局对象和控制块在同一块内存中对象和控制块在不同内存区域
异常安全性高(分配失败时无资源泄漏)需手动处理(可能导致资源泄漏)
性能更快(减少内存分配开销和碎片)略慢
自定义删除器不支持支持(如 [](T* p) { delete p; }
数组支持C++17 起支持(需显式指定数组大小)直接支持(但需用 delete[] 删除器)
构造函数参数完美转发所有参数到对象构造函数需显式创建对象(如 new T(args...)

内存分配次数差异

这是两者最核心的区别,直接影响性能和内存布局。

std::make_shared 的内存分配

auto ptr = std::make_shared<int>(42);  // 仅1次内存分配
  • 步骤
    1. 一次性分配一块足够大的内存,同时存储 对象 和 引用计数控制块
    2. 在这块内存上构造对象,并初始化控制块。
  • 优势:减少内存分配次数,提升缓存局部性(对象和控制块相邻)。

直接构造的内存分配

std::shared_ptr<int> ptr(new int(42));  // 2次内存分配

  • 步骤
    1. 先通过 new 分配对象的内存。
    2. 再分配 shared_ptr 内部的引用计数控制块。
  • 劣势:两次分配可能导致内存碎片,且访问控制块和对象时缓存命中率较低。

异常安全性差异

std::make_shared 的安全性

// 假设 Foo 构造函数可能抛出异常
auto ptr = std::make_shared<Foo>(arg1, arg2);
  • 安全机制:若构造 Foo 时抛出异常,整个内存分配会回滚,不会有资源泄漏。

直接构造的风险

std::shared_ptr<Foo> ptr(new Foo(arg1, arg2));  // 有潜在风险

风险点

  • new Foo(arg1, arg2) 先执行,分配对象内存。
  • 若 Foo 构造函数抛出异常,而 shared_ptr 尚未完全构造,对象内存无法被管理,导致泄漏。

修复方案

Foo* raw = new Foo(arg1, arg2);  // 先分配
std::shared_ptr<Foo> ptr(raw);   // 再传递给 shared_ptr(但仍需手动管理 raw)

构造函数参数

make_shared(完美转发参数到构造函数)

#include <memory>
#include <string>
using namespace std;class Person {
public:Person(string name, int age) : name_(name), age_(age) {}
private:string name_;int age_;
};int main() {// 使用 make_shared,直接传递构造函数需要的参数auto personPtr = make_shared<Person>("张三", 25);//std::shared_ptr<Person> ptr = std::make_shared<Person>("张三", 25);return 0;
}

在 make_shared<Person>("张三", 25) 中,"张三"(右值,string 字面量隐式转换为 string 临时对象 )和 25(右值) 会被完美转发给 Person 的构造函数。make_shared 内部通过模板和 std::forward 机制,保持参数的原始值类别(比如右值特性 ),高效地完成对象构造,不需要手动用 new 显式创建 Person 对象再传入智能指针构造。

直接构造 shared_ptr(需显式创建对象)

#include <memory>
#include <string>
using namespace std;class Person {
public:Person(string name, int age) : name_(name), age_(age) {}
private:string name_;int age_;
};int main() {// 先显式用 new 创建 Person 对象Person* rawPerson = new Person("李四", 30);// 再用创建好的对象指针构造 shared_ptrshared_ptr<Person> personPtr(rawPerson);return 0;
}

这里必须先通过 new Person("李四", 30) 显式创建 Person 对象,得到原始指针 rawPerson,然后再用它构造 shared_ptr。相比于 make_shared,多了手动 new 这一步,而且如果在 new 之后、构造 shared_ptr 之前发生异常,可能导致 rawPerson 指向的内存无法正确管理(引发内存泄漏风险 ),而 make_shared 是一次性完成内存分配和对象构造等逻辑,异常安全性更高。

为什么auto ptr

核心:方便少写代码

区别

//用auto
auto personPtr = make_shared<Person>("张三", 25);
//完整写出
shared_ptr<Person> ptr = std::make_shared<Person>("张三", 25);
  • 写 auto 的话,编译器帮你猜类型,你少写很多字,还不会出错。

  • 额外好处:如果以后改了 std::make_shared 的类型(比如换成 std::shared_ptr<Student>),auto 不用动,编译器自己会更新类型。

循环引用问题

循环引用(Circular Reference)是 std::shared_ptr 使用中最常见的陷阱,指两个或多个对象通过 shared_ptr 互相引用,导致引用计数永远无法归零,造成内存泄漏。

问题示例

#include <memory>
#include<iostream>
class B;  // 前向声明class A {
public:std::shared_ptr<B> b_ptr;  // A 持有 B 的 shared_ptrA() { std::cout << "A created" << std::endl; }~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr;  // B 持有 A 的 shared_ptrB() { std::cout << "B created" << std::endl; }~B() { std::cout << "B destroyed" << std::endl; }
};int main() {auto a = std::make_shared<A>();  // a 的引用计数为 1auto b = std::make_shared<B>();  // b 的引用计数为 1a->b_ptr = b;  // b 的引用计数变为 2b->a_ptr = a;  // a 的引用计数变为 2// main 函数结束时:// a 和 b 的局部变量被销毁,引用计数减为 1(而非 0)// 导致 A 和 B 的析构函数都不会被调用,内存泄漏!
}

问题本质

  • 引用计数的死锁:A 和 B 互相持有对方的 shared_ptr,导致各自的引用计数至少为 1。
  • 内存泄漏:即使 main 函数结束,对象 A 和 B 的内存也无法释放。

解决方案:使用 std::weak_ptr

std::weak_ptr 是一种弱引用,不增加引用计数,用于打破循环引用:

#include <memory>
#include<iostream>
class B;  // 前向声明class A {
public:std::weak_ptr<B> b_ptr;  // A 持有 B 的 shared_ptrA() { std::cout << "A created" << std::endl; }~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::shared_ptr<A> a_ptr;  // B 持有 A 的 shared_ptrB() { std::cout << "B created" << std::endl; }~B() { std::cout << "B destroyed" << std::endl; }
};int main() {auto a = std::make_shared<A>();  // a 的引用计数为 1auto b = std::make_shared<B>();  // b 的引用计数为 1a->b_ptr = b;  // weak_ptr 不增加 b 的引用计数b->a_ptr = a;  // a 的引用计数为 2std::cout << a.use_count() << std::endl;std::cout << b.use_count() << std::endl;// main 结束时:// 1. b 的局部变量销毁,b 的引用计数减为 1(来自 a->b_ptr 是 weak_ptr,不影响计数)// 2. a 的局部变量销毁,a 的引用计数减为 1(来自 b->a_ptr)// 3. 由于 b 的引用计数先变为 0,B 被销毁,b->a_ptr 被释放,a 的引用计数变为 0// 4. A 被销毁,内存正常释放
}

http://www.dtcms.com/a/289450.html

相关文章:

  • MES系列 - MES是提升制造执行效率与透明度的关键系统
  • 单线程 Reactor 模式
  • C++ 继承和多态
  • linux安装Mysql后添加mysql的用户和密码
  • 负的 Content-Length 问题解析与修复方案
  • Claude Code 逆向工程分析,探索最新Agent设计
  • 超参数消融
  • Kafka 在分布式系统中的关键特性与机制深度解析
  • 多任务学习AITM算法简介
  • 虚拟机动态IP配置
  • MongoDB多节点集群原理 -- 复制集
  • 玄机——第六章 流量特征分析-蚂蚁爱上树
  • c语言进阶 自定义类型 (结构体 位段)
  • LWJGL教程(3)——时间
  • 【OD机试】池化资源共享
  • 30天打牢数模基础-K近邻(KNN)讲解
  • `/etc/samba/smb.conf`笔记250719
  • 【1】计算机视觉方法(更新)
  • Spring Boot 自动装配用法
  • Spring AI 聊天记忆
  • InfluxDB 核心概念与发展历程全景解读(一)
  • 定点小数与分数
  • Laravel 框架NOAUTH Authentication required 错误解决方案-优雅草卓伊凡
  • Leetcode 124. 二叉树中的最大路径和
  • 面向对象基础笔记
  • 提升H7-TOOL自制nRF54L15脱机烧写算法文件速度,1MB程序仅需11秒,并且支持了UICR编程
  • C++23中的std::expected:异常处理
  • 以“融合进化 智领未来”之名,金仓Kingbase FlySync:国产数据库技术的突破与创新
  • SpringBoot集成Skywalking链路跟踪
  • CAN通讯理论与实践:调试和优化全讲解