【C++11】4种智能指针

目录

一、4种指针介绍

1、auto_ptr(不要使用,auto_ptr是C++98的智能指针,C++11明确声明不再支持。)

2、unique_ptr(一种强引用指针)

内部大概实现:

3、shared_ptr(一种强引用指针)

内部大概实现:

4、weak_ptr(一种弱引用指针)

内部大概实现:

针对空悬指针问题:

针对循环引用问题:

总结(语义)

使用make_shared的优势和劣势

二、用法

 

三、实际用例

unique_ptr

shared_ptr

weak_ptr 

四、智能指针原理和实现


一、4种指针介绍

1、auto_ptr(不要使用,auto_ptr是C++98的智能指针,C++11明确声明不再支持。


最原始的智能指针。

auto_ptr具有以下缺陷:

  • auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。

由于 auto_ptr 基于【排他所有权模式】,这意味着:两个指针(同类型)不能指向同一个资源,复制或赋值都会改变资源的所有权。

复制auto_ptr对象时,把指针指传给复制出来的对象,原有对象的指针成员随后重置为nullptr。

auto_ptr<TYPE> A,B;
A = B;

第二行执行完毕后,B的地址为0;

这就是智能指针auto_ptr是转移语义造成的,容易让人用错的地方。

auto_ptr<TYPE> A,B;
A = B;

第二行执行完毕后,B的地址为0;

这就是智能指针auto_ptr是转移语义造成的,容易让人用错的地方。

使用auto_ptr要知道:
1. 智能指针不能共享指向对象的所有权
2. 智能指针不能指向数组。因为其实现中调用的是delete而非delete[]
3. 智能指针不能作为容器类的元素。

所以注意:不要用auto_ptr! 不要用auto_ptr!

 

2、unique_ptr(一种强引用指针)

“它是我的所有物,你们都不能碰它!”——鲁迅
正如它的名字,独占 是它最大的特点。

 

内部大概实现:

它其实算是auto_ptr的翻版(都是独占资源的指针,内部实现也基本差不多).

但是unique_ptr的名字能更好的体现它的语义,而且在语法上比auto_ptr更安全(尝试复制unique_ptr时会编译期出错,而auto_ptr能通过编译期从而在运行期埋下出错的隐患)

假如你真的需要转移所有权(独占权),那么你就需要用std::move(std::unique_ptr对象)语法,尽管转移所有权后 还是有可能出现原有指针调用(调用就崩溃)的情况。
但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

示例用法:

void runGame(){
  std::unique_ptr<Monster> monster1 = new Monster();//monster1 指向 一个怪物
  std::unique_ptr<Monster> monster2 = monster1;//Error!编译期出错,不允许复制指针指向同一个资源。
  std::unique_ptr<Monster> monster3 = std::move(monster1);//转移所有权给monster3.
  monster1->doSomething();//Oops!monster1指向nullptr,运行期崩溃
}

 

额外:boost库的boost::scoped_ptr也是一个独占性智能指针,但是它不允许转移所有权,从始而终都只对一个资源负责,它更安全谨慎,但是应用的范围也更狭窄。)

 

3、shared_ptr(一种强引用指针)

“它是我们(shared_ptr)的,也是你们(weak_ptr)的,但实质还是我们的”——鲁迅
共享对象所有权是件快乐的事情。

多个shared_ptr指向同一处资源,当所有shared_ptr都全部释放时,该处资源才释放。
有某个对象的所有权(访问权,生命控制权) 即是 强引用,所以shared_ptr是一种强引用型指针)

内部大概实现:


每个shared_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域(SharedPtrControlBlock)的指针

(用原始指针构造时,会new一个SharedPtrControlBlock出来作为计数存放的地方,然后用指针指向它,计数加减都通过SharedPtrControlBlock指针间接操作。)

 
//shared计数放在这个结构体里面,实际上结构体里还应该有另一个weak计数。下文介绍weak_ptr时会解释。
struct SharedPtrControlBlock{
  int shared_count;
};

//大概长这个样子(化简版)
template<class T>
class shared_ptr{
  T* ptr;
  SharedPtrControlBlock* count;
};

 

每次复制,多一个共享同处资源的shared_ptr时,计数+1。每次释放shared_ptr时,计数-1。
当shared计数为0时,则证明所有指向同一处资源的shared_ptr们全都释放了,则随即释放该资源(哦,还会释放new出来的SharedPtrControlBlock)。
这也是常说的引用计数技术(好绕口)

示例用法:
void runGame(){
  std::shared_ptr<Monster> monster1 = new Monster();   //计数加到1

  do{std::shared_ptr<Monster> monster2 = monster1;    //计数加到2
  }while(0);          
  //该栈退出后,计数减为1,monster1指向的堆对象仍存在

  std::shared_ptr<Monster> monster3 = monster1;      //计数加到2
}
//该栈退出后,shared_ptr都释放了,计数减为0,它们指向的堆对象也能跟着释放.

缺陷:模型循环依赖(互相引用或环引用)时,计数会不正常

假如有这么一个怪物模型,它有2个亲人关系

class Monster{
  std::shared_ptr<Monster> m_father;
  std::shared_ptr<Monster> m_son;
public:
  void setFather(std::shared_ptr<Monster>& father);//实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //懒
  ~Monster(){std::cout << "A monster die!";}    //析构时发出死亡的悲鸣
};

然后执行下面函数

void runGame(){
    std::shared_ptr<Monster> father = new Monster();
    std::shared_ptr<Monster> son = new Monster();
    father->setSon(son);
    son->setFather(father);
}

 

猜猜执行完runGame()函数后,这对怪物父子能正确释放(发出死亡的悲鸣)吗?
答案是不能。

那么我们来模拟一遍(自行脑海模拟一遍最好),函数退出时栈的shared_ptr对象陆续释放后的情形:
开始:
father,son指向的堆对象 shared计数都是为2

son智能指针退出栈:
son指向的堆对象 计数减为1,father指向的堆对象 计数仍为2。

father智能指针退出栈:
father指向的堆对象 计数减为1 , son指向的堆对象 计数仍为1。

函数结束:所有计数都没有变0,也就是说中途没有释放任何堆对象。

为了解决这一缺陷的存在,弱引用指针weak_ptr的出现很有必要。

 

共享指针的引用计数存放在 堆中,属于对象,在构造对象的时候,new生成:


class U_Ptr                                  
{
 
private:
 
    friend class SmartPtr;      
    U_Ptr(Point *ptr) :p(ptr), count(1) { }
    ~U_Ptr() { delete p; }
 
    int count;  
    Point *p;                                                      
};
 


    SmartPtr(Point *ptr) : rp(new U_Ptr(ptr)) { }

3、为当前案例方式*m_pCount ,可以实现多个new资源块儿时,同步new m_pCount;
即可实现资源块儿与独立m_pCount资源的绑定,方便管理计数,此情况下,在拷贝的时候,将多个拷贝
关系的变量直接使用m_pCount地址传递,达到m_pCount资源在同一个资源块儿下多个对象共享的机制
    int* m_pCount; // 资源计数器,为0时可以被释放,避免重复释放问题

4、weak_ptr(一种弱引用指针)

 

“它是我们(weak_ptr)的,也是你们(shared_ptr)的,但实质还是你们的”——鲁迅

weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。

只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以weak_ptr是一种弱引用型指针,不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。)

(weak_ptr 是配合 shared_ptr 存在,主要是为了解决两个问题:一是循环引用问题,使得资源无法释放;例如 A 对象含有一个 shared_ptr<B>,而 B 对象也含有一个 shared_ptr<A>,那么很容易产生循环引用,使得内存无法释放。

weak_ptr 要解决的另外一个问题是臭名昭著的悬挂指针(dangling pointer):指针指向的内存被删除;一个简单的场景是,A 线程创建资源,并传递给 B 线程,B 线程只读访问资源;但是 A 线程随后可能释放了资源,B 没有感知,而得到了一个悬挂指针。)

内部大概实现:

计数区域(SharedPtrControlBlock)结构体引进新的int变量weak_count,来作为弱引用计数。
每个weak_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针(和shared_ptr一样的成员)。
weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造。
weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少。

被管理资源的释放只取决于shared计数,当shared计数为0,才会释放被管理资源,
也就是说weak_ptr不控制资源的生命周期。

但是计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。

//shared引用计数和weak引用计数
//之前的计数区域实际最终应该长这个样子
struct SharedPtrControlBlock{
  int shared_count;
  int weak_count;
};

//大概长这个样子(化简版)
template<class T>
class weak_ptr{
  T* ptr;
  SharedPtrControlBlock* count;
};

 

针对空悬指针问题

空悬指针问题是指:无法知道指针指向的堆内存是否已经释放。

得益于引入的weak_count,weak_ptr指针可以使计数区域的生命周期受weak_ptr控制,

从而能使weak_ptr获取 被管理资源的shared计数,从而判断被管理对象是否已被释放。(可以实时动态地知道指向的对象是否被释放,从而有效解决空悬指针问题)

它的成员函数expired()就是判断指向的对象是否存活。

 

针对循环引用问题

class Monster{
  //尽管父子可以互相访问,但是彼此都是独立的个体,无论是谁都不应该拥有另一个人的所有权。
  std::weak_ptr<Monster> m_father;    //所以都把shared_ptr换成了weak_ptr
  std::weak_ptr<Monster> m_son;      //同上
public:
  void setFather(std::shared_ptr<Monster>& father); //实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //懒
  ~Monster(){std::cout << "A monster die!";}     //析构时发出死亡的悲鸣
};

void runGame(){
  std::shared_ptr<Monster> father = new Monster();
  std::shared_ptr<Monster> son = new Monster();
  father->setSon(son);
  son->setFather(father);
}

那么我们再来模拟一遍,函数退出时栈的shared_ptr对象陆续释放后的情形:
一开始:
father指向的堆对象 shared计数为1,weak计数为1
son指向的堆对象 shared计数为1,weak计数为1

son智能指针退出栈:
son指向的堆对象 shared计数减为0,weak计数为1,释放son的堆对象,发出第一个死亡的悲鸣
father指向的堆对象 shared计数为1,weak计数减为0;

father智能指针退出栈:
father指向的堆对象 shared计数减为0,weak计数为0;释放father的堆对象和father的计数区域,发出第二个死亡的悲鸣。
son指向的堆对象 shared计数为0,weak计数减为0;释放son的计数区域。

函数结束,释放行为正确。

(可以说,当生命控制权没有彼此互相掌握时,才能正确解决循环引用问题,而弱引用的使用可以使生命控制权互相掌握的情况消失

此外:
weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。但可以使用lock()获得一个可用的shared_ptr对象,
如果对象已经死了,lock()会失败,返回一个空的shared_ptr。

 

void runGame(){
  std::shared_ptr<Monster> monster1 = new Monster();
  std::weak_ptr<Monster> r_monster1 = monster1;
  r_monster1->doSomething();//Error! 编译器出错!weak_ptr没有重载* 和 -> ,无法直接当指针用
  std::shared_ptr<Monster> s_monster1 = r_monster1.lock();//OK!可以通过weak_ptr的lock方法获得shared_ptr。
}

 

总结(语义)

 

1、不要使用std::auto_ptr

2、当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,请使用std::unique_ptr

3、当你需要一个共享资源所有权(访问权+生命控制权)的指针,请使用std::shared_ptr

当你需要一个能访问资源,但不控制其生命周期的指针,请使用std::weak_ptr

 

推荐用法:
一个shared_ptr和n个weak_ptr搭配使用 而不是n个shared_ptr
因为一般模型中,最好总是被一个指针控制生命周期,然后可以被n个指针控制访问。

逻辑上,大部分模型的生命在直观上总是受某一样东西直接控制而不是多样东西共同控制。
程序上,能够完全避免生命周期互相控制引发的 循环引用问题。

 

使用make_shared的优势和劣势

 

1、减少内存分配次数

shared_ptr维护引用计数需要的信息

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

使用原始的new函数创建shared_ptr

  • 首先是原始的new分配了原始对象, 然后将这个对象传递给 shared_ptr (即使用 shared_ptr 的构造函数) , shared_ptr 对象只能单独的分配控制块。
  • 控制块包含被指向对象的引用计数以及其他,也就是说,控制块的内存是在std::shared_ptr的构造函数中分配的。

使用make_shared创建shared_ptr

  • 如果选择使用 make_shared 的话, 内存分配的动作, 可以一次性完成,因为std::make_shared申请一个单独的内存块来同时存放指向的对象和控制块,这减少了内存分配的次数, 而内存分配是代价很高的操作。
  • 同时,使用std::make_shared消除了一些控制块需要记录的信息,减少了程序的总内存占用。

2、make_shared实现异常安全

  • 在shared_ptr的使用过程中,不能在函数实参中创建shared_ptr,如下:

 

//Define
void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) 
{
  ;
}
//Call
F(std::shared_ptr<Lhs>(new Lhs("foo")),std::shared_ptr<Rhs>(new Rhs("bar")));

C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:

 

new Lhs(“foo”))
new Rhs(“bar”))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>

如果在第2步的时候,发生了异常,第一步申请的 Lhs 对象内存就泄露了,
产生这个问题的核心在于, shared_ptr 没有立即获得裸指针,所以就有可能产生内存泄漏。当然,这个问题是可以这样解决:

 

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

但,最推荐的做法是

 

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

因为,申请原始对象和将原始对象裸指针赋值给shared_ptr是在同一个执行序列里,失败的话一起失败,成功就一起成功,这样就能保住创建的原始对象裸指针能安全的存放到std::shared_ptr中

 

2、增加代码对称性

shared_ptr很好地消除了显式的delete调用,但shared_ptr的构造还需要new调用,这导致了代码中的某种不对称性。虽然shared_ptr很好地包装了new表达式,但过多的显式new操作符也是个问题,它应该使用工厂模式来解决。因此make_shared<T>(),来消除显式的new调用,它的名字模仿了标准库的 make_pair()。

 

使用make_shared的缺点

  • 创建的对象如果没有公有的构造函数时,make_shared无法使用。
  • 使用make_shared内存可能无法及时回收,对内存要求要的场景需要注意。



链接:https://www.jianshu.com/p/8f0a67e26a8a
 

二、用法

 

三、实际用例

 

unique_ptr

另外,unique_ptr 也适用于消除“内部分配,外部释放”这种易错的机制,比如 c 语言的 strdup 函数:

    // 代码示例
    int main() {
        //  strdup函数内部为字符串分配内存,并返回其地址
        char* p = strdup("bert");

        // ...使用p

        // 使用者必须释放strdup分配的内存,否则内存泄漏
        free(p);
        return 0;
    }

可以看到 strdup 函数的使用,给使用者打开了犯错的窗户,一不小心就会内存泄漏。而使用 unique_ptr 则可以解决这一问题:

// unique_ptr代码示例
std::unique_ptr<char []> my_strdup(const char* s) 
{
        if (s == nullptr)
            return std::unique_ptr<char []>(nullptr);

        //计算字符串长度
        size_t len = strlen(s);

        //用智能指针管理分配的内存
        std::unique_ptr<char []> str(new char[len+1]);

        //拷贝字符串s
        strcpy(str.get(), s);

        //返回分配的新字符串
        return str;
}
   
int main() 
{
        auto p = my_strdup("bert");

        // ...
        // 使用p

        // 使用者什么都不需要做,无须担心内存泄漏
        return 0;
}

通过返回 unique_ptr 替代裸指针,无须使用者操心内存的释放问题,非常安全。

unique_ptr 的使用注意

unique_ptr 是旧标准库中 auto_ptr 的升级版,由于独占性语义,它不允许被复制,但是可以 move:

// 代码示例
std::unique_ptr p(new Student());
std::unique_ptr p2(std::move(p));
// 现在p2指向了Student, p则成为了null指针。

所以,unique_ptr 是可以被放到 STL 容器中;旧版本要求容器中的元素必须可以被复制,而现在放宽了:movable 的对象也是可以放入容器的:

    std::vector<std::unique_ptr<int>> ptr_vec;

 

shared_ptr

//shared_ptr_test.cc
#include <string.h>
#include <stdio.h>
#include <memory>
#include <thread>
#include <iostream>

using namespace std;
typedef int Resourse;

shared_ptr<Resourse> CreateResourse()
{
    return make_shared<Resourse>(1);
}

void User1(shared_ptr<Resourse> p) {
    if (p) {
        //1 使用p
        cout << "use p in user1 thread." << endl;
    }
    return;
}

void User2(shared_ptr<Resourse> p)
 {

    this_thread::sleep_for(chrono::milliseconds(10));
    if (p) {
        //2 使用p
        cout << "use p in user2 thread." << endl;
    }
    return;
}

int main() 
{
    shared_ptr<Resourse> res = CreateResourse();

    // 启动线程t1和t2,运行user函数,使用res
    thread t1(User1, res);
    thread t2(User2, res);

    // 等待线程结束,谁也不需要考虑res资源的释放
    t1.join();
    t2.join();
    return 0;
}

weak_ptr 

下面我们看一下 weak_ptr 如何解决这个问题;细节请参考代码和注释:

#include <string.h>
#include <stdio.h>
#include <memory>
#include <thread>
#include <iostream>

using namespace std;
typedef int Resourse;
shared_ptr<Resourse> g_resourse;

void thread_a()
{
    // 2. 创建全局资源
    g_resourse = make_shared<Resourse>(1);

    // 3. 睡眠3秒钟
    this_thread::sleep_for(chrono::seconds(3));

    // 6. 释放资源
    g_resourse = nullptr;
    cout << "free resourse, thread A exit." << endl;
}

void thread_b()
{
    //  1. 休眠,让线程A先创建资源
    this_thread::sleep_for(chrono::milliseconds(100));

    // 4. 创建weak_ptr访问资源,它可以有效检测出悬挂指针:
    weak_ptr<Resourse> pw(g_resourse);

    // 5. 隔一秒钟访问资源,若资源被释放了,则退出线程;
    int i = 0;
    while (1)
    {
        i++;

        // 调用weak_ptr的lock()尝试提升到shared_ptr
        auto res(pw.lock());

        if (res)
        {
            // 在6之前: 提升成功,指针res有效,可以使用资源,然后睡眠1秒钟
            cout << i << ":Success read resourse from thread B." << endl;
            this_thread::sleep_for(chrono::seconds(1));
        }
        else
        {
            cout << "Fail read resourse from thread B, exit." << endl;
            return; // 7. 说明资源被释放了,出现了"悬挂指针"情况,线程退出
        }
    }
}

int main()
{
    //启动线程A
    std::thread t_a(thread_a);
    //启动线程B
    std::thread t_b(thread_b);
    // 请注意看线程代码注释中的序号,大致代表了代码的执行顺序

    //等待线程结束
    t_a.join();
    t_b.join();
    return 0;
}

 

实践:使用命令 g++ -o weak_test -std=c++11 weak_ptr_test.cc -lpthread得到可执行文件 weak_test,输入./weak_test 运行,可以看到屏幕输出如下:

1 Success read resourse from thread B.
2 Success read resourse from thread B.
3 Success read resourse from thread B.
free resourse, thread A exit.
Fail read resourse from thread B, exit.

 

注意最后两行,首先 A 线程释放了资源并退出,然后 B 线程使用 weak_ptr 感知到了资源释放,避免了出现悬挂指针错误。

四、智能指针原理和实现

https://blog.csdn.net/bandaoyu/article/details/85039939

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值