c++11 智能指针学习汇总
c++为什么要引入智能指针?
C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生
C#、Java、python和go等语言中都有垃圾自动回收机制,在对象失去引用的时候自动回收,而且基本上没有指针的概念,而C++语言不一样,C++充分信任程序员,让程序员自己去分配和管理堆内存,如果管理的不好,就会很容易的发生内存泄漏问题,而C++11增加了智能指针(Smart Pointer)。主要分为shared_ptr、unique_ptr和weak_ptr三种,使用时需要引用头文件。c++98中还有auto_ptr(自动指针),基本被淘汰了,不推荐使用。而c++11中shared_ptr和weak_ptr都是参考的boost库中实现的。
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。简要的说,智能指针利用了 C++ 的 RAII 机制,在智能指针对象作用域结束后,会自动做内存释放的相关操作,不需要我们再手动去操作内存。
RAII是C++的发明者Bjarne Stroustrup提出的概念,RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
怎么说呢?c++在设计之初过分注重了性能和内存操作的灵活性,而低估了他长期给程序员带来的困扰,就没有设计垃圾回收机制。后来发现这并非趋势,其它语言都有,自己反而显得另类,所以在c++98标准中匆匆引入auto_ptr,使用效果并不好,安全性方面也存在缺陷,为了弥补这个遗憾,首先在boost库smart_ptr(智能指针)组件中引入了
scoped_ptr:只能在本作用域内使用的智能指针
scoped_array:scoped_ptr的数组版本
shared_ptr:最有价值的智能指针。共享指针(早期起名为counted_ptr)
shared_array:shared_ptr的数组版本
weak_ptr:弱指针,与shared_ptr相比,复制不会产生引用计数。lock()返回shared_ptr,并会产生引用计数。
intrusive_ptr:也维护一个引用计数器,比shared_ptr有更好的性能。但是要求T自己提供这个计数器
最后在c++11中参考boost中的智能指针模式,取其精华精简推出智能指针unique_ptr,shared_ptr和weak_ptr,而之前的auto_ptr基本被unique_ptr替代了,基本被抛弃了,虽然还保留着。智能指针的完善不仅为内存管理提供了便利,同时也在性能上做了优化的提升。
c++11智能指针
三种智能指针介绍
unique_ptr:独占指针 实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。可以通过标准库的move()函数实现指针转移
shared_ptr:共享指针 实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享
weak_ptr:弱指针 是一种不控制对象生命周期的智能指针, weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少

三种智能指针原理
多个指针将指向同一个对象。会造成程序试图删除同一个对象多次,造成内存访问异常崩溃的现象。要避免这种问题,方法有多种:1.定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。2.建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。3.创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
shared_ptr:允许多个该智能指针都指向同一块堆分配对象的内存。它是通过引用计数去解决可能出现的删除一个对象多次的问题的。通过引用计数跟踪和记录有多少个shared_ptr共同指向一个对象。例如,赋值时,计数将加1,而指针过期时,计数将减1。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。因为多了个计数对象,所以内存开销变大了,安全好用开销大。
unique_ptr:都是基于所有权的概念去解决可能出现的删除一个对象两次的问题的。他还摒弃了赋值和拷贝构造函数,要求必须显式采用移动语义move转移所有权的方式来避免出现悬空指针,安全性得到了提高,而且unique_ptr的开销跟裸指针一样的,安全好用性能高。
weak_ptr:关联时不会造成对象计数变化,完全是为了弥补shared_ptr天生有缺陷的问题,其实相比于上一代的智能指针auto_ptr来说,shared_ptr可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题,这个指针是个陪跑的角色,通常在特殊场景下使用。
三种智能指针使用的场景
unique_ptr 性能高,没有特殊要求的话可以直接用来取代raw pointer(原始指针),他还支持下标取值哦。
shared_ptr 开销大,在前者不能满足的场景例如需要多个智能指针同时拥有同一个控件的所有权的时候使用。shared_ptr的引用计数本身是安全且无锁的,但shared_ptr中封装的基础对象的读写则不是。
weak_ptr 不单独使用,通常用来配合shared_ptr使用,避免循环引用的问题。
三种智能指针使用实列
unique_ptr
std::unique_ptr 有几个常用函数如下:
void swap (unique_ptr& x)
将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。
void reset()
void reset (ponit p)
没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。
element_type* get()
得到其管理的指针。
long int use_count()
返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。
bool unique()
返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。
element_type& operator\*()
重载指针的 * 运算符,返回管理的指针指向的地址的引用。
element_type* operator->()
重载指针的 -> 运算符,返回管理的指针,可以访问其成员。
explicit operator bool()
返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。
支持下标取值和数组管理
int main()
{
//初始化方式1
std::unique_ptr<int> up1(new int(123));
//初始化方式2
std::unique_ptr<int> up2;
up2.reset(new int(123));
std::unique_ptr<int> up3;
up3 = std::move(up2);
//对于数组的管理,默认带删除器
std::unique_ptr<int[]> up4(new int[10]);
//如果希望自定义删除器,需要自定义删除器类型
std::unique_ptr<int[],void(*)[int*]> up5(new int[10],[](int* p){delete []p;});
}
shared_ptr
std::shared_ptr 有几个常用函数如下:
void swap (unique_ptr& x)
将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。
void reset()
void reset (ponit p)
没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。
element_type* get()
得到其管理的指针。
long int use_count()
返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。
bool unique()
返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。
element_type& operator\*()
重载指针的 * 运算符,返回管理的指针指向的地址的引用。
element_type* operator->()
重载指针的 -> 运算符,返回管理的指针,可以访问其成员。
explicit operator bool()
返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。
#include<iostream> #include<memory> void func(shared_ptr<int>& p) { } void func1(int* p) { } int main() { //初始化方式1 std::shared_ptr<int> sp1(new int(123)); //初始化方式2 std::shared_ptr<int> sp2; sp2.reset(new int(123)); //或者 //sp2 = sp1; //初始化方式3 std::shared_ptr<int> sp3; sp3 = std::make_shared<int>(123); //调用func()函数 func(sp3); func1(sp3.get()); return 0; }
weak_ptr
std::weak_ptr 有几个常用函数如下:
void swap (weak_ptr& x)
将当前 weak_ptr 对象的内容与 x 的内容交换。
void reset()
将当前 weak_ptr 对象管理的指针和计数器变成空的,就像默认构造的一样。
long int use_count()
返回与当前 weak_ptr 对象在同一指针上共享所有权的 shared_ptr 对象的数量。
bool expired()
检查是否过期,返回 weak_ptr 对象管理的指针为空,或者和他所属共享的没有更多 shared_ptr。lock 函数一般需要先调用 expired 判断,如果已经过期,就不能通过 weak_ptr 恢复拥有的 shared_ptr。此函数应返回与(use_count() == 0)相同的值,但是它可能以更有效的方式执行此操作。
shared_ptr<element_type> lock()
#include <iostream> #include <memory> using namespace std; class B; class A { public: shared_ptr<class B> m_spB; }; class B { public: shared_ptr<class A> m_spA; }; //weak reference class WeakB; class WeakA { public: weak_ptr<class WeakB> m_wpB; }; class WeakB { public: weak_ptr<class WeakA> m_wpA; }; void test_loop_ref() { weak_ptr<class A> wp1; { auto pA = make_shared<class A>(); auto pB = make_shared<class B>(); pA->m_spB = pB; pB->m_spA = pA; cout << "pA count: " << pA.use_count() << ", " << "pB count: " << pB.use_count() << endl; wp1 = pA; }//内存泄漏 cout << "wp1 count: " << wp1.use_count() << "\n"; weak_ptr<class WeakA> wp2; { auto pA = make_shared<class WeakA>(); auto pB = make_shared<class WeakB>(); pA->m_wpB = pB; pB->m_wpA = pA; cout << "pA count: " << pA.use_count() << ", " << "pB count: " << pB.use_count() << endl; wp2 = pA; }//无内存泄漏 cout << "wp2 count: " << wp2.use_count() << "\n"; } int main() { //std::weak_ptr 用来避免 std::shared_ptr 的循环引用 test_loop_ref(); return 0; }
auto_ptr和unique_ptr
auto_ptr可以由shared_ptr或unique_ptr替换,具体取决于具体情况
1. auto_ptr支持拷贝构造与赋值操作,但unique_ptr不直接支持,auto_ptr通过拷贝构造或者operator=赋值后,对象所有权转移到新的auto_ptr中去了,原来的auto_ptr对象就不再有效,这点不符合人的直觉。unique_ptr则直接禁止了拷贝构造与赋值操作
无论是auto_ptr还是unique_ptr,都是基于所有权的概念去解决可能出现的删除一个对象两次的问题的。这种情况下会有什么问题呢?先看auto_ptr,看一个小例子:
auto_ptr<string> films[2] = { auto_ptr<string> (new string("Fowl Balls")), auto_ptr<string> (new string("Duck Walks")) }; auto_ptr<string> pwin; pwin = films[0];//注:films[0]对内存失去了所有权 cout << films[0] << endl;//注:会出现错误,因为film[0]指向的内存此时已经不确定了
如上面代码中注释的一样,auto_ptr在智能指针赋值时,原指针会失去对内存的所有权。程序运行时,再次访问访问该内存的话会出错。(当然了,如果你对原指针film1[0]重新赋新值的话是不会有问题的)。
unique_ptr相比auto_ptr更好的点:首先,unique_ptr是在编译的时候,编译器就会发现原指针失去所有权的错误,也就是在编译阶段就报错,而不是像auto_ptr运行的时候出现一些未知的问题。其次,另一个优点是,除了可以使用new和delete之外,unique_ptr还有一个可用于数组的变体,即unique_ptr有使用new[]和delete[]的版本,而auto_ptr和shared_ptr只能使用new和delete。
2. auto_ptr不可做为容器元素,会导致编译错误。虽然unique_ptr同样不能直接做为容器元素,但可以通过move语意实现
3. 标准库提供了一个可以管理动态数组的unique_ptr版本
int main() { unique_ptr<int[]> p(new int[5] {1, 2, 3, 4, 5}); p[0] = 0; // 重载了operator[] }
参考资料:
https://www.cnblogs.com/xumaomao/p/15175448.html
https://blog.csdn.net/phdongou/article/details/117600943
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~