常见的智能指针

2023-11-09

智能指针其实是一个类模板,它与普通指针的区别在于他会自己释放内存空间。常见的智能指针有三种:unique_ptr、shared_ptr、weak_ptr。他们都在头文件<memory>中。

注:下面的普通指针就是裸指针。

目录

unique_ptr:

        unique_ptr的创建:

        unique_ptr的常用函数:

        unique_ptr可能犯的错:

shared_ptr:

        shared_ptr的创建:

        shared_ptr的常用函数:

        shared_ptr可能犯的错:

weak_ptr:

        weak_ptr的创建:

        weak_ptr的常用函数:


unique_ptr:

        unique_ptr的创建:

        unique_ptr是一种独占形的指针,即不能有两个独占性指针指向同一内存。它不允许复制,只允许移动(move),常用有三种创建方法,没有直接创建指针数组的,但可以自己自定义数据类型。

	//第一种
	unique_ptr<int> ptr1(new int(2));   //new一个内存空间给他
	cout << ptr1 << "  " << *ptr1 << endl;
	//第二种
	//unique_ptr<int> ptr2(ptr1);  //报错,不可直接拷贝(浅拷贝)
	unique_ptr<int> ptr2(move(ptr1));     //通过move将ptr1的内存给ptr1.
	cout << ptr2 << "  " << *ptr2 << endl;
	cout << ptr1 << endl;       //此时会发现其指向的地址已经被置空了
	//cout << *ptr1 << endl;   //不可再调用*ptr1,因为它指向的内存已经移动给ptr2了,会报异常。
	//第三种
	unique_ptr<int> ptr3;
	ptr3 = make_unique<int>(3);   //调用make_unique函数。
	cout << ptr3 << "  " << *ptr3 << endl;

    //第四种,创建一个指针数组
//自定义数据类型
	class shuzu {
	private:
		int* p;
		int size;
	public:
		shuzu() = default;          //显式构造构造函数
		shuzu(int k) {              //记得重写构造函数创建空间
			this->size = k;
			this->p = new int[k];
		}
		~shuzu() {                  //析构函数也要记得
			this->size = 0;
			delete[] p;
			p = nullptr;             //要置空,防止野指针的出现
		}
		int get_p() {
			if (p) { return 0; }
			else { return p[0]; }
		}
	};
//创建指针数组
	unique_ptr<shuzu> ptr4(new shuzu(4));
	cout << ptr4 << "  " << ptr4->get_p() << endl;

        unique_ptr的常用函数:

        unique_ptr有四个常用函数,一个是release,一个是reset,一个是get,还有一个swap。release可以将该指针置为空,然后返回一个普通指针指向该内存。reset有一个普通指针的参数,默认为空,会先销毁该指针指向的地址,再让该指针指向参数所指向地址。get会返回一个普通指针,可以让该指针与智能指针指向同一个地址。swap的参数是另一个unique_ptr,会交换两个指针所指向的地址。

	unique_ptr<int> ptr3;
	ptr3 = make_unique<int>(3);
	cout << "ptr3 = " << ptr3 << "  *ptr3 = " << *ptr3 << endl;
	unique_ptr<int> ptr1(new int(2));
	cout << "ptr1 = " << ptr1 << "  *ptr1 = " << *ptr1 << endl;

    int* p = ptr1.get();      //获取ptr1的地址
	cout << "p= " << p<< "  *p= " << *p<< endl;
	cout << "ptr1 = " << ptr1 << "  *ptr1 = " << *ptr1 << endl;

	ptr1.swap(ptr3);      //交换两指针指向地址
	cout << "ptr3 = " << ptr3 << "  *ptr3 = " << *ptr3 << endl;
	cout << "ptr1 = " << ptr1 << "  *ptr1 = " << *ptr1 << endl;

	int* ptr5 = ptr3.release();  //因为智能指针是unique_ptr<int>,所以返回一个int*的指针
	cout << "ptr5 = " << ptr5 << "  *ptr5 = " << *ptr5 << endl;

    //ptr3.release();    //将ptr3置空并释放内存

	ptr1.reset(ptr5);     //因为智能指针是unique_ptr<int>,所以参数是int*的指针
	cout << "ptr5 = " << ptr3 << endl;
	cout << "ptr1 = " << ptr1 << "  *ptr1 = " << *ptr1 << endl;

    ptr1.reset();     //释放ptr1指向的内存并置空

        unique_ptr可能犯的错:

我们都知道,unique_ptr是独占类型的指针,但如果我们通过一个指针给两个unique_ptr类型的指针赋初值是可以成功的,而且这两个指针还指向同一地址,这就会导致unique_ptr变为非独占型,间接导致unique_ptr的reset操作异常以及其他依赖于该指针独占性的操作出问题。所以不能做以下操作:

int main() {

	//成功创建指针并指向同一地址
	int* trap = new int(4);
	unique_ptr<int> ptr1(trap);
	unique_ptr<int> ptr2(trap);
	unique_ptr<int> ptr3(trap);
	cout << "trap的地址为:" << trap << endl;
	cout << "ptr1的地址为:" << ptr1 << endl;
	cout << "ptr2的地址为:" << ptr2 << endl;
	cout << "ptr3的地址为:" << ptr3 << endl;
	//调用函数
	ptr1.release();  //可以成功,但只是将ptr1这个指针置空
	//ptr2.reset();  //无法成功,因为该内存不只被ptr2所拥有

	return 0;  //在执行完return 0 后会报错,因为无法销毁非独占的独占型指针。
}

shared_ptr:

        shared_ptr的创建:

        shared_ptr是一种共享性的智能指针(类模板),它允许移动和复制,每复制一次后,它的计数就会加一,当计数为0时就会销毁。常用的创建方法有:①使用new创建(不推荐,因为使用new会导致重复了被创建对象的键入,导致编译次数增加,代码膨胀,最终难以维护。)②使用make_shared,用法同make_unique。③直接复制另一个shared_ptr,会让这两个指针指向同一块内存。④使用move将一个shared_ptr指向的内存移动到另一个shared_ptr,被移动的指针会被置空。

    //用法一,不推荐
	shared_ptr<int> ptr2(new int(10));
	cout << "ptr2的地址为:" << ptr2 << "  ptr2的值为:" << *ptr2 << endl;
	//用法二,使用make_shared,推荐
	shared_ptr<int> ptr1(make_shared<int>(10));
	cout << "ptr1的地址为:" << ptr1 << "  ptr1的值为:" << *ptr1 << endl;
	//直接复制
	shared_ptr<int> ptr3 = ptr1;
	cout << "ptr3的地址为:" << ptr3 << "  ptr3的值为:" << *ptr3 << endl;
	//将ptr1所指向的内存移动给ptr4
	shared_ptr<int> ptr4(move(ptr1));
	cout << "ptr4的地址为:" << ptr4 << "  ptr4的值为:" << *ptr4 << endl;
	//ptr1被移动,被置空了
	cout << "ptr1的地址为:" << ptr1 << endl;
	//ptr3不变
	cout << "ptr3的地址为:" << ptr3 << "  ptr3的值为:" << *ptr3 << endl;

        shared_ptr的常用函数:

        shared_ptr有几个常用函数:use_count、get、reset、unique、swap,其中use_count函数可以查看与该指针指向同一内存的智能指针数(也就是计数)。get同上,可以获取地址。reset也有两个使用方法,若没有参数,则会将该指针置空,然后计数减一,若计数为0,则销毁该内存;若存在参数,则参数是一个新开辟出的内存地址(不能是裸指针),先将计数减一,若计数为0,则销毁该内存,再将指针指向该地址。unique函数返回一个bool值,若该指针计数为1,则返回true,否则返回false。swap函数同unique_ptr的一样,都是交换两个指针指向的地址。

	shared_ptr<int> ptr1(make_shared<int>(10));
	cout << "ptr1的地址为:" << ptr1 << "ptr1的计数为:"<< ptr1.use_count() << "  ptr1的值为:" << *ptr1 << endl;
	//此时计数为1,unique返回true
	cout << ptr1.unique() << endl;
	shared_ptr<int> ptr3 = ptr1;
	cout << "ptr3的地址为:" << ptr3 << "ptr3的计数为:" << ptr3.use_count() << "  ptr3的值为:" << *ptr3 << endl;
	//此时计数为2,unique返回fasle
	cout << ptr1.unique() << endl;

	//使用get获取地址
	int* p = ptr1.get();

	//reset无参数,ptr1指向的地址会被置空,计数-1.
	ptr1.reset();
	cout << "ptr1的地址为:" << ptr1 << "ptr1的计数为:" << ptr1.use_count() << endl;
	//reset有参数,地址会指向新开辟出来的,值为4.
	ptr1.reset(new int(4));
	cout << "ptr1的地址为:" << ptr1 << "ptr1的计数为:" << ptr1.use_count() << "  ptr1的值为:" << *ptr1 << endl;

    //交换两指针
	ptr1.swap(ptr3);
	cout << "ptr1的地址为:" << ptr1 << "ptr1的计数为:" << ptr1.use_count() << ptr1 << "  ptr1的值为:" << *ptr1 << endl;
	cout << "ptr3的地址为:" << ptr3 << "ptr3的计数为:" << ptr3.use_count() << "  ptr3的值为:" << *ptr3 << endl;

        shared_ptr可能犯的错:

        一:同unique_ptr一样,shared_ptr也不能用一个裸指针创建两个shared_ptr,因为你在创建的时候机器会用同一个地址创建两个指针,他们因为刚被创建,计数都是一,等到删除的时候,比如删掉第一个,计数为0,机器以为这块内存无人使用了,想将他销毁,可第二个指针还在使用这块内存,造成错误的出现。

int main() {
	int* p = new int(5);

	shared_ptr<int> ptr1(p);
	shared_ptr<int> ptr2(p);
	cout << "ptr1的地址为:" << ptr1 << "  ptr1的计数为:" << ptr1.use_count() << "  ptr1的值为:" << *ptr1 << endl;
    //系统以为ptr2指向的内存刚被创建,所以ptr2的计数也是1,但实际上是2.
	cout << "ptr2的地址为:" << ptr2 << "  ptr1的计数为:" << ptr2.use_count() << "  ptr2的值为:" << *ptr2 << endl;

	return 0;  //在执行完return 0 后会报错,因为无法销毁指针。
}

        二:shared_ptr的循环引用问题,本质是因为使用的shared_ptr的参数类型是一个带有shared_ptr的成员函数,而且这两个指向同一地址。如下方的a与a->a_这两个shared_ptr指向同一地址,当程序结束时,a销毁,a的use_count减一变为1,所以a所指向的内存不销毁,而该内存不销毁,a->a_就不会销毁(不调用析构函数),其use_count不会为0,该内存一直存在。

而使用weak_ptr,并不会让a的计数加一,所以a销毁时,计数为0,a.a_也会被成功销毁。

class A
{
public:
    std::shared_ptr<A> a_;
    //std::weak_ptr<int> a;  //这个改成weak_ptr就没问题了
public:
    A() {
        std::cout << "construct A" << std::endl;
    }
    ~A() {
        std::cout << "destroy A" << std::endl;
    }
};


int main() {

    // *** circular reference *** ///
    std::shared_ptr<A> a(new A()); // a count: 1
    cout <<"a的地址为:" << a << " a的计数为:" << a.use_count() << endl;

    a->a_ = a; // b count: 2
    cout << "a->a_的地址为:" << a->a_ << " a->a_的计数为:" << a->a_.use_count() << endl;
    cout << "a的地址为:" << a << " a的计数为:" << a.use_count() << endl;

	return 0;  
}

weak_ptr:

        weak_ptr的创建:

        weak_ptr一般不单独使用,它并未重载诸如 *、->等指针操作符,而是作为shared_ptr的补充(监视)。它的初始化需要用到shared_ptr的指针,当使用一个shared_ptr初始化weak_ptr时,该shared_ptr的计数并不会加一!!!!!所以他可以用来解决上面shared_ptr可能出现的第二个问题。

	//创建
	weak_ptr<int> wptr1;
	//初始化
	shared_ptr<int> ptr1(new int(3));
	wptr1 = ptr1;
	cout << "ptr1的地址为:" << ptr1 << "  ptr1的计数为:" << ptr1.use_count() << endl;  //计数为1,不是2哦
	//可以通过复制创建
	weak_ptr<int> wptr2(wptr1);

        weak_ptr的常用函数:

        weak_ptr有几个常用函数:lock、reset、use_count、swap、expired。lock函数会返回weak_ptr所绑定的shared_ptr对象,不推荐用来初始化新的shared_ptr,若无绑定对象则返回NULL。reset会给weak_ptr“解绑”,即该指针指向空(不影响它所绑定的shared_ptr)。use_count可以用来获取它绑定对象的计数值。swap用来交换两个指针绑定的对象。expired会返回一个bool值,若weak_ptr为空或者其绑定的shared_ptr为空,则返回true(为空返回true哦)。

	weak_ptr<int> wptr1;
	shared_ptr<int> ptr1(new int(3));
	wptr1 = ptr1;
	cout << "ptr1的地址为:" << ptr1 << endl;  //计数为1,不是2哦
	shared_ptr<int> ptr2(new int(5));
	weak_ptr<int> wptr2(ptr2);
	cout << "ptr2的地址为:" << ptr2 << endl; 

	//交换wptr1与wptr2的绑定对象。
	wptr1.swap(wptr2);
	cout << wptr1.lock() << endl;  //返回ptr2的地址。
	cout << wptr2.lock() << endl;  //返回ptr1的地址。

	//未解绑前
	cout << wptr1.lock() << endl;  //返回ptr2的地址(一个指向ptr2的shared_ptr<int>的指针)。
	cout << wptr1.expired() << endl;  //ptr2不为空,返回false
	cout << wptr1.use_count() << endl;  //计数为1

	//解绑
	wptr1.reset();
	cout << wptr1.lock() << endl;  //被解绑了,返回空
	cout << wptr1.expired() << endl;  //被解绑了,返回true
	cout << wptr1.use_count() << endl;  //计数为0,因为被解绑了

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

常见的智能指针 的相关文章

  • 使用 TCP 套接字在本地代理视频

    我一直对向媒体浏览器添加对视频播客的支持非常感兴趣 我希望用户能够浏览可用的视频播客并从互联网上流式传输它们 这真的很容易 因为媒体播放器等将愉快地播放存在于云中的文件 问题是我想在本地缓存这些文件 因此同一集的后续观看将不涉及流式传输 而
  • 如何在 Caliburn.Micro 中使用 Conductor 的依赖注入

    我有时用Caliburn Micro http caliburnmicro com创建应用程序 使用最简单的 BootStrapper 我可以像这样使用 IoC 容器 SimpleContainer private SimpleContai
  • 将 ARGB 拆分为字节值

    我有一个 ARGB 值存储为 int 类型 它是通过调用 ToArgb 来存储的 我现在想要来自 int 值的各个颜色通道的字节值 例如 int mycolor 16744448 byte r g b a GetBytesFromColor
  • 等待运算符错误

    我的代码有问题 我怎么解决这个问题 这个问题出现在await操作符中 public MyModel HttpClient client new HttpClient HttpResponseMessage response await cl
  • 为什么 fgets 接受 int 而不是 size_t?

    功能如strcpy malloc strlen 和其他各种接受他们的参数或返回值作为size t代替int or an unsigned int出于显而易见的原因 一些文件功能 例如fread and fwrite use size t以及
  • 值类型如何实现引用类型

    我遇到了一个值类型正在实现 ref 的场景 类型 只是想知道这怎么可能 幕后发生了什么 结构体是值类型 接口是引用 类型但结构可以实现接口而不会出现任何错误 有什么想法吗 提前致谢 实际上 它同时以两种不同的方式进行 首先 任何值类型都可以
  • 带有嵌入 Flash 视频的 PDF 示例?

    有谁知道我在哪里可以查看嵌入 Flash 视频的 PDF 示例 我知道问这个问题很愚蠢 因为你会认为任何面向技术的用户都应该能够使用谷歌找到一个 但我真的找不到 我的另一个问题是 使用 C 中的 API 将 Flash 视频嵌入 PDF 文
  • UI 线程正在阻塞调用 COM 对象的后台线程

    我正在开发一个通过第三方 COM 库与外部设备通信的应用程序 我试图让与设备的所有通信都通过后台线程 以防止通信问题搞砸我的应用程序 并消除在 UI 线程中进行通信所引入的一些其他复杂性 问题是 每当发生导致主 UI 线程阻塞的情况 即调用
  • 我如何知道向量的实际最大大小? (不使用 std::vector::max_size)

    在在线课程中 我正在学习向量 在其中一个例子中 他们解释说 std vector max size 应该给我向量可以达到的最大大小 我决定测试一下 include
  • 将 std::pair const 转换为 std::pair const 安全吗?

    理论上或实践上 安全吗reinterpret cast a std pair
  • Web浏览器控件:如何捕获文档事件?

    我正在使用 WPF 的 WebBrowser 控件加载一个简单的网页 在这个页面上我有一个锚点或一个按钮 我想在我的应用程序后面的代码中 即在 C 中 捕获该按钮的单击事件 WebBrowser 控件是否有办法捕获加载页面元素上的单击事件
  • _MM_TRANSPOSE4_PS 在 GCC 中导致编译器错误?

    我第一次在 GCC 而不是 MSVC 中编译我的数学库 并经历了所有的小错误 我遇到了一个根本没有意义的错误 Line 284 error lvalue required as left operand of assignment 284号
  • Cookie 在 ASP.net 中失去价值

    我有以下设置 cookie 的代码 string locale DropDownList this LoginUser FindControl locale SelectedValue HttpCookie cookie new HttpC
  • 将旧的 Unity 代码升级到 Unity 5

    在触发按钮上播放动画的代码似乎不起作用 我在 Youtube 上看到了一个视频 内容很简单animation Play 它可以在该视频上运行 但我无法让它在我的计算机上运行 我做错了什么还是团结改变了它 请帮助我在网上找不到解决方案 所有
  • 如何将输出重定向到 boost 日志?

    我有一个使用boost log的C 程序 我加载了用户提供的动态链接库 我想将 stderr 重定向到 boost 日志 以便用户的库随时执行以下操作 std cerr lt lt Some stuff 它产生相同的结果 BOOST LOG
  • C中使用JNI从对象获取对象

    public class Student private People people private Result result private int amount 这是 Java 中类的示例 在C中 我试图获取 学生 中的 人 但失败了
  • 在链表程序中使用模板时重载 C++ 中的 << 运算符

    我正在尝试实现一个链接列表 但是当我尝试重载 include
  • Membership.ValidateUser() 的目的是什么

    我一直在学习有关MembershipProvider类 我认为Membership ValidateUser 方法应该用于登录用户 然而我刚刚了解到有一个FormsAuthentication Authenticate 目的是什么Valid
  • 从其对象获取结构体字段的名称和类型

    例如 我有一个类似这样的结构 struct Test int i float f char ch 10 我有一个该结构的对象 例如 Test obj 现在 我想以编程方式获取字段名称和类型obj 是否可以 顺便说一句 这是 C 你正在要求C
  • 从 C/C++ 程序进行 Ping

    我想编写一个 C 或 C 程序 给定一个 IP 地址 对其进行 Ping 然后根据 Ping 是否成功执行进一步的操作 这个怎么做 尽情享受Ping 页面 http www ping127001 com pingpage htm 其中有一个

随机推荐

  • 聊聊区块链--如何投资数字货币

    想知道更多关于区块链技术知识 请百度 链客区块链技术问答社区 链客 有问必答 未经允许 拒绝转载 https www liankexing com note note page id 7071 html 1 比特币 bitcoin 比特币是
  • Unity Fixed Joint 固定关节组件详解

    Chinar blog www chinar xin Unity物理引擎 Fixed Joint 固定关节 本文提供全流程 中文翻译 Chinar 的初衷是将一种简单的生活方式带给世人 使有限时间 具备无限可能 Chinar 心分享 心创新
  • bash脚本-----在耗时任务中显示旋转器动画

    1 bin bash sleep 5 pid frames while kill 0 pid 2 gt 1 gt dev null do for frame in frames do printf r frame Loading sleep
  • C++课设-学生信息管理系统

    前言 上学期的一个简单的C 课设项目 代码在后面 附github项目链接 一 问题描述 建立学生信息数据 包括学号 姓名 性别 三科成绩 出生时间 年龄 必须计算得到 使用继承的方法构造至少3个类 即学生类 虚基类 一年级学生和二年级学生类
  • 输入一个矩阵,按照从外向里依顺序一次打印

    输入一个矩阵 按照从外向里以顺时针的顺序依次打印出每一个数字 例如 如果输入如下4 X 4矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1 2 3 4 8 12 16 15 14 13
  • Fedora 22安装后无法找到Realtek无线网卡的解决

    在主目录中建立一个文件夹 mkdir Reaktek 接着进入该目录 执行 git clone https github com lwfinger rtlwifi new git 会出现rtlwifi new的文件夹 进入文件夹 执行 ma
  • 编译安装LNMP全程实录

    此次是在CentOs 7 4上进行安装配置 先把编译环境配置好 yum y install gcc automake autoconf libtool make yum install gcc gcc c 准备一些软件的安装包 安装包 地址
  • 线性代数——求逆矩阵

    方法一 行列式分之一乘伴随矩阵 方法二 在右边拼个单位阵做初等行变换使得左边的原矩阵变为单位阵 这时右边即逆矩阵 抽象矩阵求逆 用公式AB E 利用计算技巧凑出公式 两边加E 提取公因式 没有公因式可提时利用隐形的E AA 1 因为E可看作
  • 【Deepin】 Deepin 系统安装教程

    安装过程 准备 准备足够的磁盘空间 下载 格式化 制作启动盘 安装 设置U盘启动项 根据引导安装 新建分区 设置 记录一下第N次安装Deepin系统的过程 准备 准备足够的磁盘空间 deepin用于生活日常的话 不需要太大的空间 我准备了4
  • 理性选择key-value Store

    前言 开源产品固然好 但是各种场景的数据需求确实多少有些差距 利用现有的软硬件资源面对现有的问题快速做出调整是才是数据库工程师的真正价值 综述 key value store由于本身实现不像成熟RDBMS那么复杂 换句话说开发周期不常 性能
  • AbstractQueuedSynchronizer之AQS

    一 是什么 抽象的队列同步器 是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石 通过内置的FIFO队列来完成资源获取线程的排队工作 并通过一个int类型的变量来表示持有锁的状态 官方说法 二 与AQS相关联的知识 1 位
  • el-Popconfirm 气泡确认框修改样式无效。使用popper-class自定义样式

    样式 需要在非scoped区域才会生效 注意需要添加唯一父节点 以免影响其他页面 html结构
  • CardView的具体使用方法

    今天主要是CardView的用法 CardView是在安卓5 0提出的卡片式控件 首先介绍一下它的配置 在gradle文件下添加依赖库 compile com android support cardview v7 22 2 1 其次介绍一
  • SQL SERVER中求上月、本月和下月的第一天和最后一天[转]

    上月的第一天 SELECT CONVERT CHAR 10 DATEADD month 1 DATEADD dd DAY GETDATE 1 GETDATE 111 SELECT DATEADD mm DATEDIFF mm 0 datea
  • 【SpringMVC】DispatcherServlet重要组件之一HandlerAdapter

    作用使用处理器干活的 共三个方法 package org springframework web servlet import javax servlet http HttpServletRequest import javax servl
  • PTA程序设计类实验辅助教学平台-基础编程题--JAVA--7.3 逆序的三位数

    import java util Scanner public class Main public static void main String args Scanner sc new Scanner System in int i sc
  • C二级考试第一套题

    一 1 题目 2 程序 考察范围 1 结构体的定义 关键字使用 2 链表节点的相关操作 3 malloc的应用 include
  • PyQt6 和 PyQt5 的差异

    PyQt6 和 PyQt5 的差异 PyQt6 是 PyQt5 的下一个版本 但两个版本的写法基本上其实大同小异 这篇教学会介绍 PyQt6 和 PyQt5 有何差异 快速预览 exec 改为 exec 方法的位置或名称改变 不需要高 DP
  • 行为型模式 - 迭代器模式iterator

    模式的定义与特点 迭代器模式 iterator Pattern 为的提是可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象 多个对象聚在一起形成的总体称之为聚集 聚集对象是能够包容一组对象的容器对象 迭代子模式将迭代逻辑封装到一个独立的子
  • 常见的智能指针

    智能指针其实是一个类模板 它与普通指针的区别在于他会自己释放内存空间 常见的智能指针有三种 unique ptr shared ptr weak ptr 他们都在头文件