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

【C++】 C++11 智能指针

一、智能指针概述

  • C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。

  • 程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存

  • 使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存

C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用。

二、shared_ptr 共享指针

2.1 shared_ptr 基本原理

  • std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析构的时候,内存才会被释放。

  • shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。

在这里插入图片描述

简单来说,shared_ptr实现包含了两部分:

  1. 一个指针是所管理的数据的地址
  2. 一个指针是控制块的地址
    • 包括引用计数
    • weak_ptr 计数
    • 删除器 (Deleter)
    • 分配器 (Allocator)

因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在 堆上的。

在这里插入图片描述

2.2 shared_ptr 的基本用法

2.2.1 初始化

通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr,代码如下:

void test1(){// 智能指针初始化std::shared_ptr<int> p1(new int(1));std::shared_ptr<int> p2 = p1;std::shared_ptr<int> p3;p3.reset(new int(1));if(p3) {std::cout << "p3 is not null" << std::endl;}
}

在这里插入图片描述

我们应该优先使用make_shared来构造智能指针,因为它更高效。

std::shared_ptr<int> sp = std::make_shared<int>(100);

通过reset()方法,如果没有传入指针,表示将当前管理的指针引用计数减一;如果传入指针,代表减少之前的指针的引用计数,并且管理新的指针,将新的指针的引用计数变为1:

std::shared_ptr<int>sp = std::make_shared<int>(100);
sp.reset();
sp.reset(new int(200));

不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:

std::shared_ptr<int> p = new int(1);

下面是一个使用shared_ptr的例子,使用use_count()观察它引用计数的变化:

void test2()
{std::shared_ptr<int> p1;p1.reset(new int(1));std::shared_ptr<int> p2 = p1;// 引用计数此时应该是2std::cout << "p2.use_count() = " << p2.use_count() << std::endl;p1.reset();std::cout << "p1.reset()" << std::endl;;// 引用计数此时应该是1std::cout << "p2.use_count()= " << p2.use_count() << std::endl;if (!p1){std::cout << "p1 is empty" << std::endl;}if (!p2){std::cout << "p2 is empty" << std::endl;}p2.reset();// 引用计数此时应该是0std::cout << "p2.reset()" << std::endl;std::cout << "p2.use_count()= " << p2.use_count() << std::endl;if (!p2){std::cout << "p2 is empty" << std::endl;}
}

在这里插入图片描述

2.2.2 获取原始指针

当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:

void test3(){std::shared_ptr<int> ptr(new int(1));int *p = ptr.get();delete p; //注意,这里将重复释放内存,导致程序崩溃
}

p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:

  1. 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
  2. 保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
  3. 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

在这里插入图片描述

2.2.3 指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器,下面是一个使用自定义删除器的例子,这个删除器只要是可以可调用对象即可。

void DeleteIntPtr(int *p) {std::cout << "call DeleteIntPtr" << std::endl;delete p;
}struct DeleteIntPtr2{void operator()(int *p) {std::cout << "call DeleteIntPtr2" << std::endl;delete p;}
};void test4(){std::shared_ptr<int> ptr(new int(1), DeleteIntPtr);std::shared_ptr<int> ptr2(new int(1), DeleteIntPtr2());std::shared_ptr<int> ptr3(new int(1), [](int *p){std::cout << "call DeleteIntPtr3" << std::endl;delete p;});
}

在这里插入图片描述

当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象(C++17之前),代码如下所示:

std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});

2.2.4 使用shared_ptr要注意的问题

不要用一个原始指针初始化多个shared_ptr,例如下面错误范例:

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 逻辑错误

不要在函数实参中创建shared_ptr,对于下面的写法:

function(shared_ptr<int>(new int), g()); //有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:

shared_ptr<int> p(new int);
function(p, g());

通过shared_from_this()返回this指针:不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。

class A
{
public:std::shared_ptr<A> GetSelf(){return std::shared_ptr<A>(this); // 不要这么做}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); //此时会多次释放内存
}

在这里插入图片描述

在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

  • 正确返回thisshared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回thisshared_ptr,如下所示:

  • 因为std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法是,会调用内部这个weak_ptrlock()方法,将所观察的shared_ptr返回,避免了重新构造一个shared_ptr

class A : public std::enable_shared_from_this<A>
{
public:std::shared_ptr<A> GetSelf(){return shared_from_this(); // 正确的方式}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); std::cout << "ptr.use_count() = " << ptr.use_count() << std::endl;
}

在这里插入图片描述

避免循环引用。循环引用会导致内存泄漏,比如:


class AA;
class BB;class AA{
public:std::shared_ptr<BB> bb;~AA(){std::cout << "Deconstruction AA" << std::endl;}
};class BB{
public:std::shared_ptr<AA> aa;~BB(){std::cout << "Deconstruction BB" << std::endl;}
};void test6(){std::shared_ptr<AA> aa(new AA());std::shared_ptr<BB> bb(new BB());aa->bb = bb;bb->aa = aa;
}

在这里插入图片描述

循环引用导致ap和bp的引用计数为2,在离开作用域之后,aa和bb的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。

解决的办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节

三、unique_ptr 独占指针

3.1 unique_ptr 基本原理

  • unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr

  • unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是它大小的两倍。

std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 8
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 4

3.2 unique_ptr 的基本使用

3.2.1 unique_ptr 的初始化

unique_ptr不可以复制和引用,同一时间只能被独占:

void test1(){unique_ptr<int> my_ptr(new int(10));unique_ptr<int> my_other_ptr = my_ptr; // 报错,不能复制
}

在这里插入图片描述

unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了:

void test1(){unique_ptr<int> my_ptr(new int(10));// unique_ptr<int> my_other_ptr = my_ptr; // 报错,不能复制unique_ptr<int> my_other_ptr = std::move(my_ptr);  //正确,移动语义cout << *my_other_ptr << endl;
}

在这里插入图片描述

可以使用new的方式初始化unique_ptr,或在C++14之后使用std::make_unique<T>()来构造

std::unique_ptr<int> my_ptr(new int(10));
std::unique_ptr<int> my_ptr2 = std::make_unique<int>(10);

除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别,比如:

  • unique_ptr可以指向一个数组,代码如下所示,在C++17之后引入了对shared_ptr的数组支持
void test2(){std::unique_ptr<int []> ptr(new int[10]);ptr[9] = 9;std::shared_ptr<int []> ptr2(new int[10]); //这个是不合法的,C++17以上支持ptr2[9] = 9;std::cout << "ptr[9] = " << ptr[9] << std::endl;std::cout << "ptr2[9] = " << ptr2[9] << std::endl;
}
  • unique_ptr指定删除器和shared_ptr有区别
void test3(){std::shared_ptr<int> ptr1(new int(1), [](int *p){delete p;}); // 正确std::unique_ptr<int> ptr2(new int(1), [](int *p){delete p;}); // 错误,unique_ptr不能自定义删除器
}

在这里插入图片描述

unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:

void test3(){std::shared_ptr<int> ptr1(new int(1), [](int *p){std::cout << " shared_ptr delete p" << std::endl;delete p;}); // 正确// std::unique_ptr<int> ptr2(new int(1), [](int *p){delete p;}); // 错误,unique_ptr不能自定义删除器std::unique_ptr<int, void(*)(int*)> ptr2(new int(1), [](int *p){ std::cout << " unique_ptr delete p" << std::endl;delete p;}); //正确
}

在这里插入图片描述

四、weak_ptr 弱引用指针

4.1 weak_ptr 原理

  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。

  • weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr

  • weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。

  • weak_ptr还可以返回this指针和解决循环引用的问题

4.2 weak_ptr的基本用法

通过use_count()方法获取当前观察资源的引用计数,如下所示:

void test1(){shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);cout << wp.use_count() << endl; //结果为1,不增加引用计数
}

在这里插入图片描述

通过expired()方法判断所观察资源是否已经释放,如下所示:

void test2(){shared_ptr<int> sp(new int(10));weak_ptr<int> wp(sp);if(wp.expired()){cout << "weak_ptr无效,资源已释放" << std::endl;}else{cout << "weak_ptr有效" << std::endl;}sp.reset();if(wp.expired()){cout << "weak_ptr无效,资源已释放" << std::endl;;}else{cout << "weak_ptr有效" << std::endl;;}
}

在这里插入图片描述

通过lock方法获取监视的shared_ptr,如下所示:

std::weak_ptr<int> gw;
void f()
{if(gw.expired()) {cout << "gw无效,资源已释放" << std::endl;}else {auto spt = gw.lock();cout << "gw有效, *spt = " << *spt << endl;}}
void test3(){{std::shared_ptr<int> sp(new int(10));gw = sp;f();}f();
}

在这里插入图片描述

4.3 weak_ptr返回this指针

上述shared_ptr中,通过继承std:::enable_shared_from_this<>类就可以解决返回this指针的问题,原因就是基类std:enable_shared_from_this<A>中有一个std::weak_ptr观察当前的this指针,构造一个shared_ptr指向它,此时原来的shared_ptr引用计数就会+1,这样就不会多次构造出shared_ptr

class A : public std::enable_shared_from_this<A>
{
public:std::shared_ptr<A> GetSelf(){return shared_from_this(); // 正确的方式}~A(){std::cout << "Deconstruction A" << std::endl;}
};void test5(){std::shared_ptr<A> ptr(new A());std::shared_ptr<A> ptr2 = ptr->GetSelf(); std::cout << "ptr.use_count() = " << ptr.use_count() << std::endl;
}

4.4 weak_ptr解决循环引用问题

在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr,因为weak_ptr不增加引用计数,这样就可以打破引用循环,内存得到正确的释放

class AA;
class BB;class AA{
public:std::weak_ptr<BB> bb; //修改其中一个为weak_ptr~AA(){std::cout << "Deconstruction AA" << std::endl;}
};class BB{
public:std::shared_ptr<AA> aa; ~BB(){std::cout << "Deconstruction BB" << std::endl;}
};void test4(){std::shared_ptr<AA> aa(new AA());std::shared_ptr<BB> bb(new BB());aa->bb = bb;bb->aa = aa;
}

在这里插入图片描述

4.5 weak_ptr使用注意事项

weak_ptr在使用前需要检查合法性。

weak_ptr<int> wp;
{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;
  • 因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。得到了一个容纳NULL指针的sp_null对象
  • 在使用wp前需要调用wp.expired()函数判断一下,因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。
  • 直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己所容纳的那个指针资源的当前状态。

如果shared_ptr sp_ok和weak_ptr wp;属于同一个作用域呢?如下所示:

weak_ptr<int> wp;
shared_ptr<int> sp_ok;
{shared_ptr<int> sp(new int(1)); //sp.use_count()==1wp = sp; //wp不会改变引用计数,所以sp.use_count()==1sp_ok = wp.lock(); //引用计数+1,变成2
} 
//离开作用域,引用计数变为1
if(wp.expired()) { cout << "shared_ptr is destroy" << endl;
}
else {cout << "shared_ptr no destroy" << endl;
}

五、shared_ptr线程安全问题

5.1 使用同一个shared_ptr

引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:

情况1:多线程代码操作的是同一个shared_ptr的对象,此时是不安全的,比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr

std::thread td([&sp1]()){....});

又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用

void fn(shared_ptr<A>&sp) {...
}
...
std::thread td(fn, sp1);

这时候必然不是线程安全的

5.2 使用不同的shared_ptr

这里指的是管理的数据是同一份,而shared_ptr不是同一个对象,比如多线程回调的lambda的是按值捕获的对象。

std::thread td([sp1]()){....});

另个线程传递的shared_ptr是值传递,而非引用:

void fn(shared_ptr<A>sp) {...
}
...
std::thread td(fn, sp1);

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。也就是说,如下操作是安全的。

void fn(shared_ptr<A>sp) {...if(..){sp = other_sp;}else {sp = other_sp2;}
}

需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象A自己去保证。

更多资料:https://github.com/0voice

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

相关文章:

  • AI因子模型视角下的本周五鲍威尔演讲:通胀约束与就业压力的政策博弈
  • Spring Cloud系列—Seata分布式事务解决方案AT模式
  • 2025年6月中国电子学会青少年软件编程(图形化)等级考试试卷(一级)答案 + 解析
  • 编译器错误消息: CS0016: 未能写入输出文件“c:\Windows\Microsoft.NET... 拒绝访问
  • Linux管道
  • NVIDIA 优化框架:Jetson 平台 PyTorch 安装指南
  • 初步学习WPF-Prism
  • 图论\dp 两题
  • GIS相关调研
  • Meta首款AR眼镜Hypernova呼之欲出,苹果/微美全息投入显著抢滩市场新增长点!
  • MyBatis-Plus基础篇详解
  • HashMap工作原理
  • 使用Tomcat Clustering和Redis Session Manager实现Session共享
  • 设备树下的LED驱动实验
  • 【机器人】2025年人形机器人时代:伦理迷雾中的人类界限
  • PAT 1072 Gas Station
  • visionpro获取电脑cpu序列号
  • 生信分析自学攻略 | R语言数据类型和数据结构
  • 矿物分类系统开发笔记(二):模型训练[删除空缺行]
  • leetcode2248. 多个数组求交集
  • ES支持哪些数据类型,和MySQL之间的映射关系是怎么样的?
  • Vue3 学习教程,从入门到精通,vue3综合案例:“豪华版”待办事项(41)
  • [Polly智能维护网络] 网络重试原理 | 弹性策略
  • PyTorch数据处理工具箱(utils.data简介)
  • UE5 PCG 笔记(一)
  • C++ STL(标准模板库)学习
  • 华为鸿蒙系统SSH如何通过私钥连接登录
  • 传统概率信息检索模型:理论基础、演进与局限
  • 短剧小程序系统开发:打造沉浸式短剧观影体验
  • EPM240T100I5N Altera FPGA MAX II CPLD