auto_ptr

C++98/03 :std::auto_ptr

基本用法

#include 
int main()
{
    //方法1
    std::auto_ptr sp1(new int(8));
    //方法2
    std::auto_ptr sp2;
    sp2.reset(new int(8));

    return 0;
}

智能指针对象sp1和sp2均持有一个在堆上分配的int对象,值都是8,这两块堆内存都在sp1和sp2释放时得到释放。这是std::auto_ptr的基本用法。

缺陷:

std::auto_ptr真正容易让人误用的地方是其不常用的复制语义,即当复制一个std::auto_ptr对象时(拷贝复制或operator =复制),原对象所持有的堆内存对象也会转移给复制出来的对象。

#include 
#include 
using namespace std;
int main()
{
   //测试拷贝构造
   std::auto_ptr sp1(new int(8));
   std::auto_ptr sp2(sp1);
   if (sp1.get() != NULL)
   {
     std::cout << "sp1 is not empty." << std::endl;
   }
   else
   {
      std::cout << "sp1 is empty." << std::endl;
   }
   if (sp2.get() != NULL)
   {   
      std::cout << "sp2 is not empty." << std::endl;
   }
   else
   {
       std::cout << "sp2 is empty." << std::endl;
    }
     //测试赋值构造
    std::auto_ptr sp3(new int(8));
    std::auto_ptr sp4;
    sp4 = sp3;
    if (sp3.get() != NULL)
    {
        std::cout << "sp3 is not empty." << std::endl;
    }
    else
    {
        std::cout << "sp3 is empty." << std::endl;
    }
    if (sp4.get() != NULL)
    {
        std::cout << "sp4 is not empty." << std::endl;
    }
    else
    {
        std::cout << "sp4 is empty." << std::endl;
    }
    return 0;
}

所以我们不能使用这样的代码

std::vector> v1;

当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们希望看到的,可能会造成一些意想不到的错误。

以史为鉴,作为std::auto_ptr的替代者std::unique_ptr吸取了这个经验教训

正因为std::auto_ptr的设计存在如此重大缺陷,C++11标准在充分借鉴和吸收了boost库中智能指针的设计思想,引入了三种类型的智能指针,即std::unique_ptr、std::shared_ptr和std::weak_ptr。

C++ 11 std::unique_ptr

定义:

std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是1,std::unique_ptr对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个std::unique_ptr对象:

//初始化方式1
std::unique_ptr sp1(new int(123));

//初始化方式2
std::unique_ptr sp2;
sp2.reset(new int(123));

//初始化方式3
std::unique_ptr sp3 = std::make_unique(123);
推荐使用初始化方式3:为什么?
参考[C++11 make_shared](https://www.jianshu.com/p/03eea8262c11)

其中std::make_unique 是C++14才有 C++11没有 ,但是可以自己手动写
代码如下:

template

std::unique_ptr make_unique(Ts&& ...params)
{
    return std::unique_ptr(new T(std::forward(params)...));
}

鉴于std::auto_ptr的前车之鉴,std::unique_ptr禁止复制语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符(operator =)被标记为 =delete。

template 
class unique_ptr
{
//省略其他代码..
//拷贝构造函数和赋值运算符被标记为delete
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
std::unique_ptr sp1(std::make_unique(123));;

//以下代码无法通过编译

//std::unique_ptr sp2(sp1);

std::unique_ptr sp3;

//以下代码无法通过编译

//sp3 = sp1;

禁止复制语义也存在特例,即可以通过一个函数返回一个std::unique_ptr:

#include 
using namespace std;
std::unique_ptr func(int val)
{
    std::unique_ptr up(new int(val));
    return up;
}
int main()
{
    std::unique_ptr sp1 = func(123);
    return 0;
}

既然std::unique_ptr不能复制,那么如何将一个std::unique_ptr对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

#include 
int main()
{
    std::unique_ptr sp1(std::make_unique(123));
    std::unique_ptr sp2(std::move(sp1));
    std::unique_ptr sp3;
    sp3 = std::move(sp2);
    return 0;
}

以上代码利用std::move将sp1持有的堆内存(值为123)转移给sp2,再把sp2转移给sp3。最后,sp1和sp2不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的std::move操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而std::unique_ptr正好实现了这二者,以下是实现伪码:

template
class unique_ptr
{
    //其他函数省略...
public:
    unique_ptr(unique_ptr&& rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放        
        rhs.m_pT = nullptr;
    }
    unique_ptr& operator=(unique_ptr&& rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
        return *this;
    }
private:
    T*    m_pT;
};

这是std::unique_ptr具有移动语义的原因

std::unique_ptr不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:

#include 
#include 
using namespace std;
int main()
{
    //创建10个int类型的堆对象
    //形式1
    std::unique_ptr sp1(new int[10]);
    
    //形式2
    std::unique_ptr sp2;
    sp2.reset(new int[10]);
    //形式3
    std::unique_ptr sp3(std::make_unique(10));

    for (int i = 0; i < 10; ++i)
    {
        sp1[i] = i;
        sp2[i] = i;
        sp3[i] = i;
    }

    for (int i = 0; i < 10; ++i)
    {
        std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
    }

    return 0;
}

程序执行结果如下:

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o test_unique_ptr_int test_unique_ptr_int.cpp 
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test
test1_error_ex       test_left            test_unique_ptr_int  
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test
test1_error_ex       test_left            test_unique_ptr_int  
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_unique_ptr_int 
0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# 

std::shared_ptr和std::weak_ptr也可以持有一组堆对象,用法与std::unique_ptr相同

Deletor:自定义智能指针对象持有的资源的释放函数
默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用delete 或者delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个Socket类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以std::unique_ptr为例:

#include 
#include 
using namespace std;
class Socket
{
public:
    Socket()
    {

    }

    ~Socket()
    {

    }

    //关闭资源句柄
    void close()
    {

    }
};

int main()
{
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        //TODO: log日志
        delete pSocket;
    };

    std::unique_ptr spSocket(new Socket(), deletor);

    return 0;
}

自定义std::unique_ptr的资源释放函数其规则是:

std::unique_ptr

其中T是你要释放的对象类型,DeletorFuncPtr是一个自定义函数指针。

上面写的有点麻烦,可以通过类型推导decltype进行简化

std::unique_ptr spSocket(new Socket(), deletor);

C++ std::shared_ptr

std::unique_ptr对其持有的资源具有独占性,而std::shared_ptr持有的资源可以在多个std::shared_ptr之间共享,每多一个std::shared_ptr对资源的引用,资源引用计数将增加1,每一个指向该资源的std::shared_ptr对象析构时,资源引用计数减1,最后一个std::shared_ptr对象析构时,发现资源计数为0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作std::shared_ptr引用的对象是安全的)。std::shared_ptr提供了一个use_count()方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr用法和std::unique_ptr基本相同。

初始化:

//初始化方式1
std::shared_ptr sp1(new int(123));

//初始化方式2
std::shared_ptr sp2;
sp2.reset(new int(123));

//初始化方式3
std::shared_ptr sp3;
sp3 = std::make_shared(123);

和std::unique_ptr一样,应该优先使用std::make_shared去初始化一个std::shared_ptr对象。

看下面代码

#include 
#include 
using namespace std;
class A
{
public:
    A()
    {
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        std::cout << "A destructor" << std::endl;
    }
};

int main()
{
    {
        //初始化方式1
        std::shared_ptr sp1(new A());

        std::cout << "use count: " << sp1.use_count() << std::endl;

        //初始化方式2
        std::shared_ptr sp2(sp1);
        std::cout << "use count: " << sp1.use_count() << std::endl;

        sp2.reset();
        std::cout << "use count: " << sp1.use_count() << std::endl;

        {
            std::shared_ptr sp3 = sp1;
            std::cout << "use count: " << sp1.use_count() << std::endl;
        }

        std::cout << "use count: " << sp1.use_count() << std::endl;
    }
       
    return 0;
}

结果如下:

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_shared_ptr_use_count
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor

std::enable_shared_from_this

有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr对象给外部使用,C++新标准也为我们考虑到了这一点,有如此需求的类只要继承自std::enable_shared_from_this模板对象即可。用法如下:

#include 
#include 
using namespace std;
class A : public std::enable_shared_from_this
{
public:
    A()
    {
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        std::cout << "A destructor" << std::endl;
    }

    std::shared_ptr getSelf()
    {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr sp1(new A());

    std::shared_ptr sp2 = sp1->getSelf();

    std::cout << "use count: " << sp1.use_count() << std::endl;

    return 0;
}

运行结果如下:

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./test_shared_ptr_from_this 
A constructor
use count: 2
A destructor
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# 

上述代码中,类A继承自std::enable_shared_from_this并提供一个getSelf()方法返回自身的std::shared_ptr对象,在getSelf()中调用shared_from_this()即可。

std::enable_shared_from_this用起来比较方便,但是也存在很多不易察觉的陷阱。

陷阱一:不应该共享栈对象的 this 给智能指针对象

//其他相同代码省略...

int main()
{
    A a;

    std::shared_ptr sp2 = a.getSelf();

    std::cout << "use count: " << sp2.use_count() << std::endl;

    return 0;
}

运行修改后的代码会发现程序在std::shared_ptr sp2 = a.getSelf();产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过shared_from_this()将该对象交由智能指针对象管理。切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)

陷阱一:不应该共享栈对象的 this 给智能指针对象

#include 
#include 

class A : public std::enable_shared_from_this
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃

        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        m_i = 0;

        std::cout << "A destructor" << std::endl;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr  m_SelfPtr;

};

int main()
{
    {
        std::shared_ptr spa(new A());
        spa->func();
    }

    return 0;
}

结果如下

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./shared_ptr_trap1 
A constructor
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# 

在程序的整个生命周期内,只有A类构造函数的调用输出,没有A类析构函数的调用输出,这意味着new出来的A对象产生了内存泄漏了!

我们来分析一下new出来的A对象为什么得不到释放。当程序执行到42行后,spa出了其作用域准备析构,在析构时其发现仍然有另外的一个std::shared_ptr对象即A::m_SelfPtr 引用了A,因此spa只会将A的引用计数递减为1,然后就销毁自身了。现在留下一个矛盾的处境:必须销毁A才能销毁其成员变量m_SelfPtr,而销毁A必须先销毁m_SelfPtr。这就是所谓的std::enable_shared_from_this的循环引用问题。
我们在实际开发中应该避免做出这样的逻辑设计,这种情形下即使使用了智能指针也会造成内存泄漏。也就是说一个资源的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给该资源本身来管理。

C++ std::weak_ptr

std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助std::shared_ptr工作。

std::weak_ptr可以从一个std::shared_ptr或另一个std::weak_ptr对象构造,std::shared_ptr可以直接赋值给std::weak_ptr ,也可以通过std::weak_ptr的lock()函数来获得std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr可用来解决std::shared_ptr相互引用时的死锁问题,即两个std::shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0, 资源永远不会释放。

初始化:

#include 
#include 

int main()
{
    //创建一个std::shared_ptr对象
    std::shared_ptr sp1(new int(123));
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过构造函数得到一个std::weak_ptr对象
    std::weak_ptr sp2(sp1);
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过赋值运算符得到一个std::weak_ptr对象
    std::weak_ptr sp3 = sp1;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
    std::weak_ptr sp4 = sp2;
    std::cout << "use count: " << sp1.use_count() << std::endl;

    return 0;
}

运行结果:

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o weak_ptr_test weak_ptr_test.cpp root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./weak_ptr_test 
use count: 1
use count: 1
use count: 1
use count: 1

无论通过何种方式创建std::weak_ptr都不会增加资源的引用计数,因此每次输出引用计数的值都是1。

既然,std::weak_ptr不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr提供了一个expired()方法来做这一项检测,返回true,说明其引用的资源已经不存在了;返回false,说明该资源仍然存在,这个时候可以使用std::weak_ptr 的lock()方法得到一个std::shared_ptr对象然后继续操作资源,以下代码演示了该用法:

//tmpConn_是一个std::weak_ptr对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
    return;

std::shared_ptr conn = tmpConn_.lock();
if (conn)
{
    //对conn进行操作,省略...
}

既然使用了std::weak_ptr的expired()方法判断了对象是否存在,为什么不直接使用std::weak_ptr对象对引用资源进行操作呢?
实际上这是行不通的,std::weak_ptr类没有重写operator->和operator方法,因此不能像std::shared_ptr或std::unique_ptr一样直接操作对象,同时std::weak_ptr类也没有重写operator bool()操作,因此也不能通过std::weak_ptr对象直接判断其引用的资源是否存在:

#include 
#include 
class A
{
public:
    void doSomething()
    {
        std::cout<< "do something" << std::endl;
    }
};

int main()
{    
    std::shared_ptr sp1(new A());
    
    std::weak_ptr sp2(sp1);

    //正确代码
    if (sp1)
    {
        //正确代码
        sp1->doSomething();
        (*sp1).doSomething();
    }

    //正确代码
    if (!sp1)
    {

    }

    //错误代码,无法编译通过
    //if (sp2)
    //{
    //    //错误代码,无法编译通过
    //    sp2->doSomething();
    //    (*sp2).doSomething();
    //}

    //错误代码,无法编译通过
    //if (!sp2)
    //{

    //}

    return 0;
}

结果:

root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o weak_ptr_test2 weak_ptr_test2.cpp 
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./weak_ptr_test2do something
do something
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# 

之所以std::weak_ptr不增加引用资源的引用计数来管理资源的生命周期,是因为即使它实现了以上说的几个方法,调用它们仍然是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这样可能会造成比较棘手的错误和麻烦。

因此,std::weak_ptr的正确使用场景是那些资源如果可用就使用,如果不可用则不使用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session对象(会话对象)利用Connection对象(连接对象)提供的服务来进行工作,但是Session对象不管理Connection对象的生命周期,Session管理Connection的生命周期是不合理的,因为网络底层出错会导致Connection对象被销毁,此时Session对象如果强行持有Connection对象则与事实矛盾。

std::weak_ptr的应用场景,经典的例子是订阅者模式或者观察者模式中。

智能指针的大小

一个std::unique_ptr对象大小与裸指针大小相同(即sizeof(std::unique_ptr) == sizeof(void*)),而std::shared_ptr的大小是std::unique_ptr的一倍。以下是我在gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) 上测试的结果

#include 
#include 
#include 

int main()
{
    std::shared_ptr sp0;
    std::shared_ptr sp1;
    sp1.reset(new std::string());
    std::unique_ptr sp2;
    std::weak_ptr sp3;
   
    std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
    std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
    std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
    std::cout << "sp3 size: " << sizeof(sp3) << std::endl;
  
    return 0;
}

结果

gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) 
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# g++ -o auto_ptr_size auto_ptr_size.cpp root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# ./auto_ptr_size 
sp0 size: 16
sp1 size: 16
sp2 size: 8
sp3 size: 16
root@iZuf65i08ucxtob67o6urwZ:/usr/myC++/shared_ptr# 

在32位机器上,std::unique_ptr占 4 字节,std::shared_ptr和std::weak_ptr占8字节;在64位机器上,std_unique_ptr占8字节,std::shared_ptr和std::weak_ptr占16字节。也就是说,std_unique_ptr的大小总是和原始指针大小一样,std::shared_ptr和std::weak_ptr大小是原始指针的2倍。

智能指针使用事项:

C++新标准提倡的理念之一是不应该再手动调用delete或者free函数去释放内存了,而应该把它们交给新标准提供的各种智能指针对象。C++新标准中的各种智能指针是如此的实用与强大,在现代C++ 项目开发中,读者应该尽量去使用它们。智能指针虽然好用,但稍不注意,也可能存在许多难以发现的bug,这里我根据经验总结了几条:

1.一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作;

#include 

class Subscriber
{
 //省略具体实现
};

int main()
{    
    Subscriber* pSubscriber = new Subscriber();

    std::unique_ptr spSubscriber(pSubscriber);

    delete pSubscriber;

    return 0;
}

记住,一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。当然,除了std::weak_ptr,std::unique_ptr和std::shared_ptr都提供了获取原始指针的方法——get()函数。

int main()
{    
    Subscriber* pSubscriber = new Subscriber();

    std::unique_ptr spSubscriber(pSubscriber);
 
 //pTheSameSubscriber和pSubscriber指向同一个对象
    Subscriber* pTheSameSubscriber= spSubscriber.get();

    return 0;
}

2. 分清楚场合应该使用哪种类型的智能指针

通常情况下,如果你的资源不需要在其他地方共享,那么应该优先使用std::unique_ptr,反之使用std::shared_ptr,当然这是在该智能指针需要管理资源的生命周期的情况下;如果不需要管理对象的生命周期,请使用std::weak_ptr。

3. 认真考虑,避免操作某个引用资源已经释放的智能指针

#include 
#include 

class T
{
public:
    void doSomething()
    {
        std::cout << "T do something..." << m_i << std::endl;
    }

private:
    int     m_i;
};

int main()
{    
    std::shared_ptr sp1(new T());
    const auto& sp2 = sp1;

    sp1.reset();

    //由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
    sp2->doSomething();

    return 0;
}

上述代码中,sp2是sp1的引用,sp1被置空后,sp2也一同为空。这时候调用sp2->doSomething(),sp2->(即 operator->)在内部会调用get()方法获取原始指针对象,这时会得到一个空指针(地址为0),继续调用doSomething()导致程序崩溃。

你可能感兴趣的:(auto_ptr)