1、unique_ptr

template<typename T>
class MyUniquePtr
{
public:
   explicit MyUniquePtr(T* ptr = nullptr)
        :mPtr(ptr)
    {}
~MyUniquePtr()
{
    if(mPtr)
        delete mPtr;
}

MyUniquePtr(MyUniquePtr &amp;&amp;p) noexcept;
MyUniquePtr&amp; operator=(MyUniquePtr &amp;&amp;p) noexcept;

MyUniquePtr(const MyUniquePtr &amp;p) = delete;
MyUniquePtr&amp; operator=(const MyUniquePtr &amp;p) = delete;

T* operator*() const noexcept {return mPtr;}
T&amp; operator-&gt;()const noexcept {return *mPtr;}
explicit operator bool() const noexcept{return mPtr;}

void reset(T* q = nullptr) noexcept
{
    if(q != mPtr){
        if(mPtr)
            delete mPtr;
        mPtr = q;
    }
}

T* release() noexcept
{
    T* res = mPtr;
    mPtr = nullptr;
    return res;
}
T* get() const noexcept {return mPtr;}
void swap(MyUniquePtr &amp;p) noexcept
{
    using std::swap;
    swap(mPtr, p.mPtr);
}

private: T* mPtr; };

template<typename T> MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept { swap(*this, p); return *this; }

template<typename T> MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr) { p.mPtr == NULL; }

key

1、noexcept关键字

该关键字能够告知标准库,本函数不会产生异常,如果未标记noexcept,标准库将产生一个额外的操作,增加了开销。移动赋值操作符以及部分接口函数的声明和定义中需要标记noexcept.

2、缺失bool值转换功能

观察IO类、unique_ptr、shared_ptr等资源管理相关的类,它们一般都会重新实现:

<code>explicit operation bool() const noexcept</code>

以便能直接将对象放置于条件语句中进行判断。

int main()
{
    std::unique_ptr<int> ptr(new int(42));
if (ptr) std::cout &lt;&lt; "before reset, ptr is: " &lt;&lt; *ptr &lt;&lt; '\n';
ptr.reset();
if (ptr) std::cout&lt;&lt; "after reset, ptr is: " &lt;&lt; *ptr &lt;&lt; '\n';

}

explicit关键字不能少,否则可能出现隐式转换,导致如下问题:

std::unique_ptr<int> p1(new int(13));
std::unique_ptr<int> p2(new int(14));

if(p1 == p2) { //p1 p2 都会被转换为bool值,都为true,因此结果是两者相等。 std::cout << “p1 is equal p2” << endl; } //输出: p1 is equal p2

这时p1==p2将返回true 编译器为了使两个对象能够互相比较,就会把他们都转换为bool型,从而导致错误。

3、三/五法则和阻止拷贝

释放动态内存---->需要自定义析构函数,而根据三/五法则:

  • 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝构造函数和拷贝赋值运算符。

总之,不能让编译器产生合成版本的拷贝构造函数和拷贝赋值运算符。 而unique_ptr要求独占资源,不允许拷贝,因此,上述代码中将拷贝赋值操作删除(通过delete关键字),就不会产生合成版本的了。

4、隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数(converting constructor)

是否允许隐式的类类型转换,是由类的语义和上下文环境决定的。 同样的,使用explicit可以抑制这种转换。也由此,unique_ptr只能进行直接初始化。

5、移动构造、移动赋值及自赋值问题

unique_ptr不可拷贝,但是可以移动。这是它的重要特征,因此,绝对不能忘记自定义移动构造函数和移动赋值运算符。 而重新实现移动赋值运算符时,要记得考虑自赋值问题。上述代码考虑了该问题。

优化:使用swap函数来实现operator=(移动赋值运算符),巧妙解决自赋值问题。且相比判断*this == p而言,提高了效率。

template<typename T>
MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
{
//交换本对象和右值p的内容
    this->swap(*this, p);//p.mPtr现在指向本对象曾经指向的内存
    return *this; //返回后,右值p将被销毁,从而指向delete p.mPtr
}

为什么*this == p版本不好:

  • 1.自赋值检查: 自赋值情况的发生频率并不高,而无论何种情况都做自赋值检查,更加耗时。

为什么swap版本更好:

  • 无需再做自赋值检查。由于自赋值检查发生频率很低,因此,所有在自赋值情况下复制相等的指针,其所浪费的时间并不多。
  • 代码复用。复用了swap函数,精简了代码.
  • 使用交换技术,能够延后针对unique_ptr内置指针成员的delete操作。这意味着临时的unique_ptr超出作用域之前,是潜在的可再次使用的。当其超出作用域后,unique_ptr的析构函数会将其正确销毁。

2、shared_ptr

todo