基于C++,手把手教你实现智能指针管理功能

2023-11-24 09:52:52 浏览数 (1)

一、基础概念

谈到C ,绕不开的一个特性是智能指针,智能指针见字如面:有两个概念:一个是指针,一个是“智能”。

  • 和类似指针的相同使用方式使用他,它可以托管任何使用“new”创建的对象。
  • “智能”指的是使用者不需要关注什么时候是否需要删除这块new出来的内存,内存管理由指针内部自动删除

1.1 范畴

C 11智能指针的范畴包括

  • 类型unique_ptr, shared_ptr和weak_ptr
  • 配到的function和模板class

1.2 解决问题和实现机制

智能指针解决的问题是在它出现之前,object的拥有者释放问题。它利用了编译器一个基本机制:class的对象销毁时,析构函数destructor一定会被call到。

使用方式首先include头文件<memory>

代码语言:javascript复制
#include <memory>

1.3 类型

1.3.1 shared_ptr

它是一种拥有引用计数的智能指针类型,当引用计数减为0时,真实内存自动被释放。shared指的是ownership拥有权被多个使用者share。比如说下图,A container和B container同时分享了对象X1,X2和X3的拥有权,当A container移除X1的拥有权时,我们需要检查X1内存是否能被释放,取决于X1的使用者是否为0,在这个例子中需要考虑Container B是否还有在使用X1.

1.3.2 weak_ptr

考虑到一种引用关系图:环形引用,如下图例子,3个sp智能指针互相引用,形成一个环形依赖,此时释放container,那么3个sp永远不会被释放,因为他们的被引用计数大于0。C 11引入了weak_ptr用来出来三个sp之间的相互引用,它用来表明引用关系,但是不用于管理对象内存上。

这里你仍然有个疑问,weak_ptr不能用普通的pointer替代,回答这个问题前,我们来挖掘看看weak_ptr到底还有其他的收益吗

以下这个例子显示了3个shared指针sp1,sp2和sp3拥有了一块对象,接着还有两个wp1和wp2也拥有了这块对象。当讨论到对象是否能被释放,我们遵循一个标准:shared_count是否为0决定了删除managed_object,weaked_ptr和shared_ptr是否同时为0决定删除manager_object。weak_ptr的作用就是在这里监听着是否还有managed object,如果还有,那么下一个新来的shared_ptr还可以继续使用。

二、实战:

2.1 shared_ptr的例子

2.1.1 使用方式一

使用shared_ptr<typename class> name(new object),因为C 是不断发展的,这里展示了一个C 17的进阶版本,可以用shared_ptr管理array

代码语言:javascript复制
//需要支持使用c  17编译器
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

   int val_ { 0 };
};

int main() {
    std::shared_ptr<Test[]> ptr(new Test[3]);
    std::cout << "finishing main...n";
}

C 20支持这种写法

代码语言:javascript复制
auto ptr = std::make_shared<Test[]>(3);

2.1.2 使用方式二

使用std::make_shared,那么两种使用方式有区别吗,答案是sp和sp2的内存分配过程不一样,sp需要经历两次new过程:一次new Test,一次new 引用计数。而使用make_shared方式,两者内存的地址更接近,在系统底层上可能只需要向系统申请分配一次大内存块就行。

代码语言:javascript复制
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

   int val_ { 0 };
};

int main() {
    std::shared_ptr<Test> sp(new Test());
    std::shared_ptr<Test> sp2 = std::make_shared<Test>();
    std::cout << "finishing main...n";
}

基本上99%场合优先使用maske_shared,那么这里的1%特例是什么场景?

这里有种weak_ptr和shared_ptr共同使用的例子,之前所说的weak_ptr保存着weak_counter,这个weak_counter和shared_pointer的计数地址相近,也就是说即使shared_pointer的shared_count为0,但是由于weak_count和shared_count的内存是同一块,这使得shared_count的内存得不到释放。

2.1.3 shared_ptr和多态

C 多态是重要功能,结合shared_ptr,也可以有多态

代码语言:javascript复制
#include <memory>
#include <iostream>

class Base {
  public:
   Base() { std::cout << "Base::Basen"; }
   ~Base() { std::cout << "Base::~Base destructorn"; }

   int val_ { 0 };
};

class Derived : public Base {
  public:
   Derived() { std::cout << "Derived::Derivedn"; }
   ~Derived() { std::cout << "Derived::~Derived destructorn"; }

   //int val_ { 0 };
};


int main() {
    std::shared_ptr<Derived> dp1(new Derived);  // 调用了移动构造函数,并读取了createTest的返回rvalue值

    std::shared_ptr<Base> bp1 = dp1;  // 多态例子1
    std::shared_ptr<Base> bp2(dp1);  // 多态例子2
    std::shared_ptr<Base> bp3(new Derived);  // 多态例子3
    std::cout << "finishing main...n";
}

2.1.4类型转换

使用c 11提供的 static_pointer_cast、const_pointer_case和dynamic_pointer_cast进行基类->派生类的类型转换

代码语言:javascript复制
#include <memory>
#include <iostream>

class Base {
  public:
   Base() { std::cout << "Base::Basen"; }
   ~Base() { std::cout << "Base::~Base destructorn"; }

   int val_ { 0 };
};

class Derived : public Base {
  public:
   Derived() { std::cout << "Derived::Derivedn"; }
   ~Derived() { std::cout << "Derived::~Derived destructorn"; }

   //int val_ { 0 };
};


int main() {
    std::shared_ptr<Derived> dp1(new Derived);  // 调用了移动构造函数,并读取了createTest的返回rvalue值

    std::shared_ptr<Base> bp1 = dp1; 

    std::shared_ptr<Derived> dp2;
    dp2 = std::static_pointer_cast<Derived>(bp1);  // 类型转换
    std::cout << "finishing main...n";
}

2.2 weak_ptr

weak_ptr不能使用operator*和operator->,而且也没有get()函数。weak_ptr一般和shared_ptr配套使用。

2.2.1 weak_ptr的初始化和赋值

weak_ptr默认是空的,不指到任何对象。它可以接受shared_ptr或者另一个weak_ptr的赋值。

代码语言:javascript复制
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

   int val_ { 0 };
};

int main() {
    std::shared_ptr<Test> sp(new Test());
    std::weak_ptr<Test> wp1(sp);  // construct wp1 from a shared_ptr
    std::weak_ptr<Test> wp2;  // an empty weak_ptr - points to nothing
    wp2 = sp;  // wp2 now points to the new Test
    std::weak_ptr<Test> wp3(wp2);  // construct wp3 from a weak_ptr
    
    wp1.reset();  // set to empty state in which it's pointing to nothing
    std::cout << "finishing main...n";
}

2.2.2 使用weak_ptr

weak_ptr不能直接访问对象,只能先用lock()函数获取到shared_ptr,lock()函数会检验对象是否还存在。如果不存在,会得到空的shared_ptr。

weak_ptr默认是空的,不指像任何对象。它可以接受shared_ptr或者另一个weak_ptr的赋值。

代码语言:javascript复制
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

    void doSomeThing() { std::cout << "Test::doSomeThingn";}
   int val_ { 0 };
};

int main() {
    std::shared_ptr<Test> sp(new Test());
    std::weak_ptr<Test> wp2(sp);  // construct wp2 from a shared_ptr
    
    std::shared_ptr<Test> sp2 = wp2.lock();  // get shared_ptr from weak_ptr
    
    if(sp2) sp2->doSomeThing();
    else std::cout << "The Test is gone!";
    std::cout << "finishing main...n";
}

使用lock()得到的shared_ptr是一个全新的,sp2并不是sp。取到sp2务必检查shared_ptr管理的object还有效。

2.3 unique_ptr

unique_ptr像是shared_ptr的对立面,因为unique_ptr只运行自身的计数是1或者0,但是因为无需维护引用计数增减,所以他的内部可以设计地更简单,开销更小。

unique意味着拷贝构造和拷贝赋值函数是不被运行调用(内部通过=delete实现)。所以unique_ptr也不允许被值传递传参

代码语言:javascript复制
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

   int val_ { 0 };
};

int main() {
    std::unique_ptr<Test> ptr(new Test);  // Right
    //std::unique_ptr<Test[]> p2(p1);  // Wrong
    std::unique_ptr<Test> p3;
    //p3=p1;  // Wrong
    std::cout << "finishing main...n";
}

一些有用的使用方式,可以使用if(p)判断一个unique_ptr是否拥有某个对象,使用reset()成员函数或者=nullptr进行删除。

unique_ptr意味着如果需要进行对象的转移,可以使用move语义进行。比如说可以这样使用

代码语言:javascript复制
#include <memory>
#include <iostream>

struct Test {
   Test() { std::cout << "Test::Testn"; }
   ~Test() { std::cout << "Test::~Test destructorn"; }

   int val_ { 0 };
};

//这里用到了return value是rvalue类型,所以这里可以利用return特性进行unique_ptr的move和reassign
std::unique_ptr<Test> createTest()
{
   std::unique_ptr<Test> localPtr(new Test);
   return localPtr;  // localPtr会交出owership
}

int main() {
    std::unique_ptr<Test> p1(createTest());  // 调用了移动构造函数,并读取了createTest的返回rvalue值
    std::unique_ptr<Test> p2;
    p2 = createTest();  // 调用了移动赋值函数,并读取了createTest的返回rvalue值
    
    std::unique_ptr<Test> p3;
    p3 = std::move(p1);  // 使用了std::move函数
    std::cout << "finishing main...n";
}
c++

0 人点赞