新用户注册入口 老用户登录入口

[转载]c++ 智能指针的问题_窥见C++11智能指针

文章作者:转载 更新时间:2023-02-24 18:25:46 阅读数量:140
文章标签:智能指针C++11内存管理引用计数自动释放循环引用
本文摘要:本文详细介绍了C++11引入的智能指针,特别是shared_ptr、unique_ptr和weak_ptr,它们为程序员提供了自动管理堆内存的有效手段。智能指针通过引用计数等机制,在对象生命周期结束时自动释放内存,有效防止资源泄漏。其中,shared_ptr允许多个智能指针共享同一内存资源,而unique_ptr确保同一时间只有一个所有者,并禁止赋值操作以避免问题;针对循环引用场景,weak_ptr提供弱引用解决方案。此外,文章还探讨了使用智能指针进行动态数组创建以及在多线程环境下的注意事项,强调正确构造智能指针、避免野指针异常的重要性。
转载文章

本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_31467557/article/details/113049179。

该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。

作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。

如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。

导语: C++指针的内存管理相信是大部分C++入门程序员的梦魇,受到Boost的启发,C++11标准推出了智能指针,让我们从指针的内存管理中释放出来,几乎消灭所有new和delete。既然智能指针如此强大,今天我们来一窥智能指针的原理以及在多线程操作中需要注意的细节。

智能指针的由来

在远古时代,C++发明了指针这把双刃剑,既可以让程序员精确地控制堆上每一块内存,也让程序更容易发生crash,大大增加了使用指针的技术门槛。因此,从C++98开始便推出了auto_ptr,对裸指针进行封装,让程序员无需手动释放指针指向的内存区域,在auto_ptr生命周期结束时自动释放,然而,由于auto_ptr在转移指针所有权后会产生野指针,导致程序运行时crash,如下面示例代码所示:

auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1; //转移控制权
*p1 += 10; //crash,p1为空指针,可以用p1->get判空做保护

因此在C++11又推出了unique_ptr、shared_ptr、weak_ptr三种智能指针,慢慢取代auto_ptr。

unique_ptr的使用

unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。

unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针

除了上述用法,unique_ptr还支持创建动态数组。在C++中,创建数组有很多方法,如下所示:

// 静态数组,在编译时决定了数组大小
int arr[10];// 通过指针创建在堆上的数组,可在运行时动态指定数组大小,但需要手动释放内存
int *arr = new int[10];// 通过std::vector容器创建动态数组,无需手动释放数组内存
vector<int> arr(10);// 通过unique_ptr创建动态数组,也无需手动释放数组内存,比vector更轻量化
unique_ptr<int[]> arr(new int[10]);

这里需要注意的是,不管vector还是unique_ptr,虽然可以帮我们自动释放数组内存,但如果数组的元素是复杂数据类型时,我们还需要在其析构函数中正确释放内存。

真正的智能指针:shared_ptr

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数(感觉有参考Objective-C的嫌疑),实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。


另外,使用shared_ptr过程中有几点需要注意:

  • 构造shared_ptr的方法,如下示例代码所示,我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。
// 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值
int *p = new int(10);
shared_ptr<int>sp = p;
delete p; // sp将成为野指针,使用sp将crash
// 将裸指针作为匿名指针传入构造函数,一般做法,让shared_ptr接管裸指针的生命周期,更安全
shared_ptr<int>sp1(new int(10));
// 使用make_shared,推荐做法,更符合工厂模式,可以连代码中的所有new,更高效;方法的参数是用来初始化模板类
shared_ptr<int>sp2 = make_shared<int>(10);
  • 禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。
shared_ptr<int>sp = make_shared<int>(10);
cout << sp.use_count() << endl; //输出1
shared_ptr<int> *sp1 = &sp;
cout << (*sp1).use_count() << endl; //输出依然是1
(*sp1).reset(); //sp成为野指针
cout << *sp << endl; //crash
  • 使用shared_ptr创建动态数组,在介绍unique_ptr时我们就讲过创建动态数组,而shared_ptr同样可以做到,不过稍微复杂一点,如下代码所示,除了要显示指定析构方法外(因为默认是T的析构函数,不是T[]),另外对外的数据类型依然是shared_ptr<T>,非常有迷惑性,看不出来是数组,最后不能直接使用下标读写数组,要先get()获取裸指针才可以使用下标。所以,不推荐使用shared_ptr来创建动态数组,尽量使用unique_ptr,这可是unique_ptr为数不多的优势了。
template <typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}shared_ptr<int>sp = make_shared_array(10); //看上去是shared<int>类型,实际上是数组
sp.get()[0] = 100; //不能直接使用下标读写数组元素,需要通过get()方法获取裸指针后再操作
  • 用shared_ptr实现多态,在我们使用裸指针时,实现多态就免不了定义虚函数,那么用shared_ptr时也不例外,不过有一处是可以省下的,就是析构函数我们不需要定义为虚函数了,如下面代码所示:
class A {
public:
~A() {
cout << "dealloc A" << endl;
}
};
class B : public A {
public:
~B() {
cout << "dealloc B" << endl;
}
};
int main(int argc, const char * argv[]) {
A *a = new B();
delete a; //只打印dealloc A
shared_ptr<A>spa = make_shared<B>(); //析构spa是会先打印dealloc B,再打印dealloc A
return 0;
}
  • 循环引用,笔者最先接触引用计数的语言就是Objective-C,而OC中最常出现的内存问题就是循环引用,如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr,具体可见下一章。
class A {
public:
shared_ptr<B> b;
};
class B {
public:
shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
shared_ptr<A> spa = make_shared<A>();
shared_ptr<B> spb = make_shared<B>();
spa->b = spb;
spb->a = spa;
return 0;
} //main函数退出后,spa和spb强引用计数依然为1,无法释放

刚柔并济:weak_ptr

正如上一章提到,使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用,上一章最后的代码使用weak_ptr可改造为如下代码。

class A {
public:
shared_ptr<B> b;
};
class B {
public:
weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
shared_ptr<A> spa = make_shared<A>();
shared_ptr<B> spb = make_shared<B>();
spa->b = spb; //spb强引用计数为2,弱引用计数为1
spb->a = spa; //spa强引用计数为1,弱引用计数为2
return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了
使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,这就和OC的Strong-Weak Dance有点像了,具体操作如下:
shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
*spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20

智能指针原理

看到这里,智能指针的用法基本介绍完了,后面笔者来粗浅地分析一下为什么智能指针可以有效帮我们管理裸指针的生命周期。

使用栈对象管理堆对象

在C++中,内存会分为三部分,堆、栈和静态存储区,静态存储区会存放全局变量和静态变量,在程序加载时就初始化,而堆是由程序员自行分配,自行释放的,例如我们使用裸指针分配的内存;而最后栈是系统帮我们分配的,所以也会帮我们自动回收。因此,智能指针就是利用这一性质,通过一个栈上的对象(shared_ptr或unique_ptr)来管理一个堆上的对象(裸指针),在shared_ptr或unique_ptr的析构函数中判断当前裸指针的引用计数情况来决定是否释放裸指针。

shared_ptr引用计数的原理

一开始笔者以为引用计数是放在shared_ptr这个模板类中,但是细想了一下,如果这样将shared_ptr赋值给另一个shared_ptr时,是怎么做到两个shared_ptr的引用计数同时加1呢,让等号两边的shared_ptr中的引用计数同时加1?不对,如果还有第二个shared_ptr再赋值给第三个shared_ptr那怎么办呢?或许通过下面的类图便清楚个中奥秘。

50db612cc9f026ed05e9fa0f4aec1444.png

[ boost中shared_ptr与weak_ptr类图 ]

我们重点关注shared_ptr<T>的类图,它就是我们可以直接操作的类,这里面包含裸指针T*,还有一个shared_count的对象,而shared_count对象还不是最终的引用计数,它只是包含了一个指向sp_counted_base的指针,这应该就是真正存放引用计数的地方,包括强应用计数和弱引用计数,而且shared_count中包含的是sp_counted_base的指针,不是对象,这也就意味着假如shared_ptr<T> a = b,那么a和b底层pi_指针指向的是同一个sp_counted_base对象,这就很容易做到多个shared_ptr的引用计数永远保持一致了。

多线程安全

本章所说的线程安全有两种情况:

多个线程操作多个不同的shared_ptr对象

C++11中声明了shared_ptr的计数操作具有原子性,不管是赋值导致计数增加还是释放导致计数减少,都是原子性的,这个可以参考sp_counted_base的源码,因此,基于这个特性,假如有多个shared_ptr共同管理一个裸指针,那么多个线程分别通过不同的shared_ptr进行操作是线程安全的。

多个线程操作同一个shared_ptr对象

同样的道理,既然C++11只负责sp_counted_base的原子性,那么shared_ptr本身就没有保证线程安全了,加入两个线程同时访问同一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很有可能发生野指针访问crash。

作者:腾讯技术工程

https://mp.weixin.qq.com/s?__biz=MjM5ODYwMjI2MA==&mid=2649743462&idx=1&sn=c9d94ddc25449c6a0052dc48392a33c2&utm_source=tuicool&utm_medium=referral​mp.weixin.qq.com

本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_31467557/article/details/113049179。

该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。

作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。

如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。

相关阅读
文章标题:[转载][洛谷P1082]同余方程

更新时间:2023-02-18
[转载][洛谷P1082]同余方程
文章标题:[转载]webpack优化之HappyPack实战

更新时间:2023-08-07
[转载]webpack优化之HappyPack实战
文章标题:[转载]oracle 同时更新多表,在Oracle数据库中同时更新两张表的简单方法

更新时间:2023-09-10
[转载]oracle 同时更新多表,在Oracle数据库中同时更新两张表的简单方法
文章标题:[转载][Unity] 包括场景互动与射击要素的俯视角闯关游戏Demo

更新时间:2024-03-11
[转载][Unity] 包括场景互动与射击要素的俯视角闯关游戏Demo
文章标题:[转载]程序员也分三六九等?等级差异,一个看不起一个!

更新时间:2024-05-10
[转载]程序员也分三六九等?等级差异,一个看不起一个!
文章标题:[转载]海贼王 动漫 全集目录 分章节 精彩打斗剧集

更新时间:2024-01-12
[转载]海贼王 动漫 全集目录 分章节 精彩打斗剧集
名词解释
作为当前文章的名词解释,仅对当前文章有效。
智能指针智能指针是C++中一种封装了原始指针的类,它在对象生命周期结束后能够自动释放所管理的堆内存资源,从而帮助程序员更安全、便捷地进行内存管理。在C++11标准中引入的智能指针主要包括unique_ptr、shared_ptr和weak_ptr三种类型,它们通过不同的引用计数策略来跟踪和控制动态分配的对象。
引用计数引用计数是一种内存管理技术,用于跟踪有多少个智能指针(如C++中的shared_ptr)正在指向特定的堆内存区域。每当有新的智能指针指向该内存时,引用计数加一;当智能指针被销毁或其指向改变时,引用计数减一。当引用计数降为零时,系统会自动释放该内存区域,确保不会发生内存泄漏。
循环引用循环引用是指两个或多个对象互相持有对方的智能指针,形成一个闭环关系,导致引用计数始终大于零,无法达到释放内存的目的。例如,在C++中,如果类A有一个指向类B的shared_ptr成员变量,同时类B也有一个指向类A的shared_ptr成员变量,那么即使程序不再需要这两个对象,由于互相引用,它们的引用计数也不会减少到零,造成内存泄漏。为了解决这个问题,C++引入了weak_ptr,它不增加引用计数,仅提供对对象的弱引用,能够在循环引用场景下避免内存泄漏问题。
延伸阅读
作为当前文章的延伸阅读,仅对当前文章有效。
随着C++17和C++20标准的发布,智能指针的功能和应用场景得到了进一步拓展和完善。在C++17中,std::make_unique函数被正式引入,它与std::make_shared类似,可以安全、高效地创建unique_ptr实例,避免了直接使用new操作符可能导致的问题。此外,C++20对三种智能指针进行了增强,例如shared_ptr现在支持定制删除器和分配器,并且允许通过std::atomic_shared_ptr进行原子操作,增强了其在多线程环境下的安全性。
同时,对于智能指针在容器中的应用也有了更深入的研究和发展。STL容器如vector、list等支持存储智能指针类型元素,使得资源管理更为便捷和可靠。而C++20引入的std::jthread线程类以及相关库函数,则鼓励开发者使用std::shared_ptr来传递线程间共享资源,以防止因资源生命周期管理不当引发的各类问题。
另外,关于智能指针在实际项目开发中的最佳实践,业界持续涌现出大量的实战案例和经验总结。例如,在大型游戏引擎Unreal Engine 4和Unity3D中,智能指针被广泛用于内存管理和对象生命周期控制,这些成功案例为C++开发者提供了宝贵的参考和借鉴。
值得注意的是,尽管智能指针极大程度上简化了内存管理,但过度依赖或错误使用也可能带来额外开销甚至隐藏bug。因此,对于特定场景下是否应采用智能指针,以及选用何种类型的智能指针,开发者需要根据具体需求进行权衡和决策,同时结合现代编译器和运行时优化技术,确保程序性能和安全性的最优化。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
bg [job_number] - 将停止的任务放到后台继续运行。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
基于Redis的分布式锁互斥性与可靠性实现及命名空间与原子性保障 04-22 可自定义刻度动画的jQuery进度条插件 02-07 jQuery和css3网站操作提示向导插件 12-28 jQuery创意响应式两栏滚动幻灯片特效 11-30 带视频播放的全屏轮播图布局特效 09-07 黑色炫酷个人摄影师网站通用模板下载 01-20 Cassandra中哈希分区与范围分区策略:数据分布、Murmur3Partitioner与负载均衡实践 11-17 [转载]java培训后好找工作吗 11-13 响应式环保包装盒设计公司网站静态模板 11-04 本次刷新还10个文章未展示,点击 更多查看。
中文建筑工程公司静态html网站模板下载 07-03 红色大气高端特色餐厅加盟网站模板 06-21 Vue.js 中的数据绑定与取消绑定:事件监听器、$destroy() 方法及 v-model 指令的运用与虚拟DOM、组件销毁的关系解析 06-20 响应式游戏应用商店单页网站html模板 06-15 自考大学通用模板下载 06-13 jqtimeline.js-简单又好用的jquery时间轴插件 06-04 [转载]Java Work 05-26 红色简洁电影售票平台网站html模板 05-02 投资集团项目展示页面网站HTML5模板 03-22 soulmate粉色干净浪漫唯美婚礼单页响应式网站模板 03-07 页面滚动时动态为元素添加class的jQuery插件 03-05
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"