c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)

智能指针是 “行为像指针”的对象。

背景:为了防止资源泄漏,请使用RAII对象(资源获得时机便是初始化时机 Resource Acquisition Is Initialization,在构造函数里面获得资源,并在析构函数里面释放资源。

 智能指针的作用是:能够处理内存泄漏问题和空悬指针问题。

分为auto_ptr、unique_ptr、shared_ptr和weak_ptr四种

引用头文件:#include <memory>

auto_ptr

就是动态分配对象以及当对象不再需要时自动执行清理。

使用std::auto_ptr,要#include <memory>。

构造和析构

1. 将已存在的指向动态内存的普通指针作为参数来构造

int*p=new int(0);

auto_ptr<int> ap(p);

2. 直接构造智能指针

auto_ptr< int > api( new int( 0 ) );

因为auto_ptr析构的时候肯定会删除他所拥有的那个对象,两个auto_ptr不能同时拥有同一个对象。

因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。

拷贝构造与赋值

因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权。即他们会变成null.

这种情况较为隐蔽的情形出现在将auto_ptr作为函数参数按值传递,因为在函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这样,传入的实参auto_ptr就失去了其对原对象的所有权,而该对象会在函数退出时被局部auto_ptr删除。所以警惕把智能指针作为函数参数。

引用或指针时,不会存在上面的拷贝过程。结论:const reference是智能指针作为参数传递的底线。

初始化

对于智能指针,因为构造函数有默认值0,我们可以直接定义空的auto_ptr如下:

auto_ptr< int > p_auto_int; //不指向任何对象

auto_ptr常用的成员函数

1) get()

返回auto_ptr指向的那个对象的内存地址。

2) reset()

重新设置auto_ptr指向的对象。会先释放掉前一个绑定对象的内存,再重新绑定新的对象。

3) release()

返回auto_ptr指向的那个对象的内存地址,并释放对这个对象的所有权。

用此函数初始化auto_ptr时可以避免两个auto_ptr对象拥有同一个对象的情况(与get函数相比)

延伸:std::auto_ptr为什么被废弃,取而代之的是

参考 :C++智能指针:std::auto_ptr为什么被废弃 - 知乎

shared_ptr 

C++11提供的一种智能指针类,有两个成员变量:一个是指向变量的指针;一个是资源被引用的次数。引用次数加减操作内部自动加锁解锁。所以shared_ptr 属于引用计数型智慧指针,并采用原子操作保证该区域中的引用计数值被互斥地访问能够持续跟踪有多少对象指向某笔资源,并在无人指向它时自动删除该资源。

shared_ptr中除了有一个指针,指向所管理数据的地址。还有一个指针执行一个控制块的地址,里面存放了所管理数据的数量(常说的引用计数)、weak_ptr的数量、删除器、分配器等。也就是说对于引用计数这一变量的存储,是在堆上的,多个shared_ptr的对象都指向同一个堆地址。在多线程环境下,shared_ptr的引用计数是线程安全的.

延伸注意:多线程代码操作的是同一个shared_ptr的对象,不是线程安全的,应该要进行加锁操作。
其实原理和其他正常的对象一样,多线程共享对象不是原子操作,不能自动实现互斥功能。

//前提
shared_ptr<foo> p1;                 //线程A的局部变量
shared_ptr<foo> p2(new foo);        //线程A和线程B所共享
shared_ptr<foo> p3(new foo);        //线程B的局部变量

//操作
然后线程A先执行语句:p1=p2; //在执行p1=p2时,步骤1:先改变ptr的指向,步骤2:然后才修改引用计数。
现在线程B开始执行p2=p3;//在执行p2=p3时,步骤1:先改变ptr的指向,步骤2:然后才修改引用计数。


可能会出现的问题
1.在线程A执行完步骤一时,还没来得及执行步骤二,就轮到线程B来执行。
2.线程B开始执行p2=p3,并且没有被打断,也就是说步骤一二都完成。导致p2原来的第一个资源的引用计数已经为0,所以会销毁该资源,也就是说,步骤二执行完之后,p1的ptr是一个悬空指针

与auto_ptr不同之处:两个shared_ptr能同时拥有同一个对象,即可以通过copy构造函数或copy assignment复制他们。

如 :std::auto_ptr 、std::tr1::shared_ptr

使用原始指针创建 shared_ptr 对象

std::shared_ptr<int> p1(new int());

创建空的 shared_ptr 对象

std::shared_ptr<int> p1 = std::make_shared<int>();
检查 shared_ptr 对象的引用计数

p1.use_count();

分离关联的原始指针
1.不带参数的reset(): p1.reset();    放弃当前的。相当于置0,即 p1.use_count() = 0;引用次数减1.

2.带参数的reset():p1.reset(new int(34));  放弃原来的—计数变量会减1;管理新的—又会加1,有时候会发现不变,抵消了

3.使用nullptr重置: p1 = nullptr; 

    std::shared_ptr<int> p1 = std::make_shared<int>();
    *p1 = 78;
    std::cout << "p1 = " << *p1 << std::endl; // 输出78

    // 打印引用个数:1
    std::cout << "p1 Reference count = " << p1.use_count() << std::endl;

    // 第2个 shared_ptr 对象指向同一个指针
    std::shared_ptr<int> p2(p1);

    // 下面两个输出都是:2
    std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
    std::cout << "p1 Reference count = " << p1.use_count() << std::endl;

    // 比较智能指针,p1 等于 p2
    if (p1 == p2) {
        std::cout << "p1 and p2 are pointing to same pointer\n";
    }

    std::cout << "Reset p1 " << std::endl;

    // 无参数调用reset,无关联指针,引用个数为0 
    p1.reset();
    std::cout << "p1 Reference Count = " << p1.use_count() << std::endl; =》0
    std::cout << "p2 Reference count = " << p2.use_count() << std::endl; =》1
    // 带参数调用reset,引用个数为1
    p1.reset(new int(11));
    std::cout << "p1  Reference Count = " << p1.use_count() << std::endl; =》1
    std::cout << "p2 Reference count = " << p2.use_count() << std::endl; =》1

    // 把对象重置为NULL,引用计数为0
    p1 = nullptr;
    std::cout << "p1  Reference Count = " << p1.use_count() << std::endl; =》0
    std::cout << "p2 Reference count = " << p2.use_count() << std::endl;  =》1
在c++ 11中,自定义删除器 Deleter(主要用于动态数组)
    析构函数中删除内部原始指针,默认调用的是delete()函数而不是delete[ ],因此不能用在动态数组。

我们可以将回调函数传递给 shared_ptr 的构造函数,该构造函数将从其析构函数中调用以进行删除,即下面3种办法:

1.// 自定义删除器
void deleter(Sample * x)
{
    std::cout << "DELETER FUNCTION CALLED\n";
    delete[] x;
}
// 构造函数传递自定义删除器指针
std::shared_ptr<Sample> p3(new Sample[12], deleter);

2.或者更智能的写法:

std::shared_ptr<int> ptr(new Sample[12], [](Sample * x) {delete[] x;});
std::shared_ptr<tagDataInfo> ptr4(new tagDataInfo[10], [](tagDataInfo *pinfo) {delete[]pinfo; });//lambda表达式方式创建智能指针数组
	ptr4.get()[1].offs = 2;//数组的访问

3.或者是使用默认的

std::shared_ptr<int> sp3(new int[10](), std::default_delete<int[]>());

在c++17 

1.shared_ptr支持了动态数组,同时增加了opreator[],并可以使用int[]类的数组类型做模板参数

std::shared_ptr<int[]> sp1(new int[5]());
for (int i = 0; i < 5; ++i) {
		sp1[i] = (i + 1) * (i + 1);
	}

std::shared_ptr<tagDataInfo[]> ptr5(new tagDataInfo[5]());//c++ 17才支持

主要用于数据库、文件句柄、socket、自定义资源类等等,如

1.shared_ptr管理FILE文件指针

2.shared_ptr管理socket

3. 我们假用一个空指针,在其退出作用域时,自动调用任意的函数!!!any_fun(void* p) {....}
boost::shared_ptr<void> p(nullptr, any_func); //在其退出作用域时,自动调用任意的函数!!!

 

shared_ptr是一个伪指针,充当普通指针,我们可以将*->与 shared_ptr 对象一起使用。但

与普通指针相比,shared_ptr仅提供-> 、*==运算符,没有+-++--[]等运算符。

也可以像其他 shared_ptr 对象一样进行比较;

如:

std::shared_ptr<int> p1 = std::make_shared<int>();

*p1 = 78;

当我们创建 shared_ptr 对象而不分配任何值时,它就是空的;普通指针不分配空间的时候相当于一个野指针,指向垃圾空间,且无法判断指向的是否是有用数据。
shared_ptr 检测空值方法 

1.if(!ptr3)

2.if(ptr3 == NULL)

3.if(ptr3 == nullptr)

访问对象的成员变量。
std::shared_ptr<int> p4(new int(5));或者std::shared_ptr<int> p4 = make_shared<int>(5); //一样效果

1.   int *pInt = p4.get();    *pInt   = 1;    返回auto_ptr指向的那个对象的内存地址。

2. 直接访问    p4->     或者是   (*p4).   因为 shared_ptr是一个伪指针,充当普通指针,我们可以将*->与 shared_ptr 对象一起使用。

不要使用同一个原始指针构造 shared_ptr

会导致内存泄漏从而程序崩溃

创建多个 shared_ptr 的正常方法是使用一个已存在的shared_ptr 进行创建,而不是使用同一个原始指针进行创建。

    int *num = new int(23);
    std::shared_ptr<int> p1(num);
    
    std::shared_ptr<int> p2(p1);  // 正确使用方法
    std::shared_ptr<int> p3(num); // 不推荐

    std::cout << "p1 Reference = " << p1.use_count() << std::endl; // 输出 2
    std::cout << "p2 Reference = " << p2.use_count() << std::endl; // 输出 2
    std::cout << "p3 Reference = " << p3.use_count() << std::endl; // 输出 1

假如使用原始指针num创建了p1,又同样方法创建了p3,当p1超出作用域时会调用delete释放num内存,此时num成了悬空指针,当p3超出作用域再次delete的时候就可能会出错。

不要用栈中的指针构造 shared_ptr 对象

shared_ptr 默认的析构函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。

建议使用 make_shared

为了避免以上两种情形,建议使用make_shared()<>创建 shared_ptr 对象,而不是使用默认构造函数创建。

auto sp1 = boost::make_shared<std::string>("make_shared");//不仅快而且高效,分配一次内存

boost::shared_ptr<std::string> sp2(new std::string("make_shared")); //要动用两次动态内存分配

另外不建议使用get()函数获取 shared_ptr 关联的原始指针,因为如果在 shared_ptr 析构之前手动调用了delete函数,同样会导致类似的错误。

特点:

1.shared_ptr允许多个指针指向同一个对象;unique_ptr则"独占"所指向的对象。

2.智能指针是模板类而不是指针。类似vector,智能指针也是模板,当创建一个智能指针时,必须提供额外的信息即指针可以指向的类型。

3.shared_ptr的类型转换不能使用一般的static_cast,这种方式进行的转换会导致转换后的指针无法再被shared_ptr对象正确的管理。应该使用专门用于shared_ptr类型转换的 static_pointer_cast<T>() , const_pointer_cast<T>() 和dynamic_pointer_cast<T>()。

4.C++开发处理内存泄漏最有效的办法就是使用智能指针,使用智能指针就不会担心内存泄露的问题了,因为智能指针可以自动删除分配的内存。

5.每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候, 内存才会被释放。

6.可以通过构造函数、赋值函数或者make_shared函数初始化智能指针。

7.shared_ptr基于”引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。

8.可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。

9.不要把this指针给shared_ptr;

10.不要在函数实参里创建shared_ptr;

11.在多线程环境中使用共享指针的代价非常大,这是因为你需要避免关于引用计数的数据竞争;
12.不delete get()返回的指针;

13.如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了

14.所以,传递shared_ptr参数还是用传值更好!在传引用的情况下,如果...代码中对shared_ptr对象有副作用时,比如传入的t是一个对象的成员变量而这个对象又正好被释放了,那么这个shared_ptr的引用计数可能被减为0从而导致所指对象的释放

15.智能指针值传递:抛弃临时对象,让所有的智能指针都有名字,就可以避免此类问题的发生

//header file
void func( shared_ptr<T1> ptr1, shared_ptr<T2> ptr2 ); 
//call func like this
shared_ptr<T1> ptr1( new T1() );
shared_ptr<T2> ptr2( new T2() );
func(ptr1, ptr2  );

即先定义赋值好,再作为函数的参数。

16.shared_ptr、weak_ptr对比

 17.shared_ptr中的指针转型

  1. static_pointer_cast<T> -------------- 向上转,派生类到基类
  2. dynamic_pointer_cast<T>----------- 向下转,基类到派生类
  3. const_pointer_cast<T> --------------去除const属性

     struct  SS
    {
        int a;
        int b;
    };
    SS aa;
    std::shared_ptr<SS> c = std::make_shared<SS>(aa);
    (*c).a = 1;
    c.get()->b =2;

通过这样的方式,访问对象的成员变量。

缺点:通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptrweak_ptr一起引入了标准库,用来解决循环引用的问题。

ps:

1、不使用相同的普通指针初始化多个智能指针。因为当某个智能指针对象释放其内存时,这个普通指针相应会被delete,此时其他智能指针管理的资源已经被释放了,再对资源进行操作其行为是未定义.

2.不delete get()返回的指针。get()即返回智能指针对象中保存的指针,这个应该很容易理解,delete了get()返回的指针,那么相当于释放了智能指针的资源.

3.如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。

4.不delete get()返回的指针。get()即返回智能指针对象中保存的指针,这个应该很容易理解,delete了get()返回的指针,那么相当于释放了智能指针的资源

unique_ptr

unique_ptr 独享所有权:unique_ptr对象始终是关联的原始指针的唯一所有者。我们无法复制unique_ptr对象,它只能移动。

创建一个空的 unique_ptr 对象

创建一个空的unique_ptr<int>对象,因为没有与之关联的原始指针,所以它是空的。

std::unique_ptr<int> ptr1;

检查 unique_ptr 对象是否为空

有两种方法可以检查 unique_ptr 对象是否为空或者是否有与之关联的原始指针。

// 方法1
if(!ptr1)
    std::cout<<"ptr1 is empty"<<std::endl;
// 方法2
if(ptr1 == nullptr)
    std::cout<<"ptr1 is empty"<<std::endl;

使用原始指针创建 unique_ptr 对象

1.要创建非空的 unique_ptr 对象,需要在创建对象时在其构造函数中传递原始指针,即:

std::unique_ptr<Task> taskPtr(new Task(22));

2.使用 std::make_unique 创建 unique_ptr 对象 / C++14

std::make_unique<>() 是C++ 14 引入的新函数

std::unique_ptr<Task> taskPtr = std::make_unique<Task>(34);

获取被管理对象的指针

使用get()·函数获取管理对象的指针。

Task *p1 = taskPtr.get();

重置 unique_ptr 对象

在 unique_ptr 对象上调用reset()函数将重置它,即它将释放delete关联的原始指针并使unique_ptr 对象为空

unique_ptr 对象不可复制

由于 unique_ptr 不可复制,只能移动。因此,我们无法通过复制构造函数或赋值运算符创建unique_ptr对象的副本。

// 编译错误 : unique_ptr 不能复制
std::unique_ptr<Task> taskPtr3 = taskPtr2; // Compile error

// 编译错误 : unique_ptr 不能复制
taskPtr = taskPtr2; //compile error

转移 unique_ptr 对象的所有权

我们无法复制 unique_ptr 对象,但我们可以转移它们。这意味着 unique_ptr 对象可以将关联的原始指针的所有权转移到另一个 unique_ptr 对象。

// 通过原始指针创建 taskPtr2
std::unique_ptr<Task> taskPtr2(new Task(55));
// 把taskPtr2中关联指针的所有权转移给taskPtr4
std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
// 现在taskPtr2关联的指针为空
if(taskPtr2 == nullptr)
    std::cout<<"taskPtr2 is  empty"<<std::endl;

// taskPtr2关联指针的所有权现在转移到了taskPtr4中
if(taskPtr4 != nullptr)
    std::cout<<"taskPtr4 is not empty"<<std::endl;

// 会输出55
std::cout<< taskPtr4->mId << std::endl;

std::move() 将把 taskPtr2 转换为一个右值引用。因此,调用 unique_ptr 的移动构造函数,并将关联的原始指针传输到 taskPtr4。在转移完原始指针的所有权后, taskPtr2将变为空。

释放关联的原始指针

在 unique_ptr 对象上调用 release()将释放其关联的原始指针的所有权,并返回原始指针。这里是释放所有权,并没有delete原始指针,reset()会delete原始指针。

成员函数作用
reset()重置unique_ptr为空,delete其关联的指针。
release()不delete关联指针,并返回关联指针。释放关联指针的所有权,unique_ptr为空。
get()仅仅返回关联指针

shared_ptr 和unique_ptr 比较

1.内存占用:shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高。

2.原子操作性能低考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

使用场景

  1. shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
  2. 对象的延迟销毁。陈硕在《Linux 多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用 BlockingQueue<std::shared_ptr<void>> 将对象转移到另外一个线程中释放,从而解放关键线程

weak_ptr

目的:它的出现完全是为了弥补它老大shared_ptr天生有缺陷的问题,将shared_ptrweak_ptr一起引入了标准库,用来解决循环引用的问题。

weak_ptr本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能配合shared_ptr来使用,可以将shared_ptr的对象赋值给weak_ptr,并且这样并不会改变引用计数的值同时也不能将weak_ptr对象直接赋值给shared_ptr类型的变量查看weak_ptr的代码时发现,它主要有lock、swap、reset、expired、operator=、use_count几个函数,与shared_ptr相比多了lock、expired函数,但是却少了get函数,甚至连operator* 和 operator->都没有。

	shared_ptr<int> sptr(new int(1));
	weak_ptr<int> wptr = sptr;
    shared_ptr<int> sptr1 = wptr.lock();

weak_ptr中只有函数lockexpired两个函数比较重要,因为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired函数来检测是否过期,然后使用lock函数来获取其对应的shared_ptr对象,然后进行后续操作:

void test2()
{
    shared_ptr<CA> ptr_a(new CA());     // 输出:CA() called!
    shared_ptr<CB> ptr_b(new CB());     // 输出:CB() called!

    cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1
    cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 输出:ptr_b use count : 1
    
    weak_ptr<CA> wk_ptr_a = ptr_a;
    weak_ptr<CB> wk_ptr_b = ptr_b;

    if (!wk_ptr_a.expired())
    {
        wk_ptr_a.lock()->show();        // 输出:this is class CA!
    }

    if (!wk_ptr_b.expired())
    {
        wk_ptr_b.lock()->show();        // 输出:this is class CB!
    }

    // 编译错误
    // 编译必须作用于相同的指针类型之间
    // wk_ptr_a.swap(wk_ptr_b);         // 调用交换函数

    wk_ptr_b.reset();                   // 将wk_ptr_b的指向清空
    if (wk_ptr_b.expired())
    {
        cout << "wk_ptr_b is invalid" << endl;  // 输出:wk_ptr_b is invalid 说明改指针已经无效
    }

    wk_ptr_b = ptr_b;
    if (!wk_ptr_b.expired())
    {
        wk_ptr_b.lock()->show();        // 输出:this is class CB! 调用赋值操作后,wk_ptr_b恢复有效
    }

    // 编译错误
    // 编译必须作用于相同的指针类型之间
    // wk_ptr_b = wk_ptr_a;


    // 最后输出的引用计数还是1,说明之前使用weak_ptr类型赋值,不会影响引用计数
    cout << "ptr_a use count : " << ptr_a.use_count() << endl; // 输出:ptr_a use count : 1
    cout << "ptr_b use count : " << ptr_b.use_count() << endl; // 输出:ptr_b use count : 1
}

总结
weak_ptr虽然是一个模板类,但是不能用来直接定义指向原始指针的对象。
weak_ptr接受shared_ptr类型的变量赋值,但是反过来是行不通的,需要使用lock函数。
weak_ptr设计之初就是为了服务于shared_ptr的,所以不增加引用计数就是它的核心功能。
由于不知道什么之后weak_ptr所指向的对象就会被析构掉,所以使用之前请先使用expired函数检测一下。
 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值