当前位置: 首页>编程语言>正文

C++技能点之智能指针(一)

C++技能点之智能指针(一),第1张
智能指针思维导图

一、 引入智能指针的目的【内存泄漏】

  • 说起智能指针,首先想到的是为什么要引入智能指针?裸指针有什么问题?来看下面一段代码
for(int i=0; i<100; i++)
    char* p = new char[100];
  • 在这段代码中,我们通过new分配了10000个字节的堆内存空间。但由于作用域的限制,这10000个字节无法被其它地方使用,也没有被释放,就造成了内存泄漏
  • 所谓内存泄漏,本质上就是指针变量与其指向的资源不一定会同时被释放。那么如何才能保证指针变量与其指向的资源被同时释放呢?有聪明的程序员想到,如果某块资源没有任何指针指向它,就应该被释放。这也就引入了用引用计数的方式来实现智能指针。[1]
  • 引用计数实际比较容易理解,就是指向资源的指针数量,如果有新增指针指向这块资源引用计数就加1,如果有指向这块资源的指针被释放引用计数就减1。当引用计数为0时就意味着没有任何指针指向它,就被释放掉。这样的指针也就被称为智能指针。
  • C++11开始引入了三种常见的智能指针,分别是std::shared_ptr, std::weak_ptr, std::unique_ptr,接下来将分别介绍一下这三种智能指针。

二、std::shared_ptr

  • 先来看一段代码,来自cppreference [2]
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>

struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // Note: non-virtual destructor is OK here
    ~Base() { std::cout << "  Base::~Base()\n"; }
};

struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};

void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // thread-safe, even though the
                                    // shared use_count is incremented
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                    << "  lp.get() = " << lp.get()
                    << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}

int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();

    std::cout << "Created a shared Derived (as a pointer to Base)\n"
                << "  p.get() = " << p.get()
                << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // release ownership from main
    std::cout << "Shared ownership between 3 threads and released\n"
                << "ownership from main:\n"
                << "  p.get() = " << p.get()
                << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted Derived\n";
}
  • 我们来简单分析一下这段代码, main函数中先创建了一个指向子类对象的基类指针pstd::make_shared可以代替new,通过get方法可以拿到裸指针,use_count()返回该对象的引用计数】所以第一段输出为:
  Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0x100200018, p.use_count() = 1
  • 接下来创建了三个线程对象, 分别是t1, t2, t3。这个例子实际上是为了解释std::shared_ptr是线程安全的,原因是std::shared_ptr的引用计数是原子操作的。【reset()可用来减少一个引用计数】所以第二段的输出如下:
Shared ownership between 3 threads and released
ownership from main:
  p.get() = 0x0, p.use_count() = 0 
local pointer in a thread: 
  lp.get() = 0x100200018, lp.use_count() = 6 
local pointer in a thread: 
  lp.get() = 0x100200018, lp.use_count() = 4 
local pointer in a thread: 
  lp.get() = 0x100200018, lp.use_count() = 2 
  • 这里稍微多解释下,为什么lp.use_count()的结果是6,4,2【其实还可能会有其它的结果】?因为构建三个线程使得p的引用计数为3,三个线程的lp又指向p,所以lp最大值为6,每次离开作用域后引用计数减2,所以这里的结果是6,4,2。
  • 最后是析构,当最后一个线程被delete时引用计数为0因此发生析构,输出如下:
  Derived::~Derived() 
  Base::~Base() 
All threads completed, the last one deleted Derived 

三、std::weak_ptr

  • 同样先来看一段代码,来自现代C++教程:高速上手C++11/14/17/20 [3]
#include <iostream>
#include <memory>
    
struct A;
struct B;

struct A {
    std::shared_ptr<B> pointer;
    ~A() {
        std::cout << "A has been destroyed" << std::endl;
    }
};
struct B {
    std::shared_ptr<A> pointer;
    ~B() {
        std::cout << "B has been destroyed" << std::endl;
    }
};
int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->pointer = b;
    b->pointer = a;
}
  • 在这段代码中,由于A,B的内部又有pointer智能指针相互引用,导致最终a,b被销毁后,智能指针的引用计数不为0,所以依然无法被析构。如下图所示:
    C++技能点之智能指针(一),第2张
    内部的智能指针相互引用

    C++技能点之智能指针(一),第3张
    a,b被销毁后
  • 解决这个问题的办法就是将shared_ptr替换成weak_ptr,因为shared_ptr是强引用,而weak_ptr是弱引用,弱引用不会引起引用计数的增加。如下图所示:
    C++技能点之智能指针(一),第4张
    weak_ptr是弱引用

四、std::unique_ptr

  • 相比于前两种智能指针而言,std::unique_ptr的特性是独占资源,不允许和其它指针共享。但是std::unique_ptr支持移动语义,即std::move。来看以下一段代码,来自learncpp [4]
#include <iostream>
#include <memory> 

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    std::unique_ptr<Resource> res1(std::make_unique<Resource>()); 
    // std::unique_ptr<Resource> res2 = res1;  // 非法,因为拷贝构造被delete了
    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");

    std::unique_ptr<Resource> res2(std::move(res1)); 
    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");

    return 0;
}
  • 首先要注意的是和std::make_shared不同,std::make_unique是在C++14实现的,据说是因为C++标准委员会忘了...[3]
  • 其次简单分析下代码可知,res1指向了一块资源,此时的res1不为空。但是std::move将其移动给了res2,此时res1为空,res2不为空。输出结果如下:
Resource acquired 
res1 is not null 
res1 is null 
res2 is not null 
Resource destroyed 

下一篇:C++技能点之智能指针(二)

参考

[1] C++ 智能指针:原理与实现【这篇写的很好】
[2] cppreference
[3] 现代C++教程:高速上手C++11/14/17/20【C++的新特性通俗易懂】
[4] learncpp


https://www.xamrdz.com/lan/5s71848549.html

相关文章: