智能指针是 C++ 中用于管理动态分配的对象生命周期的一种特殊指针。它们提供了自动内存管理和资源释放的机制,避免了手动调用 delete
来释放内存的麻烦和潜在的内存泄漏。
C++ 标准库提供了两种常用的智能指针: std::shared_ptr
和 std::unique_ptr
。
(其实还有 std::weak_ptr
,用于破解循环引用的问题)
std::shared_ptr
:它是一种共享所有权的智能指针。多个std::shared_ptr
对象可以同时拥有同一个对象的所有权。它使用引用计数的方式来跟踪对象的引用次数。当最后一个std::shared_ptr
对象超出作用域或被显式地设置为nullptr
,引用计数变为零时,对象会自动销毁。这样可以确保对象在没有引用时被正确地释放,避免了内存泄漏。std::unique_ptr
:它是一种独占所有权的智能指针。一个std::unique_ptr
对象拥有唯一的所有权,不能被其他智能指针共享。它使用了移动语义,因此具有很高的效率。当std::unique_ptr
对象超出作用域时,或者通过std::move
转移所有权时,对象会自动销毁。这样可以避免资源的重复释放和悬空指针的问题。
智能指针的好处在于它们提供了自动资源管理,减少了手动管理内存的错误和复杂性。此外,它们还能帮助解决资源泄漏、悬空指针和内存安全等问题。
以下是使用智能指针的示例代码:
1 |
|
在这个示例中, std::shared_ptr
和 std::unique_ptr
能够自动管理动态分配的整数对象的生命周期。无需手动释放内存,当 std::shared_ptr
对象超出作用域时,引用计数减少;当 std::unique_ptr
对象超出作用域时,对象自动销毁。
# 底层实现
当我们创建智能指针 shared_ptr
指向一个对象的时候,其构造函数会分配一个空间创建一个管理对象 manager object
,这个管理对象是动态分配的,其包含了一个裸指针,指向我们要管理的对象,还包含了 shared_ptr
的引用计数和 weak_ptr
的引用计数。
一开始,当创建一个 shared_ptr
指向一个新创建的对象,显然,sp 的引用计数为 1,wp 的引用计数为 0,然后裸指针指向对象。
正如我们所经常听说的那样,当 sp 引用计数为 0 时,对象会销毁。这里多了一个不为大家所熟知的小细节,就是此时这个管理对象还不会释放,只有等 wp 也减为 0 的时候,才会释放这个管理对象。
# 关于智能指针的注意事项
在使用智能指针时,有一些注意事项需要注意:
- 避免循环引用:如果使用
std::shared_ptr
,应避免形成循环引用,即两个或多个对象之间相互持有std::shared_ptr
,导致引用计数无法降为零,从而导致内存泄漏。可以通过使用std::weak_ptr
打破循环引用来解决这个问题。 - 不要使用裸指针和智能指针混合使用:尽量避免在代码中同时使用裸指针和智能指针,因为这可能会导致资源管理的混乱。如果需要使用裸指针,可以使用
std::unique_ptr::get()
方法获取裸指针。 - 使用合适的智能指针类型:根据情况选择合适的智能指针类型,例如使用
std::shared_ptr
进行共享所有权的管理,或者使用std::unique_ptr
进行独占所有权的管理。选择适当的智能指针类型可以提高代码的可读性和安全性。 - 只能用智能指针指向用 new 分配的、能够用 delete 释放的对象上
- 尽可能地用
make_shared
去代替 new(但它也不是百利而无一害,见下文:make_shared)
# 智能指针作为函数的入口参数和返回参数
智能指针可以作为函数的参数和返回值类型,从而方便地管理动态分配的资源。
以下是一个示例:
1 |
|
在这个示例中,
processObject
函数接受一个 std::shared_ptr<int>
参数,用于处理动态分配的整数对象。 createObject
函数创建一个动态分配的整数对象,并返回一个 std::unique_ptr<int>
。最后,再回答 make_shared
的作用和对象存放位置的问题。 std::make_shared
是一个模板函数,用于创建智能指针对象,它的作用是在一次内存分配中同时创建对象和管理对象的引用计数。由于对象和引用计数信息存储在一块连续的内存中,因此可以提高性能和内存利用率。
至于对象存放的位置, std::make_shared
创建的对象和引用计数信息通常存储在一块连续的内存块中,这个内存块位于堆上。这样的设计可以减少内存碎片化,并提高内存访问的效率。
# weak_ptr
1、初始化:
wp 的初始化默认是空的,不指向任何东西 —— 甚至不指向一个 manager object。
所以你只能通过从 shared_ptr
或现有的 weak_ptr
进行拷贝构造或复制构造然后将 weak_ptr
指向它:
1 | shared_ptr<Thing> sp(new Thing); |
2、引用
你不能直接用 wp 去引用对象,而是通过一个 sp 来接住 wp.lock()
返回的内容。lock 函数会判断当前 wp 指向的对象是否还存在 / 有效,如果存在,该函数会返回一个该对象的 sp,如果不存在,则返回一个空的 sp:
1 | shared_ptr<Thing> sp2 = wp2.lock(); // get shared_ptr from weak_ptr |
通常,我们有 3 种方式去引用一个 wp:
1 | //第一种方法,通过 sp 和 lock,然后判断 sp 是否为空 |
3、特殊使用场景
wp 除了下文提到的解决循环引用的问题之外,还有一个特殊的使用场景就是获取对象的 this 指针。
当一个对象要通过 shared_ptr
的形式获取一个 this 指针以便实现自动的内存管理时,如果我们仅仅通过拷贝 this 指针来构造 sp 的话,由于 this 指针是个裸指针,于是又会生成一个新的管理对象来指向被管理的对象,于是现在有两个 manager object 了。那么最终就很可能造成内存重复释放的错误:
1 | void Thing::foo() |
为了解决这个问题,需要借助 std::enabled_shared_from_this
来实现,是一个类模板,将 Thing 继承自它,然后再需要用到 this 指针的地方通过调用 shared_from_this()
这个函数来实现。
enabled_shared_from_this
的底层逻辑大概就是在定义类时,类还有一个 wp 的成员变量指向负责管理该类对象的 manager object,然后每次我们调用 shared_from_this
的时候就是拿到 wp 所指向的这个 manager object 所指向的对象 —— 不会多生成一个管理对象。
1 | class Thing : public enable_shared_from_this<Thing> { |
# make_shared
当我们创建一个 sp 指向一个新 new 出来的对象时,一种语法是:
1 | shared_ptr<Thing> p(new Thing); |
在这种情况下,会触发两次内存分配,一次是调用 Thing 的构造函数,分配其内存,还有一次是调用 sp 的构造函数,分配其内存给一个管理对象。而两次内存分配会导致性能 / 效率的降低。
所以,为了优化这个问题,有了 make_shared
这个函数,用来一次分配足够的内存:
1 | shared_ptr<Thing> p(make_shared<Thing>()); // only one allocation! |
在这种情况下,只会发生一次内存分配,效率提高。
这里有个小问题,为什么 make_shared
也需要传入一个模板参数 Thing 呢?是为了灵活性考虑,这样的话 make_shared
生成的对象就可以是其他类型然后 cast 造型给 sp 使用。比如在继承关系中用于父类、子类的指针指向:
1 | shared_ptr<Base> bp(make_shared<Derived1>()); |
最后,如果 Thing 的构造函数需要传递参数,在 make_shared 后面接着加上即可:
1 | shared_ptr<Thing> p (make_shared<Thing>(42, "I'm a Thing!")); |
这里提一嘴,虽然说我们用 make_shared
可以优化效率,但是它也不是就完全没有缺点。根据前面 “底层实现” 提到的内容,如果我们用 make_shared
生成的内容,sp 已经减为 0 了,但是 wp 还大于 0,那么整块内存都不会被释放。
# 循环引用
循环引用是指两个或多个对象相互持有 std::shared_ptr
,导致它们之间的引用计数无法降为零,从而造成内存泄漏。当存在循环引用时,即使不再使用这些对象,它们所占用的内存也无法被释放,从而导致资源泄漏。
下面我们通过一个简单的示例来展示循环引用问题:
1 |
|
在上述示例中,我们定义了两个类 A
和 B
,它们分别包含一个 std::shared_ptr
成员变量用于相互引用。在 main
函数中,我们创建了两个对象 aPtr
和 bPtr
,然后通过相互赋值建立了循环引用。
当程序结束时,这两个对象的析构函数并不会被调用,因为它们之间的循环引用导致引用计数无法降为零。这就造成了内存泄漏,因为这些对象所占用的内存无法被释放。
为了解决循环引用问题,C++ 提供了 std::weak_ptr
。 std::weak_ptr
是一种弱引用,它可以引用 std::shared_ptr
指向的对象,但并不会增加对象的引用计数。通过将其中一个指针使用 std::weak_ptr
来打破循环引用,从而实现对象的正确释放。
1 | //于是,只需要把上面的其中一个指针声明为weak_ptr即可 |
std::weak_ptr
并不能直接打破循环引用,而是通过辅助的机制来解决循环引用的问题。它并不增加引用计数,因此即使存在循环引用,对象的引用计数也会降为零,从而触发析构函数的调用。
在前面的示例中,当 aPtr
和 bPtr
的引用计数降为零时,它们的析构函数会被调用,从而释放它们所占用的内存。由于 bPtr
使用的是 std::weak_ptr
,它并不增加 aPtr
的引用计数,因此循环引用并不会阻止 aPtr
的析构。这样就避免了 std::shared_ptr
循环引用导致的内存泄漏问题。
因此,可以说 std::weak_ptr
并非直接打破循环引用,而是通过协助 std::shared_ptr
来实现循环引用的解决方案。它在不增加引用计数的前提下,提供了一种访问被 std::shared_ptr
管理的对象的方式,并且不会阻止对象的销毁。
# unique_ptr
它和 sp 其实差不多了,区别就是 ownership 的独占,which 通过禁用拷贝构造和赋值构造来实现,所以你不能拷贝或赋值一个 up 给另一个 up:
1 | unique_ptr<Thing> p1 (new Thing); // p1 owns the Thing |
由于禁用拷贝构造,所以如果要把 up 作为函数参数进行传参时,只能 pass by reference,不能 pass by value。
1、所有权转移
而由于 up 对于对象的所有权是独占的,所以有时需要转移所有权,这依赖于移动语义,使用移动构造和移动赋值函数。
其中,我们知道,函数的返回值是右值,所以 up 的这种移动语义可以用于我们在函数中返回一个局部的 up 给外层调用该函数的 up,实现移动:
1 | //create a Thing and return a unique_ptr to it: |
2、所有权的显式转移
通过 std::move()
函数来显式地进行所有权的转移:
1 | unique_ptr<Thing> p1(new Thing); // p1 owns the Thing |
另外,前面在讲 sp 的时候讲到了有个函数 make_shared
,而 C++11 居然没有相对应的 make_unique
,直到 C++14 的时候才引入了 make_unique
函数。不过, make_unique
不会有性能上的提升,因为 up 并不会分配管理对象,所以没有额外的内存分配,不会有两次内存分配,就不会有性能低的问题。—— 可能这就是为什么 C++11 引入了 make_shared
却没有同时引入 make_unique
的原因吧。
# shared_ptr
Shared pointer,a class template:
1 |
|
简单来说,share pointer 具备引用计数、自动释放的特点。多个 shared_ptr
指向同一块内存时,每多一个指针指向该内存,引用数相应自增,反之亦然。当一块内存的 shared_ptr
引用数减少为 0 时,自动释放该内存。
即,我们可以通过智能指针来申请动态内存,其会有一个相应的引用计数(申请时初始值为 1,表明当前这个 shared_ptr
指向该内存)。每当有新的智能指针指向该内存时,引用计数自增。
https://cplusplus.com/reference/memory/shared_ptr/
智能指针的创建:
空智能指针:
1 | std::shared_ptr<int> p1; //指向 int 类型的智能指针,此时不传入任何实参,为空指针 |
非空智能指针:
1 | std::shared_ptr<int> p3(new int(10)); //指向一个值为 3 的 int 型的内存 |
构造函数(拷贝构造和移动构造):
1 | //拷贝构造函数 |
成员方法:
swap()
交换两个相同类型shared_ptr
的内容reset()
当该函数没有传入实参时,作用是将当前shared_ptr
所指内存的引用计数减 1,同时将当前指针对象重置为一个空指针;
当该函数传入一个实参(一个新申请的堆空间)时,则当前指针对象会指向该新申请的堆空间且引用计数初始值为 1,同时,原指向的堆空间(如果有)的引用计数自然而然减 1,如果减 1 后为 0,则释放该内存
get()
获得shared_ptr
对象内部包含的普通指针use_count()
返回当前智能指针所指向内存的引用计数值,即所有指向该内存的智能指针的数量unique()
判断当前智能指针指向的堆内存,是否不再有其他shared_ptr
指向它
参考链接:
https://cplusplus.com/reference/memory/shared_ptr/reset/
http://c.biancheng.net/view/7898.html
http://c.biancheng.net/view/430.html
https://blog.csdn.net/ff_gogo/article/details/123512482
https://www.cnblogs.com/dream397/p/14620324.html
实际应用:
1 | m_dds_ctx.reset(new minieye::DDS::Context(FLAGS_config_json_radar, true)); |
# deleter 删除器
对于自定义的类,我们知道存在构造函数和析构函数,析构函数能够在对象销毁时被自动调用,结合智能指针则能够实现自动管理,让其在引用计数减为 0 时自动调用析构函数释放资源。
但是,对于内置的类型,比如 int、float 这种,并不存在任何析构函数,所以无论我们是否使用智能指针,我们都不能很好地调用所谓的 “析构函数” 来释放资源。比如:
1 | std::shared_ptr<int> num{new int(5)}; |
针对这种情况,我们需要用到 deleter。
在智能指针中,可以指定一个删除器,用于在引用计数减为 0 时调用该删除器,进行资源的释放。以 shared_ptr
举例,它的其中两种构造函数如下:
1 | template <class U, class D> shared_ptr(U* p, D del); |
deleter
可以是以下类型之一:
- 函数指针:指向一个函数,该函数会在智能指针的资源需要释放时被调用。
- 函数对象(Functor):是一个类对象,重载了函数调用运算符
operator()
,可以像函数一样调用。 - Lambda 表达式:一种匿名函数,可以用作智能指针的
deleter
。
于是就有了这种智能指针的构造方式:
1 | // 使用 Lambda 表达式作为 deleter 的 std::shared_ptr |
整体示例代码:
1 |
|
大概是这样。
这里需要注意的是,我们的 shared_ptr
的构造函数存在几种形式:
1 | template< class Y > |
对于前者,当引用计数值为 0 时会使用
delete ptr
或 delete[] ptr
,具体取决于 T
是否为数组类型。而对于后者,则是通过传入一个用户自定义的删除器,在引用计数值为 0 自动调用。详细信息见:shared_ptr但是由此又似乎会得出结论,哪怕我们没有指定删除器,利用原本的 delete 语法也能处理基本类型嘛?再者,当使用智能指针管理基础类型时,真的不能有效释放内存吗?sus
# Q&A
智能指针如何判空?
我们知道,对于裸指针,我们可以直接在 if 条件语句中将其用作条件进行判空,如:
1 | int* rp = new int(5); |
同样的,对于智能指针,也能这么操作。
智能指针可以使用 operator bool()
或 get()
方法来进行空指针判断。以下是两种常用的方法:
- 使用
operator bool()
:
std::shared_ptr
和 std::unique_ptr
都定义了 operator bool()
,可以用于将智能指针直接用于布尔上下文中进行判空。
1 | std::shared_ptr<int> ptr = std::make_shared<int>(42); |
- 使用
get()
方法:
get()
方法返回底层裸指针,你可以将其与 nullptr
比较来进行判空。
1 | std::shared_ptr<int> ptr = std::make_shared<int>(42); |
不过第二种方法有点没必要,好好的智能指针不用,还把它转化成裸指针,何苦呢
另外,这里的 operator bool()
是操作符重载,具体地:
operator bool()
是一个类型转换运算符,用于将类对象转换为布尔值。在 C++ 中,运算符重载允许程序员自定义类对象在特定上下文中的操作行为,而 operator bool()
是其中一种常见的运算符重载。
运算符重载的语法如下:
1 | class MyClass { |
在上述示例中, operator bool()
被定义为将 MyClass
类的对象转换为布尔值。注意其中的 explicit
关键字,它表示这个转换是显式的,防止隐式转换。
对于智能指针类(如 std::shared_ptr
和 std::unique_ptr
), operator bool()
的实现通常是检查底层指针是否为 nullptr
。这样,当智能指针指向有效对象时,转换结果为 true
,指向空对象时,转换结果为 false
。这种设计使得智能指针在条件语句中的使用更加自然,就像原始指针一样。
以下是智能指针 operator bool()
的典型实现:
1 | class SmartPtr { |
这使得你可以像下面这样使用智能指针:
1 | SmartPtr ptr = ...; // 通过某种方式初始化 |
这种运算符重载的使用,使得在条件语句中判空更加直观,提高了代码的可读性。
# 相关阅读
stack overflow:如何判断我的 shared_ptr 被谁持有?