• 智能指针


    unique_ptr

    为什么引入智能指针unique_ptr

    普通指针的不足
    1. 内存泄漏:最常见的00问题是忘记释放分配给指针的内存,导致内存泄漏。这通常发生在异常情况或程序员忘记在不再需要内存时释放它。
    2. 悬空指针:当对象被删除,但指针未被重置时,指针就悬空了。悬空指针可能会指向无效的内存区域,尝试访问可能会导致未定义的行为。
    3. 所有权不明:当指针被传递给另一个函数或线程时,很难跟踪谁拥有该指针,并负责最终的删除。这种所有权不明确常导致多重释放或非预期的内存泄漏。
    4. 资源管理困难:对于非内存资源(如文件句柄、网络连接等),使用普通指针进行管理同样需要程序员手动关闭或释放资源,这增加了出错的可能性。
    普通指针的释放
    1. 类内的指针:在析构函数中释放。
    2. C++内置数据类型,如何释放?
    3. new出来的类,本身如何释放?
    智能指针的设计思路

    智能指针的核心设计思路是利用对象的生命周期来管理资源(特别是内存),从而避免资源泄露和其他相关问题。

    1. 智能指针是类模板,在栈上创建智能指针对象
      智能指针在本质上是模板类,意味着它们可以用于指向任何类型的对象。
    2. 把普通指针交给智能指针管理
      当你创建了一个原始指针后,你可以将其交给一个智能指针来管理。这样做的好处是,智能指针会负责这个原始指针所指向的内存的释放工作。
    3. 智能指针对象过期时,调用析构函数释放普通指针的内存
      由于智能指针是在栈上分配的,因此当智能指针的生命周期结束(例如,当它超出作用域时)时,它的析构函数会自动被调用。在智能指针的析构函数中,它会释放其管理的原始指针的内存。

    unique_ptr 使用

    unique_ptr提供了严格的所有权管理机制,通过独占的方式管理其指向的对象。也就是说unique_ptr拥有其指向的对象,确保同一时间只有一个unique_ptr拥有该对象。当这个unique_ptr被销毁(例如,超出作用域)时,指向的对象也随即被销毁。

    使用unique_ptr需要包含头文件#include

    unique_ptr的结构

    #include 
    
    template <typename T, typename D = std::default_delete<T>>
    class unique_ptr {
    public:
        // 基础构造函数,接受一个指向已分配对象的指针。
        explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}
    
        // 析构函数,负责释放 unique_ptr 拥有的对象。
        ~unique_ptr() {
            if (ptr) {
                D deleter;
                deleter(ptr); // 默认使用 delete 删除器
            }
        }
    
        // 移动构造函数,接受一个右值引用至其他 unique_ptr。
        // “窃取”资源,将源 unique_ptr 置为空状态。
        unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
            other.ptr = nullptr;
        }
    
        // 移动赋值操作符,接受一个右值引用至其他 unique_ptr。
        // 释放当前对象,接管新对象的资源,将源 unique_ptr 置为空状态。
        unique_ptr& operator=(unique_ptr&& other) noexcept {
            if (this != &other) {
                reset(); // 释放当前指针
                ptr = other.ptr; // 接管资源
                other.ptr = nullptr; // 将源 unique_ptr 置空
            }
            return *this;
        }
    
        // 禁用拷贝构造函数
        unique_ptr(const unique_ptr&) = delete;
    
        // 禁用拷贝赋值操作符
        unique_ptr& operator=(const unique_ptr&) = delete;
    
        // 解引用操作符,返回所指对象的引用。
        T& operator*() const {
            return *ptr;
        }
    
        // 箭头操作符,允许通过 unique_ptr 访问其所拥有对象的成员。
        T* operator->() const noexcept {
            return ptr;
        }
    
        // 释放 unique_ptr 对象的所有权,返回原始指针,unique_ptr 不再负责对象的删除。
        T* release() noexcept {
            T* old_ptr = ptr;
            ptr = nullptr;
            return old_ptr;
        }
    
        // 释放 unique_ptr 当前拥有的对象,并可选地接受一个新的对象指针。
        void reset(T* p = nullptr) noexcept {
            if (ptr) {
                D deleter;
                deleter(ptr); // 删除当前对象
            }
            ptr = p; // 接受新的对象指针(如果提供的话)
        }
    
    private:
        T* ptr; // 内部原始指针,指向 unique_ptr 所拥有的对象。
    };
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 第一个模板参数 T 是指针指向的对象类型。
    • 第二个模板参数 D 是用于删除所拥有对象的策略,即删除器。默认情况下,使用的是 default_delete,它会用 delete 操作符来删除对象。

    基本用法

    测试类AA的定义:

    class AA
    {
    public:
    	string m_name;
    	AA() { cout << m_name << "调用构造函数AA()。\n"; }
    	AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
    	~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 初始化

    方法一:直接初始化

    unique_ptr<AA> p1(new AA("天梦"));
    
    • 1

    方法二:使用 std::make_unique(C++14 及以后版本推荐使用)

    auto p1 = make_unique<AA>("天梦");   // 使用 make_unique (类型推断)
    unique_ptr<AA> p2 = make_unique<AA>("天梦");  // 明确指定类型
    
    • 1
    • 2

    方法三:使用已存在的指针(不推荐)

    AA* p = new AA("西施");
    unique_ptr<AA> p0(p);  // 用已存在的地址初始化,这种方式不推荐因为它可能导致所有权不明确。
    
    • 1
    • 2
    2. 使用方法

    unique_ptr 重载了解引用操作符 * 和箭头操作符 ->,这意味着它们可以像普通指针一样使用,但它们提供了额外的内存管理功能。

    auto p = make_unique<AA>("天梦");
    p->method();  // 调用对象的方法
    (*p).method();  // 同上
    
    • 1
    • 2
    • 3

    不支持拷贝构造和拷贝赋值,因为 unique_ptr 的设计目的是提供对其所指对象的唯一所有权。

    下面具体解释一下为什么unique_ptr不支持拷贝构造和拷贝赋值。

    我认为unique_ptr它核心是提供了对动态分配内存的“独占所有权”。这种所有权模型具有严格限制,为了避免资源泄露和与其他资源共享相关的问题。

    1. 防止资源的多重删除:如果说unique_ptr能够被复制,那么你最终会得到多个智能指针实例指向同一个资源。这本身并不坏,但问题在于每个实例可能都认为自己“拥有”资源,因此,当这些实例中其中一个离开作用域销毁其拥有的资源后,会导致其他指向同一资源的unique_ptr实例成为野指针,他们也可能尝试删除已经不复存在的资源,从而导致未定义行为,通常是程序崩溃。
    2. 强化唯一所有权语义unique_ptr 的核心思想是它是资源的唯一所有者。允许复制会破坏这种唯一性,因为复制的结果是有两个所有者指向同一个资源。通过禁止复制,unique_ptr 确保了所有权的概念始终得到维护,你可以明确地知道资源的生命周期和所有者身份。
    3. 异常安全:使用原始指针的代码通常对异常情况的处理不够,可能会导致资源泄漏。例如,如果在执行复制操作的过程中抛出异常,程序可能会处于一种状态,其中某些已分配资源没有被清理。而 unique_ptr 通过确保资源只有一个所有者来避免这种情况,即使在异常情况下,也会在 unique_ptr 对象销毁时释放资源。
    AA* p = new AA("天梦");
    	unique_ptr<AA> pu2 = p;              // 错误,不能把普通指针直接赋给智能指针。
    	unique_ptr<AA> pu3 = new AA("天梦"); // 错误,不能把普通指针直接赋给智能指针。
    	unique_ptr<AA> pu2 = pu1;           // 错误,不能用其它unique_ptr拷贝构造。
    	unique_ptr<AA> pu3;
    	pu3 = pu1;                            // 错误,不能用=对unique_ptr进行赋值。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但是,你可以通过移动语义转移所有权:

    unique_ptr<AA> p1 = make_unique<AA>("天梦");
    unique_ptr<AA> p2 = std::move(p1); // p1 释放所有权,p2 成为新的所有者
    
    • 1
    • 2
    • 注意

    1. 不要用同一个原始指针(普通指针、裸指针)初始化多个unique_ptr对象。
    2. get()方法返回裸指针。
    3. 不要用unique_ptr管理不是new分配的内存。
    4. 不支持指针算术(即,不支持 +, -, ++, --)。

    请看简单示例:

    #include 
    #include 
    using namespace std;
    
    class AA
    {
    public:
        string m_name;
        AA() { cout << m_name << "调用构造函数AA()。\n"; }
        AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
        ~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
    };
    
    
    int main() {
        AA* p = new AA("天梦");  // 定义原始指针p,分配内存并构造AA对象。
        unique_ptr<AA> pu(p);  // 创建智能指针对象,接管原始指针p的内存管理权。
        
        cout << "裸指针的值是: " << p << endl;  // 显示原始指针的内存地址。
        cout << "pu输出的结果是: " << pu.get() << endl;  // 显示智能指针管理的对象的内存地址。
        cout << "pu.get() 输出的结果是: " << pu.get() << endl;  // 同上,显示智能指针中存储的原始指针地址。
        cout << "pu的地址是: " << &pu << endl;  // 显示智能指针自身的地址。
    
        return 0;
    }
    
    • 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

    其中pu输出的结果是智能指针管理的对象的内存地址。这是因为unique_ptr重载了operator<<,返回的是unique_ptr所指向的对象的内存地址,就像它是一个裸指针一样。

    3. 用于函数的参数

    unique_ptr 作为函数参数时,通常传递引用或者通过 std::move 转移所有权,因为它们不支持拷贝语义。

    void some_function(unique_ptr<AA>& ptr) {
        // 不会转移所有权的函数
    }
    
    void some_function_take_ownership(unique_ptr<AA> ptr) {
        // 接受所有权的函数
    }
    
    // 调用
    auto p = make_unique<AA>("天梦");
    some_function(p);  // 传递引用,p 仍然拥有对象
    some_function_take_ownership(std::move(p));  // 转移所有权,p 不再拥有对象
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    unique_ptr的使用技巧

    1. 将一个unique_ptr赋给另一个时,如果源unique_ptr是一个临时右值,编译器允许这样做。如果源unique_ptr将存在一段时间,编译器禁止这样做。一般用于函数的返回值。

      unique_ptr<AA> p0;
      p0 = unique_ptr<AA>(new AA ("天梦")); //使用匿名对象赋值。
      
      • 1
      • 2

      我们使用移动赋值操作符将临时 unique_ptr 的所有权赋给 p0。在这个操作完成后,p0 现在拥有这个 AA 对象,而临时的 unique_ptr 不再拥有任何对象。

    2. nullptrunique_ptr赋值将释放对象,空的unique_ptr == nullptr

         unique_ptr<AA> p (new AA("天梦"));
         if(p != nullptr) cout<< "p is not null" << endl;
      
         p = nullptr;
         if(p == nullptr) cout<< "p is null" << endl;
      
      • 1
      • 2
      • 3
      • 4
      • 5

      如果你使用 nullptrstd::unique_ptr 赋值,它将释放其当前所拥有的对象(如果有的话),并将内部指针设置为 nullptr。这可以被用作一种释放 unique_ptr 所拥有的资源的方法。

    3. release()释放对原始指针的控制权,将unique_ptr置为空,返回裸指针(可以用于把unique_ptr传递给子函数,子函数将负责释放对象)

    4. std::move()可以转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr)

      #include 
      #include 
      using  namespace std;
      
      class AA
      {
      public:
      	string m_name;
      	AA() { cout << m_name << "调用构造函数AA()。\n"; }
      	AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
      	~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
      };
      
      // 函数func1()需要一个指针,但不对这个指针负责。
      void func1(const AA* a) {
      	cout << a->m_name << endl;
      }
      
      // 函数func2()需要一个指针,并且会对这个指针负责。
      void func2(AA* a) {
      	cout << a->m_name << endl;
      	delete a;
      }
      
      // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。
      void func3(const unique_ptr<AA> &a) {
      	cout << a->m_name << endl;
      }
      
      // 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。
      void func4(unique_ptr<AA> a) {
      	cout << a->m_name << endl;
      }
      
      int main()
      {
      	unique_ptr<AA> pu(new AA("tianmeng"));
      
      	cout << "开始调用函数。\n";
      	//func1(pu.get());        // 函数func1()需要一个指针,但不对这个指针负责。
      	//func2(pu.release());  // 函数func2()需要一个指针,并且会对这个指针负责。
      	//func3(pu);                // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。
      	func4(move(pu));     // 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。
      	cout << "调用函数完成。\n";
      
      	if (pu == nullptr) cout << "pu是空指针。\n";
      }
      
      • 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
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
    5. reset()释放对象。

      void reset(T * _ptr= (T *) nullptr);
      pp.reset();        // 释放pp对象指向的资源对象。
      pp.reset(nullptr);  // 释放pp对象指向的资源对象
      pp.reset(new AA("bbb"));  // 释放pp指向的资源对象,同时指向新的对象。
      
      • 1
      • 2
      • 3
      • 4
    6. swap()交换两个unique_ptr的控制权

      pp1.swap(pp2); // 交换两个 unique_ptr 的所有权
      
      • 1
    7. unique_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。

      #include 
      #include 
      using namespace std;
      
      class Base {
      public:
          virtual ~Base() = default;
          virtual void show() const {
              cout << "Base class" << endl;
          }
      };
      
      class Derived : public Base {
      public:
          void show() const override {
              cout << "Derived class" << endl;
          }
      };
      
      int main() {
          unique_ptr<Base> basePtr = make_unique<Derived>();
          basePtr->show();  // Outputs: Derived class
      
          return 0;
      }
      
      • 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
    8. unique_ptr不是绝对安全,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。

      #include 
      #include 
      using  namespace std;
      
      class AA
      {
      public:
         string m_name;
         AA() { cout << m_name << "调用构造函数AA()。\n"; }
         AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
         ~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
      };
      
      unique_ptr<AA> p (new AA("全局变量"));
      
      int main(){
         unique_ptr<AA> p1(new AA("局部变量"));
      
         exit(0);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
    9. unique_ptr提供了支持数组的具体化版本。

      数组版本的unique_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
      声明unique_ptr管理的数组:

      unique_ptr<int[]> myArray(new int[5]);
      
      • 1

      或者使用现代的 std::make_unique 函数(从 C++14 开始支持数组版本):

      auto myArray = std::make_unique<int[]>(5);
      
      • 1

      与裸指针数组类似,你可以使用 [] 操作符来访问数组中的每个元素。由于返回的是引用,你可以用它作为左值

    shared_ptr

    shared_ptr共享它所指向的对象,多个shared_ptr可以指向(关联)相同的的对象,并且在内部采用计数机制来实现。

    当新的shared_ptr与对象关联时,引用计数加一。

    shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。

    基本用法

    初始化
    1. 方法一:直接使用构造函数

      shared_ptr<AA> p0(new AA("西施"));
      
      • 1
    2. 方法二:使用mark_shared

      shared_ptr<AA> p0 = make_shared<AA>("天梦");  // C++11标准,效率更高。
      shared_ptr<int> pp1=make_shared<int>();         // 数据类型为int。
      shared_ptr<AA> pp2 = make_shared<AA>();       // 数据类型为AA,默认构造函数。
      shared_ptr<AA> pp3 = make_shared<AA>("天梦");  // 数据类型为AA,一个参数的构造函数。
      shared_ptr<AA> pp4 = make_shared<AA>("天梦",8); // 数据类型为AA,两个参数的构造函数。
      
      • 1
      • 2
      • 3
      • 4
      • 5
    3. 方法三:采用已有的裸指针

      AA* p1 = new AA("天梦");
      shared_ptr<AA> p2(p1);
      
      • 1
      • 2
    4. 方法四:复制构造和赋值函数

      shared_ptr<AA> p3 = p0;  // 使用赋值操作符
      shared_ptr<AA> p4(p0);  // 使用复制构造函数
      
      • 1
      • 2
    使用方法
    • 操作符重载shared_ptr 重载了 *->,这意味着你可以像使用裸指针一样使用它。
    • 引用计数:你可以使用 use_count() 方法来查询当前有多少个 shared_ptr 共享同一个对象。而 unique() 方法则检查是否只有一个 shared_ptr 指向该对象。
    • 赋值操作shared_ptr支持赋值,左值的shared_ptr的计数器将减1,右值shared_ptr的计算器将加1。
    • 获取裸指针get() 方法可以返回内部的裸指针,但通常应该避免使用它,除非真的需要裸指针。
    • 注意事项:不应该使用同一个裸指针来初始化多个独立的 shared_ptr,这会导致在程序结束时该对象被多次删除,从而引起未定义的行为。
    用于函数的参数

    unique_ptr相同

    shared_ptr的使用技巧

    1. nullptrshared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr
    2. std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr
    3. reset()改变与资源的关联关系。
    4. swap()交换两个shared_ptr的控制权。
    5. shared_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
    6. shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放。
    7. shared_ptr的引用计数是线程安全的,但在多线程环境中修改shared_ptr或其所指向的对象时仍需要加锁。
    8. 如果unique_ptr足以解决问题,那么优先选择unique_ptr。因为unique_ptrshared_ptr更高效,使用的资源也更少。

    智能指针的删除器

    在C++中,尽管智能指针可以自动删除所指向的对象,但在某些情况下,可能需要以特定的方式删除这些对象,例如,当对象是在非标准堆上分配的,或者与其他资源(如文件句柄或数据库连接)相关联时。这时,自定义删除器就派上了用场。

    删除器是什么

    默认情况下,智能指针,如shared_ptrunique_ptr,在其生命周期结束时使用delete操作符来释放其管理的对象。但是,你可以提供一个自定义的删除器,以便更改智能指针的行为。

    删除器可以是

    • 全局函数:这是一个简单的函数,接受一个原始指针参数,并执行所需的清理操作。
    • 仿函数(Functor):这是一个定义了operator()的类或结构。它可以保持状态,并可以作为删除器使用。
    • Lambda表达式:这是一个简短的、可内联的匿名函数。它非常适合作为简短的删除器。

    注意:智能指针使用删除器时,只要在模板签名里指定删除器的类型。

    如:

    unique_ptr <AA,*decltype*(deletefunc)*> p4(*new* AA("p4"),deletefunc);  *//* *普通函数*
    
    • 1

    下面是详细示例:

    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    class AA
    {
    public:
        string m_name;
        AA() { cout << m_name << "调用构造函数AA()。\n"; }
        AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
        ~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
    };
    
    void deletefunc(AA *a){  // 自定义删除器   普通函数
        cout<<"自定义删除器(全局函数)"<<endl;
        delete a;
    }
    
    struct deleteclass{    // 自定义删除器 仿函数
        void operator()(AA *a){
            cout<<"自定义删除器(仿函数)"<<endl;
            delete a;
        }
    };
    
    auto deletelambda = [](AA *a){  // 自定义删除器 lambda表达式
        cout<<"自定义删除器(lambda表达式)"<<endl;
        delete a;
    };
    
    int main(){
        shared_ptr <AA> p1(new AA("p1"),deletefunc);  // 普通函数
        shared_ptr <AA> p2(new AA("p2"),deleteclass());  // 仿函数
        shared_ptr <AA> p3(new AA("p3"),deletelambda);  // lambda表达式
        
        unique_ptr <AA,decltype(deletefunc)*> p4(new AA("p4"),deletefunc);  // 普通函数
        unique_ptr <AA,void (*)(AA *)> p5(new AA("p5"),deletefunc);  // 普通函数
        unique_ptr <AA,deleteclass> p6(new AA("p6"));  // 仿函数
        unique_ptr <AA,decltype(deletelambda)> p7(new AA("p7"),deletelambda);  // lambda表达式
        
    }
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    weak_ptr

    shared_ptr存在的问题

    首先,我们要明确shared_ptr的主要优点:自动引用计数。这意味着每当有一个新的shared_ptr指向同一个资源,这个资源的引用计数会增加,当某个shared_ptr不再存在,计数会减少。只有当引用计数为0时,资源才会被释放。然而,这种机制有一个重要的弱点,就是循环引用。

    循环引用

    循环引用发生在两个或多个shared_ptr对象相互引用,导致他们之间形成了一个引用环。在这种情况下,涉及到的所有对象的引用计数都不会将至为0,这就意味着这些对象永远不会被自动删除,从而导致内存泄漏。

    #include 
    #include 
    using namespace std;
    
    class B;
    
    class A {
    public:
        shared_ptr<B> b_ptr;
        ~A() {
            cout << "A destructor called!" << endl;
        }
    };
    
    class B {
    public:
        shared_ptr<A> a_ptr;
        ~B() {
            cout << "B destructor called!" << endl;
        }
    };
    
    int main() {
        {
            shared_ptr<A> a = make_shared<A>();
            shared_ptr<B> b = make_shared<B>();
    
            a->b_ptr = b;  // A对象引用B对象
            b->a_ptr = a;  // B对象引用A对象
        }  // a和b的析构函数都不会被调用,因为存在循环引用
    
        cout << "End of main()" << endl;
    }
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在上述示例中,我们在A和B两个类中,分别创建了一个shared_ptr指向另一个类的对象,并在main函数中创建A和B的shared_ptr实例,互相引用。

    由于A的实例持有指向Bshared_ptr,并且B的实例持有指向Ashared_ptr,所以两者形成了一个引用循环。当我们退出作用域后,尽管ab的生命周期已经结束,但由于存在循环引用,它们的引用计数都不为0,因此析构函数不会被调用,从而导致内存泄漏。

    weak_ptr是什么

    为了解决shared_ptr可能导致的循环引用问题,C++11引入了weak_ptr,与shared_ptr不同,weak_ptr不会增加或减少其所观察对象的引用计数。这意味着weak_ptr可以观察一个对象,但不会导致该对象的生命期延长或缩短。

    功能和用途

    • weak_ptr主要与shared_ptr一起使用,以观察shared_ptr所拥有的资源。
    • weak_ptr观察的资源被销毁(例如,所有的shared_ptr都被销毁了)时,weak_ptr可以检测到这一点。
    • weak_ptrshared_ptr的一个"安全伙伴",使开发人员能够安全地管理复杂的对象关系,避免循环引用导致的内存泄漏。

    如何使用weak_ptr

    由于weak_ptr没有重载的->*操作符,您不能直接通过weak_ptr来访问其观察的对象。但您可以通过以下方式与weak_ptr互动:

    1. operator=():此操作允许您将一个shared_ptr或另一个weak_ptr赋值给当前的weak_ptr。这样,weak_ptr就可以观察(但不共享)该资源。
    2. expired():此函数用于检查weak_ptr指向的资源是否仍然存在。如果资源已经被销毁(即与之相关的所有shared_ptr都已经被销毁),则expired()返回true
    3. lock():这可能是weak_ptr中最常用的函数。它尝试获取指向对象的shared_ptr。如果对象仍然存在(即其引用计数不为0),lock()返回一个有效的shared_ptr。否则,它返回一个空的shared_ptr。这是一个线程安全的操作。
    4. reset():这将使weak_ptr不再指向任何对象,即它将其内部指针置为空。
    5. swap():此函数交换两个weak_ptr对象的内容。

    使用weak_ptr的常见场景

    • 解决循环引用:如果两个对象互相引用并且使用shared_ptr,可能会出现循环引用的问题。在这种情况下,您可以使用weak_ptr来打破这种循环。
    • 缓存和池:如果您正在实现某种资源池或缓存,并希望知道某资源是否仍在使用(但不想阻止其被删除),weak_ptr是一个好选择。
    • 观察者模式:当对象需要被多个观察者观察,但不应该由观察者保持活跃时,可以使用weak_ptr

    下面是一个简单示例:

    #include 
    #include 
    
    using namespace std;
    
    class Sample {
    public:
        Sample() {
            cout << "Sample constructor called!" << endl;
        }
        ~Sample() {
            cout << "Sample destructor called!" << endl;
        }
    };
    
    int main() {
        // 使用 shared_ptr 创建一个 Sample 对象
        shared_ptr<Sample> sp1 = make_shared<Sample>();
    
        // 使用 weak_ptr 指向该 Sample 对象
        weak_ptr<Sample> wp1;
    
        // 1. operator=()
        wp1 = sp1; // 使用 operator=() 将 shared_ptr 赋值给 weak_ptr
    
        // 使用 weak_ptr 指向同一个 Sample 对象
        weak_ptr<Sample> wp2 = wp1;
    
        // 2. expired()
        if (wp1.expired()) {
            cout << "wp1 is expired." << endl;
        } else {
            cout << "wp1 is not expired." << endl;
        }
    
        // 3. lock()
        shared_ptr<Sample> sp2 = wp1.lock(); // 提升 weak_ptr 为 shared_ptr
        if (sp2) {
            cout << "Successfully locked wp1!" << endl;
        }
    
        // 释放 sp1 指向的资源
        sp1.reset();
    
        if (wp1.expired()) {
            cout << "wp1 is now expired." << endl;
        }
    
        // 4. reset()
        wp2.reset(); // 将 wp2 置为空
    
        if (wp2.expired()) {
            cout << "wp2 is expired." << endl;
        }
    
        // 5. swap()
        wp1.swap(wp2); // 交换 wp1 和 wp2 的内容
    
        if (wp2.expired()) {
            cout << "After swap, wp2 is still expired." << endl;
        }
        if (wp1.expired()) {
            cout << "After swap, wp1 is now expired." << endl;
        }
    
        return 0;
    }
    
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
  • 相关阅读:
    java线程的状态与联系(一张图理解)
    左右切换箭头代替滚动条,实现类似走马灯效果
    linux内核各方向资料贴
    数据传输安全面临的主要挑战
    mmsegmentation 训练自制数据集
    阿里云轻量应用服务器月流量限制说明(部分套餐不限流量)
    10.11作业
    MyBatis学习:mapper.xml文件中传参时,标签使用javaType和jdbcType属性
    干货 | 答编辑/审稿人问之样本量大关
    哪款电容笔适用于ipad2017?Ipad2017推荐电容笔
  • 原文地址:https://blog.csdn.net/m0_73790534/article/details/133972310