c++11智能指针的基本使用

简介: 程序员自己管理堆内存可以提高程序的效率,但是管理比较麻烦,使用普通指针,容易造成堆内存泄漏(忘记释放),二次释放问题。

前言

程序员自己管理堆内存可以提高程序的效率,但是管理比较麻烦,使用普通指针,容易造成堆内存泄漏(忘记释放),二次释放问题。


shared_ptr指针

shared_ptr机制

使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,再最后一个shared_ptr析构的时候,内存才会被释放。


shared_ptr实现

1.一个指向堆上创建的对象裸指针,raw_ptr

2.一个指向内部隐藏的,共享的管理对象,share_count_object.使用use_count()可以知道当前堆上的对象被多少对象引用了,简单来说就是引用计数。


构建shared_ptr方式

1.优先使用make_shared来构造智能指针,因为比较高效。

auto sp1 = make_shared<int>(100);

2.构造函数构造

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

3.对于一个未初始化的智能指针,可以使用reset函数初始化,比如

std::shared_ptr<int> p1; 
p1.reset(new int(1));

注意构建shard_ptr注意以下2点


(1).reset()函数如果参数为空时,是将智能指针对象置空,并将智能指针所指的对象计数减一(如果所指的对象有智能指针指向时)。


(2).要注意智能指针并不是一个普通的指针,因此不能将一个原始指针指针赋值给一个智能指针,比如以下这种方法是错误的

 std::shard_ptr<int> p = new int(1); 

构建方式代码如下:

void shared_ptr_test1(){
    qDebug() << "测试构造shared_ptr";
    std::shared_ptr<int> p1(new int(1));
    std::shared_ptr<int> p2 = p1;
    std::shared_ptr<int> p3;
    p3.reset(new int(1));//给p3赋值
    std::shared_ptr<int> p4 = p3;
    if(p3){
        cout << "p3 is not null" << endl;
    }
    cout << "p1 use_count:" << p1.use_count() << endl;
    cout << "p2 use_count:" << p2.use_count() << endl;
    cout << "p3 use_count:" << p3.use_count() << endl;
    //只是让p3不指向任何对象,但是实际上原来指向的对象还被p4指向
    p3.reset();
    cout << "after reset() p3 , p3 use_count:" << p3.use_count() << endl;
    cout << "after reset() p3 , p4 use_count:" << p4.use_count() << endl;
    p4.reset();//此时p3开始指向的对象已经没有指针指向
    cout << "after reset() p4 , p4 use_count:" << p4.use_count() << endl;
}

shared_ptr 操作

1.获取原始指针(非特殊情况下不要使用):get()


注意:该函数返回一个原始的指针值,使用需要遵守以下几个规定


(1)不要保存p.get()的返回值,无论保存为裸指针还是shared_ptr都是错误的。


(2)保存为裸指针不知道什么时候就变成为悬空指针,保存为shared_ptr则产生了独立指针


(3)不要delete p.get()的返回值,会导致对一块内存delete 2次的错误。


2.指定删除器(常用)


如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。


特别是用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象。


删除器例子程序如下:

static void delete_int_ptr(int *p){
    qDebug() << "使用函数作为删除器";
    delete p;
}
void shared_ptr_test3(){
    qDebug() << "测试shared_ptr删除器";
    std::shared_ptr<int> p1(new int(1), delete_int_ptr);
    std::shared_ptr<int> p2(new int(1), [](int *p){
        qDebug() << "使用lambda作为删除器";
        delete p;
    });
    std::shared_ptr<int> p3(new int[10], [](int *p){
        qDebug() << "删除数组";
        delete []p;
    });
    qDebug() << "注意释放顺序:p3,p2,p1-栈是先进后出";
}

使用shared_ptr注意的问题

1.不要用一个原始指针初始化多个shared_ptr,这样会导致double delete,例子程序见shared_ptr_test4。

void shared_ptr_test4(){
    qDebug() << "多个智能指针指向同一个指针会导致double delete,QT环境没报错?但是这是一种错误";
    int *ptr = new int;
    shared_ptr<int> p1(ptr, [](int *p1){
        qDebug() << "delete p1:" << p1;
        delete  p1;
    });
    shared_ptr<int> p2(ptr, [](int *p2){
        qDebug() << "delete p2:" << p2;
        delete p2;
    }); // 逻辑错误
    qDebug() << "p1.get():" << p1.get() << "p2.get():" << p2.get();
    //其实指针对象有2个引用,但是计数却是分别计数的,这样就会导致多次内存释放。
    qDebug() << "p1.use_cont:" << p1.use_count() << "p2.use_count:" <<p2.use_count();
}

2.不要再函数实参中创建shared_ptr,对于下面的写法

function(shared_ptr(new int), g()); //有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也 可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还 没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针。

shared_ptr p(new int); 
function(p, g());

3.通过shared_from_this()返回this指针,不要将this指针作为shared_ptr返回回来,因为this指针本质上是一个裸指针,因此这样会导致重复析构,类似1,实际上也是将一个原始指针初始化了多个shared_ptr. 正确做法是让目标类通过std::enable_shared_from_this类,然后使用基类的 成员函数shared_from_this()来返回this的shared_ptr,如下所示。例子参见shared_ptr_test5.

class A{
public:
    shared_ptr<A> get_self(){
        return shared_ptr<A>(this);//错误做法
    }
    ~A(){
        qDebug() << "Deconstruction A";
    }
};
class B:public std::enable_shared_from_this<B>{
public:
    shared_ptr<B> get_self(){
        return shared_from_this();//正确做法
    }
    ~B(){
        qDebug() << "Deconstruction B";
    }
};
void shared_ptr_test5(){
    qDebug() << "测试返回this指针的正确做法和错误做法";
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->get_self();
    shared_ptr<B> sp3(new B);
    shared_ptr<B> sp4 = sp3->get_self();
}

4.避免循环引用,循环引用会导致内存泄漏(函数完成计数无法清零导致,使用weak_ptr可以解决这个问题),循环引用导致cp和dp的引用计数为2,在离开作用域之后,cp和dp的引用计数减为1,并不回减为0,导 致两个指针都不会被析构,产生内存泄漏。例子程序见shared_ptr_test6

class C;
class D;
class C{
public:
    std::shared_ptr<D> dptr;
    ~C() {
        cout << "C is deleted" << endl;
    }
};
class D{
public:
    std::shared_ptr<C> cptr;
    ~D() {
        cout << "D is deleted" << endl;
    }
};
void shared_ptr_test6(){
    {
        std::shared_ptr<C> cp(new C);
        std::shared_ptr<D> dp(new D);
        cout << "dp use count:" << dp.use_count() << endl;
        cp->dptr = dp;
        cout << "dp use count:" << dp.use_count() << endl;
        dp->cptr = cp;
    }
    cout<< "main leave" << endl; // 循环引用导致cp dp退出了作用域都没有析构
}

5. shared_ptr是否线程安全

(1)多线程代码操作的不是同一个shared_ptr的对象,多线程安全


指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕获的对象。

std::thread td([sp1]()){....});

另个线程传递的shared_ptr是值传递,而非引用:

void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。


(2)多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。


比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr

std::thread td([&sp1]()){....});

或者通过回调函数的参数传入的shared_ptr对象,参数类型引用

void fn(shared_ptr<A>&sp) {
...
}
..
std::thread td(fn, sp1);

需要注意:所管理数据的线程安全性问题显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象A自己去保证。


unique_ptr指针

unique_ptr 构造方式

1.构造函数构造 unique_ptr my_ptr(new T);

unique_ptr<int> p(new int);

2.C++14中的std::make_unique

auto p(std::make_unique<int>());

2种构造方式说明:使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。


unique_ptr基本使用

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将 一个unique_ptr赋值给另一个unique_ptr.

unique_ptr my_ptr(new T); 
unique_ptr my_other_ptr = my_ptr; // 报错,不能复制

虽然unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其 他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。见unique_ptr_test1().

static unique_ptr<int> get_unique_ptr(){
    unique_ptr<int> p(new int);
    cout <<"unique_ptr p.get():" << p.get() << endl;
    return p;
}
void unique_ptr_test1(){
    std::unique_ptr<int> my_ptr(new int); // 构造函数
    //auto upw1(std::make_unique<int>());
    unique_ptr<int> my_other_ptr = std::move(my_ptr); // std::move会剥夺my_ptr指针的指向
    cout << "unique_ptr_test1 my_ptr.get():" <<my_ptr.get() << endl;//my_ptr 为 NULL.
    //unique_ptr<int> ptr = my_ptr; // 报错,不能赋值
    unique_ptr<int> my_ohter_ptr2 = get_unique_ptr();//
    cout << "unique_ptr_test1 my_other_ptr2.get()" << my_ohter_ptr2.get() << endl;
}

unique_ptr可以指向一个数组,c++17以后,shared_ptr也可以指向一个数组

std::unique_ptr<int []> ptr(new int[10]);//C++11支持
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); //C++17才支持

unique_ptr指定删除器和shared_ptr有区别,unique_ptr需要确定删除器的类型。

std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确
    //std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误
    //unique_ptr需要确定删除器的类型(比如void(*)(int*))
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p)
{delete p;});

weak_ptr指针

主要作用

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。


工作原理

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr

是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。


weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。weak_ptr还可以返回this指针和解决循环引用的问题。


基本用法

1 .通过use_count()方法来获取当前观察资源的引用计数

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; //结果讲输出1

2.通过expired()方法判断所观察资源是否已经释放

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
  cout << "weak_ptr无效,资源已释放";
else
  cout << "weak_ptr有效";

3.通过lock方法获取监视的shared_ptr

std::weak_ptr<int> gw;
void f()
{
  if(gw.expired()) {
  cout << "gw无效,资源已释放";
  }
  else {
  auto spt = gw.lock();
  cout << "gw有效, *spt = " << *spt << endl;
  }
}
int main()
{
  {
  auto sp = atd::make_shared<int>(42);
  gw = sp;
  f();
  }
  f();
  return 0;
}

weak_ptr返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生

std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法是会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回。


weak_ptr解决循环引用问题

在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr

#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
    std::shared_ptr<B> bptr; 
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::weak_ptr<A> aptr;// 修改为weak_ptr
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;//并不会增加ap所指对象的计数
    }
    cout<< "main leave" << endl;
    return 0;
}

这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。


weak_ptr注意事项

weak_ptr在使用前需要检查合法性,这是由于weak_ptr不增加引用计数造成的。

weak_ptr<int> wp;
{
  shared_ptr<int> sp(new int(1)); //sp.use_count()==1
  wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
  shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;

因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后

一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法直到自己所容纳的那个指针资源的当前状态。


总结

关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管 理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。weak_ptr用于解决shared_ptr循环引用的问题。 本博客说明了C++11 中智能指针的部分用法,当然share_ptr还有其他用法,这里只是列举了个人觉得比较重要的部分。资料总结来源于腾讯课程-零声学院-darren老师


相关文章
|
7天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
33 0
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
95 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
62 1
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
45 2
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
3月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
56 3
|
2月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
3月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。