文章

智能指针

智能指针

[TOC]

1 什么是智能指针

C++没有垃圾回收机制,需要程序员自己释放和分配内存,否则就会照成内存泄漏。

智能指针是指向动态对象的指针,当其应该被释放时,智能指针可以确保自动释放内存,不需要手动释放,避免内存泄漏问题,更加容易也更加安全地使用动态内存。

智能指针的本质是类模版,当智能指针所指向的对象使用完后,对象会自动调用析构函数去释放指针所指向的空间。

以下是智能指针基本框架,所有智能指针类模版都包含一个对象指针、构造函数、析构函数:1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>  

using namespace std;
template <typename T>
class SmartPtr {
public:
	SmartPtr(T* _ptr) :ptr(_ptr) {}  //构造函数  
	~SmartPtr() 
	{ //析构函数  
		if (ptr != nullptr) 
		{ 
			cout << "smartprt: delete" << endl;
			delete ptr;
			ptr = nullptr;
		}
	}
private:
	T* ptr;   //指针对象  
};
int main(int argc, char* argv[]) 
{
	SmartPtr<int> prt_int(new int(1));  //指向int类型的智能指针  
	SmartPtr<string> prt_string(new string("abc"));   //指向string类型的智能指针  
	return 0;
}

2 智能指针的类型

现行可用的智能指针包含三种:unique_ptr、shared_ptr、weak_ptr,以下依次介绍了这三种智能指针的大概原理和基本用法。

还有一种auto_ptr,他是C++98的智能指针,C++11已抛弃,故本文中不做介绍。

我在VS2022中新建了一个控制台程序,可以直接调用上述指针。但是若提示报错的话,就需要 #include <memory>

2.1 unique_ptr

注意unique_ptr是独占对象的所有权的,它不允许其他的智能指针共享其内部的指针。就是在某个时候一定只有一个unique_ptr指向一个特定的对象,当unique_prt被销毁的时候它所指向的对象也会被销毁2

unique_ptr直接禁用了拷贝构造函数和赋值构造函数1。可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr3

能使用unique_ptr时就不要使用share_ptr指针(后者需要保证线程安全,所以在赋值or销毁时overhead开销更高)4

(1)构造方式

1
2
3
4
5
6
7
 std::unique_ptr<Entity> e1 = new Entity();        //不合法,指针是不可转让的  
 std::unique_ptr<Entity> e1(new Entity());         //OK   
 std::unique_ptr<Entity> e1 = std::make_unique<Entity>();  //首选   
 auto e1 = std::make_unique<Entity>();             //首选    
 std::unique_ptr<Entity> e2 = e1;                  //不合法,指针是不能复制的   
 std::unique_ptr<Entity> e2 = std::move(e1);       //可移动,所有权转移    
 func(std::move(e1));                              //这样函数传参:所有权转移 

(2)其他成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
 //通过函数返回的方式初始化    
 unique_ptr<int> SmartPtr = func();
 //解除对原始内存的管理    
 SmartPtr.reset();
 //重新指定智能指针管理的原始内存    
 SmartPtr.reset(new int(2));
 //get()方法可以获取智能智能指针管理的原始地址    
 cout << "ptr addr = " << SmartPtr.get() << endl;
 cout << "ptr content = " << *SmartPtr.get() << endl;
 //放弃对指针的控制权返回指针,并将自身置为空。  
 //共享指针释放,内存不释放:  
 SmartPtr.release();
 SmartPtr = nullptr;

(3)使用方法

可通过智能指针直接调用类内的成员函数。也可以通过get()函数取得原始资源的指针,再通过该指针进行调用。

在参考文章5中有用法示例的代码。

2.2 shared_ptr

shared_ptr实现了共享拥有的概念,利用“引用计数”来控制堆上对象的生命周期。允许多个shared_ptr指向同一块资源,并且保证共享资源只会被释放一次,所以程序不会崩溃1

原理:在初始化的时候引用计数设为1,每当被拷贝或者赋值的时候引用计数+1,析构的时候引用计数-1,直到引用计数被减到0,那么就可以delete掉对象的指针了。

每个shared_ptr都有两个指针,一个原始指针,一个计数区域的指针(SharedPtrControlBlock)5

(1)提供的函数

构造shared_ptr的方法:

1
2
3
4
5
6
7
8
9
 std::shared_ptr<Entity> e1(new Entity());                 //OK    
 std::shared_ptr<Entity> e1 = std::make_shared<Entity>();  //首选
 auto e1 = std::make_shared<Entity>();                     //首选    
 std::shared_ptr<Entity> e2 = e1;                          //可复制,计数+1    
 std::shared_ptr<Entity> e2 = std::move(e1);               //可移动,计数不变   
 e2.reset();                   //释放托管对象的所有权(若有),计数-1  
 e2.reset(new Entity());       //用新的指针代替原先托管的对象  
 func(std::move(e1));          //这样函数传参:计数不变    
 func(e1);                     //这样函数传参:计数+1 

创建新的shared_ptr对象的最佳方法是使用std :: make_shared,因为std::make_shared 一次性为int对象和用于引用计数的数据都分配了内存,最为高效,而new操作符只是为int分配了内存6

详细解释下为什么使用make_shared的创建方式会更加的高效了,在我们是使用new的方法去初始化一个shared_ptr的时候,我们需要先在堆上申请分配一块内存,用来存储对象,然后用这个变量调用share_ptr的构造函数,再次分配一次内存,而使用make_shared就只需要分配一次内存就可以了2

关于智能指针调用reset的初始化方式,这个函数在智能指针没有值的时候调用是用来初始化的,当这个智能指针有值的时候,调用reset函数就会引起原有对象智能指针的引用计数-12

Shared_ptr的其他成员函数:

1
2
3
4
5
6
 use_count() //返回引用计数的个数  
 unique()    //返回是否是独占所有权(use_count是否为1)  
 swap()      //交换两个shared_ptr对象(即交换所拥有的对象,引用计数也随之交换)  
 reset()     //放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少 
 get()       //返回存储的指针。
             //存储的指针指向shared_ptr对象解引用的对象,一般与其拥有的指针相同

(2)使用方法

1
2
3
4
5
6
 std::shared_ptr<entity> e = std::make_shared<entity>();
 //使用方法一,取得原始资源进行使用:  
 entity * t = e.get();
 t->func();   //func()是entity类内的成员函数  
 //使用方法二,可通过智能指针直接调用类内的成员函数  
 e->func();   //func()是entity类内的成员函数 

一定要谨慎的使用get()函数,当我们用一个裸指针上面的(entity*) 来保存地址的时候,我们没有办法掌握ptr会在什么时候释放这块内存地址,这样我们在使用的过程中就会产生不可预知的错误。如果我们获取了这个裸指针,一不小心调用了delete,就会导致同一块内存地址析构了两次。如果我们用一个shared_ptr来保存这个地址,那么我们就相当于又有了一个从1开始计数的shared_ptr与ptr都指向这块内存,最终的结果就是调用两次析构2

在参考文章5中有用法示例的代码。

(3)其他

使用make_shared的优势和劣势,见参考文章5

2.3 weak_ptr

weak_ptr不共享指针,不能操作资源,它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用是监测shared_ptr所管理的资源是否存在1

C++11标准虽然将weak_ptr定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至可以说,weak_ptr是为了辅助shared_ptr的存在5,它不管理shared_ptr内部的指针,借助 weak_ptr 类型指针,我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等7

需要注意的是,当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数7

除此之外,weak_ptr<T> 模板类中没有重载 *-> 运算符,因为他不共享指针,不能操作资源,只能访问所指的堆内存,而无法修改它7,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视shared_ptr中管理的资源是否存在3

利用weak_ptr可以解决shared_ptr的一些问题:

  • 返回管理this的shared_ptr1
  • 解决循环引用问题,避免内存泄漏1

(1)函数

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 //创建一个空weak_ptr指针:  
 std::weak_ptr<int> wp1;
 
 //凭借已有weak_ptr指针,创建一个新的weak_ptr指针:  
 std::weak_ptr<int> wp2(wp1);
 //若 wp1 为空指针,则 wp2 也为空指针;  
 //反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,  
 //则 wp2 也指向该块存储空间(可以访问,但无所有权)。  
 
 //weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,  
 //因为在构建 weak_ptr 指针对象时,  
 //可以利用已有的 shared_ptr 指针为其初始化。例如:  
 std::shared_ptr<int> sp(new int);
 std::weak_ptr<int> wp3(sp);
 //由此,wp3 指针和 sp 指针有相同的指针。  
 //再次强调,weak_ptr 类型指针不会导致堆内存空间的引用计数增加或减少。

其他常用的成员方法及各自功能见下面的表格7

成员方法 功能
operator=() 重载 = 赋值运算符, weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
swap(x) 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
reset() 将当前weak_ptr指针置为空指针(清空对象,使其不监测任何资源)。
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前weak_ptr指针为否过期,判断观测的资源是否已经被释放。
lock() 获取管理所监测资源的shared_ptr对象。如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
operator=() 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。

(2)使用方法

在参考文章5中有用法示例的代码。

2.4 auto_ptr

auto_ptr是管理权转移的思想,即原对象拷贝给新对象,原对象就会被设置成nullptr。此时就只有新对象指向资源空间。如果此时再去调用原对象,那整个程序会崩溃,所以现在很多公司禁用auto_ptr1。不仅如此,在C++11中,该智能指针已被弃用。

不要使用std::auto_ptr5

2.5 小结

类型 描述
unique_ptr 独占所指向的对象。同一时间只有一个智能指针能指向该对象,禁止指针的拷贝。
shared_ptr 共享指针,强引用。允许多个指针指向同一个对象,使用计数机制记录被共享指针数,对象与资源在最后一个引用被销毁时释放。
weak_ptr 弱引用,不共享指针、不能操作资源。监视shared_ptr所管理的对象,进行对象内存管理的是shared_ptr。
auto_ptr 【已弃用】采用所有权模式,可以剥夺所有权,即当对象拷贝或者赋值后,前面的对象就悬空了。

(1)从指针与对象的对应关系进行分类4

一种是可以使用多个智能指针管理同一块内存区域,每增加一个智能指针,就会增加1次引用计数。shared_ptr属于这种。

另一类是不能使用多个智能指针管理同一块内存区域,通俗来说,当智能指针2来管理这一块内存时,原先管理这一块内存的智能指针1只能释放对这一块指针的所有权。auto_ptr、unique_ptr、weak_ptr属于这种。

(2)从是否引用计数的角度进行分类8

  • 不带引用计数的智能指针:

    auto_ptr:不推荐使用,且C++11标准中被抛弃。

    scoped_ptr:不支持拷贝构造,和赋值重载,实现到私有权限,无法访问,后期g++ 10没有该对象了。

    unique_ptr:推荐使用

  • 带引用计数:

    多个智能指针可以管理同一个资源,每个对象资源匹配一个引用计数,给一个资源做赋值或者拷贝构造的时候,引用计数加1。一个资源出了它的作用域之后,引用计数减1,如果引用计数count == 0,资源就释放了。

    shared_ptr:强智能指针,可以改变资源的引用计数。

    weak_ptr:弱智能指针,不会改变资源的引用计数。

    定义对象的时候,用强智能指针;引用对象的地方使用弱指针,注意:弱智能指针是无法调用对象的成员的,它只是一个观察的作用,可以在使用的时候升级为强指针8

    std::shared_ptr<A> ps = _ptra.lock();

3 删除器

智能指针初始化的时候可以指定删除动作,这个删除操作对应的函数是删除器。删除器本质是一个回调函数,我们只需进行实现,其调用是由智能指针完成。

在c++11中,当智能指针指向数组时,需要指定删除器,因为默认删除器不支持数组对象。自定义删除器可以是函数指针、仿函数、lambda、包装器1

当我们用shared_ptr管理数组的时候,一定要指定删除器2

unique_ptr指定删除器与shared_ptr的不同,unique_ptr指定删除器的时候,需要确定删除器的类型2

4 std::enable_shared_from_this

std::enable_shared_from_this 是一个模板类,它在标准库(STL)中提供,用于 让类对象能安全地生成自身的 std::shared_ptr 实例9

当你希望 类的实例能够从内部成员函数中获取指向自身的 std::shared_ptr 时,这个特性非常有用。为了这样做,该类必须继承自 std::enable_shared_from_this

下面是一个简单的例子,展示了如何用 std::enable_shared_from_this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <memory>
#include <iostream>

class MyClass : public std::enable_shared_from_this<MyClass>
{
public:
    std::shared_ptr<MyClass> get_shared() {
        return shared_from_this();
    }

    void do_something() {
        // 在这里,你可以安全地使用 shared_from_this
        std::shared_ptr<MyClass> sharedPtr = get_shared();
        // ...
    }

    ~MyClass() {
        std::cout << "MyClass destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<MyClass> myInstance = std::make_shared<MyClass>();
    myInstance->do_something();
    return 0;
}

在这个例子中,MyClass 继承自 std::enable_shared_from_this<MyClass>。这允许 MyClass 的实例在成员函数do_something 中安全地调用 get_shared 方法, get_shared 会返回一个新的 std::shared_ptr<MyClass>,指向调用方法的对象实例。这是安全的,因为 myInstance 已经是通过 std::make_shared 创建的 std::shared_ptr

请注意,为了能够安全地使用 shared_from_this,对象必须已经被 std::shared_ptr 管理。如果对象不是由 std::shared_ptr 创建的,调用 shared_from_this 将会抛出一个 std::bad_weak_ptr 异常。

5 小结

当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,使用std::unique_ptr;

当需要一个共享资源所有权(访问权+生命控制权)的指针,使用std::shared_ptr;

当需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

推荐用法5:一个shared_ptr和n个weak_ptr搭配使用 而不是n个shared_ptr,因为一般模型中,最好总是被一个指针控制生命周期,然后可以被n个指针控制访问。这样从逻辑上看,大部分模型的生命在直观上总是受某一样东西直接控制而不是多样东西共同控制。从程序上看,也能够完全避免生命周期互相控制引发的循环引用问题。

6 一些智能指针相关的问题

6.1 智能指针怎么解决交叉引用造成的内存泄漏

结论:创建对象时使用shared_ptr强智能指针指向,其余情况都使用weak_ptr弱智能指针指向。具体解释和例子见参考文章:42

6.2 不要混合使用智能指针和裸指针

在同一个项目中坚持只使用智能指针,不使用裸指针,否则会出现忘记delete裸指针的情况。另外需要注意的事项还会很多,很复杂10

6.3 不使用 get()函数初始化或(reset)另外的智能指针4

6.4 不要使用同一个原始指针构造 shared_ptr6

创建多个 shared_ptr 的正常方法是使用一个已存在的 shared_ptr 进行创建,而不是使用同一个原始指针进行创建。

假如使用原始指针num创建了p1,又同样方法创建了p3,当p1超出作用域时会调用delete释放num内存,此时num成了悬空指针,当p3超出作用域再次delete的时候就可能会出错6

6.5 不要用栈中的指针构造 shared_ptr 对象6

shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。

当 shared_ptr 对象超出作用域调用析构函数delete 栈上对象的指针时会出错。

6.6 shared_ptr 和 unique_ptr 哪个好

std::shared_ptrstd::unique_ptr 都有它们自己的使用场景,不能简单地说哪一个更好。它们提供了不同类型的内存管理策略,适用于不同的情况。9

std::unique_ptr

  • 是C++11引入的智能指针,代表了对某个资源的独占所有权。
  • std::unique_ptr 被销毁时,它所拥有的资源也会被自动释放。
  • std::unique_ptr 尺寸小,通常与原生指针一样,没有额外的内存开销。
  • 不支持拷贝操作,但可以通过 std::move 进行所有权的转移。
  • 适合用于确保资源在任何时候只有一个拥有者的情况。

std::shared_ptr

  • 是C++11引入的另一种智能指针,它允许多个指针实例共享对同一个对象的所有权。
  • std::shared_ptr 使用引用计数机制来跟踪有多少个 std::shared_ptr 实例拥有共同的对象。
  • 当最后一个拥有对象的 std::shared_ptr 被销毁时,关联的资源会被自动释放。
  • std::shared_ptr 有额外的内存和性能开销,因为它需要维护引用计数。
  • 适合用于多个所有者需要管理同一个资源的情况。

选择哪一个取决于你的具体需求:

  • 如果你需要独占所有权模型,或者是为了避免额外的性能开销,std::unique_ptr 是更好的选择。
  • 如果你需要多个所有者共享同一个资源,并且不介意额外的内存和性能开销,那么 std::shared_ptr 是更合适的。

在不需要共享所有权的情况下,推荐的做法是默认使用 std::unique_ptr,因为它更轻量级,并且通过明确单一所有权有助于减少错误。只有在确实需要多个引用共享所有权时,才选用 std::shared_ptr

参考文章11121314151617181920212223

  1. CSDN. c++11智能指针[DB/OL]. (2022-07-17).https://blog.csdn.net/weixin_43858819/article/details/125529689 ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8

  2. CSDN. C++11_智能指针 [DB/OL]. (2022-04-23). https://blog.csdn.net/weixin_44387482/article/details/124356614 ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7

  3. CSDN. c++11之智能指针 [DB/OL]. (2022-09-14). https://blog.csdn.net/qq_56673429/article/details/124837626 ↩︎ ↩︎2

  4. ELEMENT-UI. 【C++11】Smart Pointer智能指针[DB/OL]. (2023-01-08). https://www.ngui.cc/el/2678278.html?action=onClick ↩︎ ↩︎2 ↩︎3 ↩︎4

  5. 博客园. 【C++11】4种智能指针[DB/OL]. (2022-10-04). https://www.cnblogs.com/bandaoyu/p/16752819.html ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8

  6. CSDN. C++ 智能指针 shared_ptr 详解与示例 [DB/OL]. (2018-12-24). https://blog.csdn.net/shaosunrise/article/details/85228823 ↩︎ ↩︎2 ↩︎3 ↩︎4

  7. C语言中文网. C++11 weak_ptr智能指针 [DB/OL]. (2019-01-05). http://c.biancheng.net/view/7918.html ↩︎ ↩︎2 ↩︎3 ↩︎4

  8. CSDN. 深入理解掌握智能指针 [DB/OL]. (2022-02-12). https://blog.csdn.net/weixin_40533189/article/details/122857572 ↩︎ ↩︎2

  9. 来源:ChatGPT ↩︎ ↩︎2

  10. CSDN. 智能指针和普通指针混用注意之一[DB/OL]. (2019-01-05). https://blog.csdn.net/ziliwangmoe/article/details/85840770 ↩︎

  11. CSDN. 智能指针shared_ptr的reset使用 [DB/OL]. (2023-02-07). https://blog.csdn.net/tianyexing2008/article/details/128919341 ↩︎

  12. 博客园. C++11智能指针——shared_ptr类成员函数详解[DB/OL]. (2021-07-19). https://www.cnblogs.com/JCpeng/p/15031742.html ↩︎

  13. CSDN. c++ unique_ptr共享指针release函数 [DB/OL]. (2023-02-09). https://blog.csdn.net/weixin_43061687/article/details/128862268 ↩︎

  14. CSDN. 深入掌握C++智能指针 [DB/OL]. (2019-03-20).https://blog.csdn.net/QIANGWEIYUAN/article/details/88562935?spm=1001.2014.3001.5502 ↩︎

  15. CSDN. C++动态内存与智能指针[DB/OL]. (2021-12-16). https://blog.csdn.net/weixin_43848885/article/details/121983343 ↩︎

  16. 从零实现智能指针:unique_ptr ↩︎

  17. C++14 make_unique ↩︎

  18. 【C++】 浅析 std::share_ptr 内部结构 ↩︎

  19. 探秘C++标准模板库中的三种智能指针 ↩︎

  20. C++类循环依赖破解:前向声明与智能指针的妙用 ↩︎

  21. C++中的RAII机制及其智能指针的应用 ↩︎

  22. 简单易学,三分钟搞定智能指针 ↩︎

  23. C++智能指针的基本实现 ↩︎

本文由作者按照 CC BY 4.0 进行授权