【C++11】三大神器之——智能指针
创始人
2024-04-23 02:25:14
0

文章目录

      • 前言
    • 一、智能指针的原理
      • 1、RAII机制
      • 2、简单的实现
    • 二、智能指针的用法
      • 1、智能指针的分类
      • 2、unique_ptr
        • 基本语法
      • 3、shared_ptr
        • 基本语法
      • 4、删除器
      • 5、weak_ptr

前言

一、智能指针的原理

1、RAII机制

RAII(Resource Acquisition is Initialization),即【资源获取即初始化】,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

智能指针就是RAII最具代表的实现之一。使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。

2、简单的实现

在了解了RAII机制之后,我们可以尝试实现一个简单的智能指针。

#include 
using namespace std;
/*RAII技术被认为是C++中管理资源的最佳方法,
进一步引申,使用RAII技术也可以实现安全、
简洁的状态管理,编写出优雅的异常安全的代码。*/
template
class Smart_ptr
{
private:T* m_p;
public://调用构造函数时,申请资源Smart_ptr(T* p = nullptr):m_p(p){cout << "constructor ptr" << endl;}//调用析构函数时,自动释放资源~Smart_ptr(){if(m_p){cout << "delete ptr" << endl;delete m_p;}}//普通指针的功能//通过重载运算符使其拥有和普通指针一样的功能T& operator*()const {return *m_p;}T* operator->()const {return m_p;}
};

在上述代码中,我们利用RAII机制,实现了智能指针自动释放资源,又通过重载了operator*和operator->,使其具有和指针一样的行为和功能。

我们可以通过以下的测试用例,观察RAII机制下的智能指针的工作情况:

struct AA
{int m_a;AA(int a = 0):m_a(a){cout << "constructor AA" << endl;}~AA(){cout << "destructor AA" << endl;}
};int main()
{//语句块方便观察析构函数的调用//指向自定义类型的对象{AA* pa = new AA;Smart_ptr sp1(pa);cout << sp1->m_a << endl;}//指向自定义类型的匿名对象{Smart_ptr sp2(new AA(5));cout << sp2->m_a << endl;}//指向内置类型的对象{Smart_ptr sp3(new int(5));cout << (*sp3) << endl;}return 0;
}

运行结果:
constructor AA
constructor ptr
0
delete ptr
destructor AA
constructor AA
constructor ptr
5
delete ptr
destructor AA
constructor ptr
5
delete ptr

二、智能指针的用法

1、智能指针的分类

在C++11中,提供了三种智能指针,接下来我们来逐一了解。使用这些智能指针时需要引用头文件< memory>。

  • std::shared_ptr:共享的智能指针
  • std::unique_ptr:独占的智能指针
  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

(在C++11之前还有auto_ptr,但由于它并非安全的所以本文不作介绍)

2、unique_ptr

unique_ptr独享它指向的对象,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。

以下是unique_ptr的声明:

template >//第二个模板参数D:指定删除器,缺省用delete释放资源
class unique_ptr
{
public:explicit unique_ptr(pointer p) noexcept;	// 不可用于转换函数。~unique_ptr() noexcept;    T& operator*() const;            // 重载*操作符。T* operator->() const noexcept;  // 重载->操作符。unique_ptr(const unique_ptr &) = delete;   // 禁用拷贝构造函数。unique_ptr& operator=(const unique_ptr &) = delete;  // 禁用赋值函数。unique_ptr(unique_ptr &&) noexcept;	  // 右值引用。unique_ptr& operator=(unique_ptr &&) noexcept;  // 右值引用。// ...
private:pointer ptr;  // 内置的指针。
};

可以看到,unique_ptr禁用了拷贝构造,也就是说不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。

基本语法

(1)初始化
unique_ptr的初始化方法和我们实现的智能指针的类似。事实上,我们可以使用以下的方法:

class AA{};
new* pa = new AA; 
//通过构造函数
std::unique_ptr ptr1(new AA); // 分配内存并初始化
std::unique_ptr ptr1(pa);	//将原始指针移交给智能指针管理
//通过移动函数
std::unique_ptr ptr2 = move(ptr1);
//通过reset初始化
ptr2.reset(new int);//但以下这些方法是错误的:
// std::unique_ptr pu1 = p;              // 错误,不能把普通指针直接赋给智能指针。
// std::unique_ptr pu2 = new AA("hello."); // 错误,不能把普通指针直接赋给智能指针。
// std::unique_ptr pu3 = pu2;           // 错误,不能用其它unique_ptr拷贝构造。
// std::unique_ptr pu3;
// pu3 = pu1;                            // 错误,不能用=对unique_ptr进行赋值。

(2)一些技巧

  • 智能指针unique_ptr重载了*和->操作符,使其可以像使用指针一样使用。
  • 用nullptr给unique_ptr赋值将释放对象,空的unique_ptr==nullptr。
  • 使用get()方法返回原始指针。
  • 使用release()释放对原始指针的控制权,将unique_ptr置为空,返回原始指针(可用于把unique_ptr传递给子函数,子函数将负责释放对象)。
  • 使用std::move()可以转移对原始指针的控制权(即允许赋值右值,这点在初始化时提起过)。
  • 使用reset()释放对象:
    1.pp.reset(); // 释放pp对象指向的资源对象
    2.pp.reset(nullptr); // 释放pp对象指向的资源对象
    3.pp.reset(new AA(“bbb”)); // 释放pp指向的资源对象,同时指向新的对象
  • swap()交换两个unique_ptr的控制权。
  • 不支持指针的运算(+、-、++、–)。

注意:unique_ptr也不是绝对安全的,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。

unique_ptr还提供了支持数组的具体化版本,使用如下:

unique_ptr parr1(new int[3]);          // 不指定初始值
unique_ptr parr1(new int[3]{ 33,22,11 });  // 指定初始值

3、shared_ptr

shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象。在其内部采用计数机制来实现,当新的shared_ptr与对象关联时,引用计数增加1,当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。

基本语法

(1)初始化
和unique_ptr不同的是,shared_ptr没有删除拷贝构造和赋值,并且在C++11标准中可以通过std::make_shared初始化,效率更高(std::make_unique在C++14中才有)。

struct AA{};
//通过构造函数
shared_ptr pa0(new AA);
//通过移动函数
shared_ptr pa1 = move(pa0);
//通过拷贝函数
shared_ptr pa2 = pa1;
//通过std::make_shared(推荐)
shared_ptr pa3 = std::make_shared();
//通过reset初始化
pa0.reset(); //重置pa0, 使pa0的引用基数为0
pa0.reset(new int);

此外:

  • 使用use_count()方法返回引用计数器的值。
  • 使用unique()方法,如果use_count()为1,返回true,否则返回false。
  • 不要用同一个原始指针初始化多个shared_ptr。
  • shared_ptr没有release()函数。
  • 其他的函数使用方法和unique_ptr相同。
class AA
{
public:string m_name;AA() { cout << m_name << "调用构造函数AA()。\n"; }AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};int main()
{shared_ptr pa0(new AA("aa"));     // 初始化资源aashared_ptr pa1 = pa0;                       // 用已存在的shared_ptr拷贝构造,计数加1shared_ptr pa2 = pa0;                       // 用已存在的shared_ptr拷贝构造,计数加1cout << "pa0.use_count()=" << pa0.use_count() << endl;   // 值为3cout << "pa0.get() = " << pa0.get() << endl;//pa0.get() = 0x1fb131d1420cout << "pa1.get() = " << pa1.get() << endl;//pa1.get() = 0x1fb131d1420cout << "pa2.get() = " << pa2.get() << endl;//pa2.get() = 0x1fb131d1420shared_ptr pb0 = make_shared("bb");    // 初始化资源bbshared_ptr pb1 = pb0;                      // 用已存在的shared_ptr拷贝构造,计数加1cout << "pb0.use_count()=" << pb0.use_count() << endl;   // 值为2cout << "pb0.get() = " << pb0.get() << endl;//pb0.get() = 0x1fb131d17a0cout << "pb1.get() = " << pb1.get() << endl;//pb1.get() = 0x1fb131d17a0pb1 = pa1;      // 资源aa的引用加1,资源bb的引用减1pb0 = pa1;      // 资源aa的引用加1,资源bb的引用成了0,将被释放cout << "pa0.use_count()=" << pa0.use_count() << endl;   // 值为5。cout << "pb0.use_count()=" << pb0.use_count() << endl;   // 值为5。
}

大家不要误解了共享的含义:shared_ptr指针指向的资源只有一个,它并不是被复制的,而shared_ptr可以有多个。

(2)一些细节

  • 用nullptr给shared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr。
  • std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr。
  • shared_ptr的线程安全性:
    1.shared_ptr的引用计数本身是线程安全(引用计数是原子操作)。
    2.多个线程同时读同一个shared_ptr对象是线程安全的。
    3.如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
    4.多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。

4、删除器

在默认情况下,智能指针过期的时候,用delete原始指针释放它管理的资源,程序员可以自定义删除器,改变智能指针释放资源的行为(旨在释放资源的同时能干点其他事情)。

删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针:

void deletefunc(AA* a) {    // 删除器,普通函数。cout << "自定义删除器(全局函数)。\n";delete a;
}struct deleteclass               // 删除器,仿函数。
{void operator()(AA* a) {cout << "自定义删除器(仿函数)。\n";delete a;}
};auto deleterlamb = [](AA* a) {   // 删除器,Lambda表达式。cout << "自定义删除器(Lambda)。\n";delete a;
};

给shared_ptr指定删除器十分简单,写入函数名即可:

//普通函数版本
shared_ptr pa1(new AA("aa"), deletefunc);
//仿函数版本
shared_ptr pa1(new AA("bb"), deleteclass());
//lambda函数版本
shared_ptr pa1(new AA("cc"), deleterlamb);

而在unique_ptr中会复杂一些:

//普通函数版本
//模板参数用decltype推断会简单一些
unique_ptr pu1(new AA("aa"), deletefunc);
unique_ptr pu1(new AA("aa"), deletefunc);
//仿函数版本
unique_ptr pu1(new AA("bb"), deleteclass());
//lambda函数版本
unique_ptr pu3(new AA("cc"), deleterlamb);

5、weak_ptr

现在有如下一段代码:

#include  
#include 
using  namespace std;class BB;
class AA
{
public:string m_name;AA() { cout << m_name << "调用构造函数AA()。\n"; }AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }shared_ptr m_p;
};
class BB
{
public:string m_name;BB() { cout << m_name << "调用构造函数AA()。\n"; }BB(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }~BB() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }shared_ptr m_p;
};int main()
{shared_ptr pa = make_shared("aa");shared_ptr pb = make_shared("bb");pa->m_p = pb;pb->m_p = pa;cout << "pa.use_count()=" << pa.use_count() << endl;// 结果为2cout << "pb.use_count()=" << pb.use_count() << endl;// 结果为2return 0;
}

我们会发现shared_ptr的计数器不灵了,因为上述的代码让pa和pb陷入了一个逻辑死区:我等你先死,你等我先死,结果谁都死不了。

为了解决这个问题,C++引入了weak_ptr。weak_ptr 是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。

我们将上文代码中的类的成员变量指针替换成weak_ptr,结果就一切正常了,大家可以自己试一试。

使用weak_ptr:

  • weak_ptr没有重载 ->和 *操作符,不能直接访问资源。
  • 有以下成员函数:
    1)operator=(); // 把shared_ptr或weak_ptr赋值给weak_ptr
    2)expired(); // 判断它指资源是否已过期(已经被销毁)
    3)lock(); // 返回shared_ptr,如果资源已过期,返回空的shared_ptr
    4)reset(); // 将当前weak_ptr指针置为空
    5)swap(); // 交换

对weak_ptr应用多在多线程中,对此我们总结weak_ptr的灵魂特性:

  • weak_ptr不控制对象的生命周期,但是,它知道对象是否还活着。
  • 用lock()函数把它可以提升为shared_ptr,如果对象还活着,返回有效的shared_ptr,如果对象已经死了,提升会失败,返回一个空的shared_ptr。
  • 提升的行为(lock())是线程安全的。

我们可以通过以下的代码应用这3点:

	shared_ptr pa = make_shared("aa");{shared_ptr pb = make_shared("bb");pa->m_p = pb;pb->m_p = pa;shared_ptr pp = pa->m_p.lock();            // 把weak_ptr提升为shared_ptr。if (pp == nullptr)cout << "语句块内部:pa->m_p已过期。\n";elsecout << "语句块内部:pp->m_name=" << pp->m_name << endl;}shared_ptr pp = pa->m_p.lock();            // 把weak_ptr提升为shared_ptr。if (pp == nullptr)cout << "语句块外部:pa->m_p已过期。\n";elsecout << "语句块外部:pp->m_name=" << pp->m_name << endl;

相关内容

热门资讯

安卓系统自带的网页,功能与特色... 你有没有发现,每次打开安卓手机,那熟悉的系统界面里总有一个默默无闻的小家伙——安卓系统自带的网页浏览...
美咖云系统安卓版,开启智能生活... 你有没有发现,最近手机上多了一个叫“美咖云系统安卓版”的小家伙?它就像一个魔法师,轻轻一点,就能让你...
安卓系统推荐最好的手机,盘点性... 你有没有想过,拥有一部性能卓越的手机,就像是拥有了移动的宝藏库?在这个信息爆炸的时代,一部好手机不仅...
安卓11系统能精简吗,释放潜能 你有没有发现,随着手机越来越智能,系统也越来越庞大?安卓11系统,这个最新的操作系统,是不是也让你觉...
安卓自动重启系统软件,揭秘原因... 手机突然自动重启,是不是感觉整个人都不好了?别急,今天就来和你聊聊这个让人头疼的安卓自动重启系统软件...
苹果手机x刷安卓系统,探索安卓... 你有没有想过,你的苹果手机X竟然也能刷上安卓系统?是的,你没听错,就是那个一直以来都和我们苹果手机X...
安卓系统智商低吗,智商低下的真... 你有没有想过,为什么安卓系统的智商总被调侃得好像有点低呢?是不是觉得它总是慢吞吞的,有时候还犯点小错...
安卓系统手机联系人,揭秘你的社... 你有没有发现,手机里的联系人列表就像是一个小小的社交圈呢?里面藏着我们的亲朋好友、工作伙伴,甚至还有...
安卓系统免费铃声下载,打造个性... 手机里那首老掉牙的铃声是不是让你觉得有点out了呢?别急,今天就来给你支个招,让你轻松给安卓手机换上...
安卓系统用哪个桌面好,打造个性... 你有没有发现,手机桌面可是我们每天都要面对的“脸面”呢?换一个好看的桌面,心情都能跟着好起来。那么,...
虚拟大师是安卓10系统,功能与... 你知道吗?最近在手机圈里,有个新玩意儿引起了不小的轰动,那就是虚拟大师!而且,更让人惊喜的是,这个虚...
安卓系统与苹果优缺点,系统优缺... 说到手机操作系统,安卓和苹果绝对是两大巨头,它们各有各的特色,就像两道不同的美味佳肴,让人难以抉择。...
安卓win双系统主板,融合与创... 你有没有想过,一台电脑如果既能流畅运行安卓系统,又能轻松驾驭Windows系统,那该有多爽啊?没错,...
安卓系统可精简软件,轻松提升手... 你有没有发现,手机里的安卓系统越来越庞大,软件也越装越多,有时候感觉手机就像个“大肚子”,不仅运行速...
安卓系统基于linux的代码,... 你有没有想过,那个陪伴你每天刷抖音、玩游戏、办公的安卓系统,其实背后有着一套复杂的基于Linux的代...
苹果和安卓的拍照系统,谁更胜一... 你有没有发现,现在手机拍照已经成为我们生活中不可或缺的一部分呢?无论是记录生活的点滴,还是捕捉美丽的...
苹果和安卓系统不同吗,系统差异... 你有没有想过,为什么你的手机里装的是苹果的iOS系统,而朋友的手机却是安卓系统呢?这两种系统,看似都...
安卓系统有多少级,揭秘其多级架... 你有没有想过,那个陪伴我们日常生活的安卓系统,它其实有着丰富的层级结构呢?没错,就是那个让我们的手机...
华为鸿蒙系统与安卓的,技术融合... 你知道吗?最近科技圈可是炸开了锅,华为鸿蒙系统与安卓的较量成为了大家热议的话题。这不,今天我就来给你...
什么安卓手机是苹果系统,搭载苹... 你有没有想过,为什么有些人宁愿花大价钱买苹果手机,而有些人却对安卓手机情有独钟呢?其实,这个问题背后...