首页 > 其他分享 >智能指针与RAII

智能指针与RAII

时间:2023-05-16 09:23:28浏览次数:43  
标签:std int RAII 智能 new shared ptr 指针

对象生存期和资源管理(RAII)

RAII:Resource Acquisition Is Initialization,资源获取即初始化。是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。我们常常利用栈对象(栈对象在进程结束时会被自动回收)的生命周期来管理资源(内存资源、文件描述符、文件、锁)。

四个基本特征

  1. 在构造函数中初始化资源,或者称为托管资源
  2. 在析构函数中释放资源
  3. 提供若干访问资源的方法(比如:读写文件)
  4. 一般不允许复制或者赋值(对象语义)

有关语义的知识,可以查看这篇文章:https://www.cnblogs.com/MyXjil/p/17300170.html

智能指针

在现代 C++ 编程中,标准库包含智能指针,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。它对RAII思想的体现是至关重要的。

智能指针的基本框架

智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间

#include <iostream>
#include <string>

using namespace std;

// 类模板
template<class T>
class Smartptr	// 自定义的智能指针
{
public:
    Smartptr(T* _ptr)
    : ptr(_ptr)
    {

    }
    ~Smartptr()
    {
        // 析构函数中释放指针所指向的空间
        if(ptr != nullptr)
        {
            delete ptr;
            ptr = nullptr;
        }
    }

    // 智能指针需要能够像普通指针一样使用,所以需要重载 *运算符和 ->运算符
    T& operator*()
    {
        return *ptr;
    }
    T& operator->()
    {
        return ptr;
    }
private:
    T* ptr;
};

void test1()
{
    Smartptr<int> pInt(new int(10));
    Smartptr<string> pStr(new string("hello"));
    cout << "*pInt = " << *pInt << endl;
    cout << "Str = " << *pStr << endl; 
}

void test2()
{
    // 我们的类中没有自定义拷贝构造函数和赋值运算符函数,于是下面的代码会调用类中原始的拷贝构造函数
    // 这时,pInt1和pInt2会指向同一片区域,于是程序结束时会对这一资源对象释放两次,程序进而会崩溃
    Smartptr<int> pInt1(new int(10));
    Smartptr<int> pInt2(pInt1);	// error
}

int main()
{
    test1();
    return 0;
}

综上所述,我们不能使用原生的拷贝构造函数和赋值运算符函数,并且在定义的拷贝构造函数和赋值运算符函数时需要考虑只能释放一次资源对象。

智能指针内存模型

C++库中的智能指针

auto_ptr(一般不会使用)

// 源码
class auto_ptr
{
public:
    auto_ptr(_Tp* __p = 0)  
    : _M_ptr(__p) 
    {
        
    } 
    
	auto_ptr(auto_ptr& __a)  
	: _M_ptr(__a.release()) 
	{

	}
    
    _Tp* release() 
    {
        _Tp* __tmp = _M_ptr;
        _M_ptr = nullptr;
        return __tmp;
    }
 private:
     _Tp* _M_ptr;
};
// 使用实例
#include <iostream>
#include <memory>

using std::cout;
using std::endl;
using std::auto_ptr;

void test()
{
    //pInt称为裸指针(原生的指针)
    int *pInt = new int(10);
    auto_ptr<int> ap(pInt);//智能指针
    cout << "*ap = " << *ap << endl;
    cout << "*pInt = " << *pInt << endl;

    cout << endl;
    //表面上执行的是拷贝操作,但是底层已经将右操作数ap所托管
    //的堆空间的控制权已经交给了左操作数ap2,并且ap底层的数据
    //成员已经被置为空了,该拷贝操作存在隐患,所以该拷贝操作是存在缺陷的
    auto_ptr<int> ap2(ap);
    cout << "*ap2 = " << *ap2 << endl;
    /* cout << "*ap = " << *ap << endl;//error 会出现段错误*/
}

int main(int argc, char **argv)
{
    test();
    return 0;
}

unique_ptr

具有移动语义,不允许复制或者赋值(源码中将拷贝构造函数和赋值运算符函数=delete,禁用了)。可以作为容器的元素(容器中可以存unique_ptr类型元素)

// 使用实例
#include <iostream>
#include <memory>
#include <vector>

using std::cout;
using std::endl;
using std::unique_ptr;
using std::vector;

class Point
{
public:
    Point(int ix = 0, int iy = 0)
    : _ix(ix)
    , _iy(iy)
    {
        cout << "Point(int = 0, int = 0)"  << endl;
    }

    void print() const
    {
        cout << "(" << _ix
             << ", " << _iy
             << ")" << endl;
    }

    ~Point()
    {
        cout << "~Point()" << endl;
    }
private:
    int _ix;
    int _iy;
};

void test()
{
    unique_ptr<int> up(new int(10));
    cout << "*up = " << *up << endl;
    cout << "up.get() = " << up.get() << endl;
    
    //独享所有权的智能指针,独立拥有托管的空间
    //在语法层面将auto_ptr的缺陷摒弃了   
    /* unique_ptr<int> up2 = up;//error */
    unique_ptr<int> up3(new int(20));
    /* up3 = up;//error */

    cout << endl;
    vector<unique_ptr<Point>> vec;
    unique_ptr<Point> up4(new Point(10, 20));
    vec.push_back(std::move(up4)); // 不能传左值,但是可以传右值
    vec.push_back(unique_ptr<Point>(new Point(1, 3))); // 或者直接构建右值,显式调用unique_ptr的构造函数
}

int main()
{
    test();
    return 0;
}

对于第52行和53行的代码解释:

unique_ptr已经将拷贝构造和赋值运算符函数这类复制的函数删除掉了,如果传左值,会有复制的操作,是会出错的。因此只能传右值作为参数

share_ptr(强引用智能指针)

是一个共享所有权的智能指针,允许对象之间进行复制或者赋值,展示出来的就是值语义。使用引用计数的观点。当对象之间进行复制或者赋值的时候,引用计数会加+1,当最后一个对象销毁的时候,引用计数减为0,此时会回收托管的空间。

share_ptr的原理

shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:

  • shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
  • 当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
  • 如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
  • 如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。

shared_ptr常用函数

  • s.get():返回shared_ptr中保存的裸指针
  • s.reset():重置shared_ptr
    • 不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针s不是唯一指向该对象的指针,则引用计数减少1,同时将P置空
    • 带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若s不是唯一的指针,则只减少引用计数,并指向新的对象
  • s.use_count():返回shared_ptr的强引用计数
  • s.unique():若use_count为1,返回true,否则返回false

shared_ptr的构造

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;

// 用make_shared来构建智能指针,因为make_shared比较高效
auto sp1 = make_shared<int>(100);
//或
shared_ptr<int> sp1 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));

// 注意:不能将一个原始指针直接赋值给一个智能指针
std::shared_ptr<int> p = new int(1);

使用时要注意的问题

1、不要使用不同的智能指针托管同一片堆空间(会出现double free的问题)

// 这种情况实际是用同一个裸指针ptr构建了两个智能指针p1和p2,但它们实际上没有任何联系。因此,在他们离开作用域之后,前面申请的堆空间会被释放两次

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int>	p2(ptr); // error

2、不要在函数实参中创建shared_ptr

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

func(shared_ptr<int>(new int),g()); // 有缺陷
// 正确写法
shared_ptr<int> p(new int);
func(p,g());

3、不要将this指针作为shared_ptr 返回出来

class A
{
public:
    shared_ptr<A> GetSelf()
    {
        return shared_ptr<A>(this); // 不要这么做
    }
    ~A()
    {
        cout << "Destructor A" << endl;
    }
};
int main()
{
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2 = sp1->GetSelf();
	return 0;
}
// 实际上是用同一个this指针构造了两个智能指针sp1和sp2,但他们实际上是没有任何关系的。在他们离开作用域之后,会调用两次析构函数,本质上和问题1是类似的

正确返回this指针和shared_ptr的做法是:让目标类继承std::enable_shared_from_this类,然后使用基类的成员函数 shared_from_this()来返回this的shared_ptr

#include <iostream>
#include <memory>

using namespace std;

class A: 
public std::enable_shared_from_this<A>
{
public:
    shared_ptr<A>GetSelf()
    {
        return shared_from_this();
    }
    ~A()
    {
        cout << "Destructor A" << endl;
    }
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf(); // ok
    return 0;
}

4、避免循环引用

问题:存在循环引用导致内存泄漏的风险

案例:

struct ListNode
{
    int _data;
    shared_ptr<ListNode> _next;
    
    ~ListNode() 
    { 
        cout << "~ListNode()" << endl;
    }
}

int main()
{
	shared_ptr<ListNode> n1(new ListNode);		// 1
	shared_ptr<ListNode> n2(new ListNode);		// 2
 
	n1.get()->_next = n2;			// 3(n1结点的下一个指向n2)
	n2.get()->_next = n1;			// 4(n2结点的下一个指向n1)
 
	return 0;
}

此时运行程序会发现析构函数并没有被调用,因为在这种情况下,两个智能指针相互托管导致对方的资源无法释放,于是就出现了循环引用的问题:

首先,程序释放n2,此时n2所指向对象的引用计数自减

然后程序释放n1,此时n1所指向对象的引用计数自减

至此,程序结束,两个对象的引用计数始终不会减到0,所以两者都不会调用析构函数

小结:循环引用就是两个智能指针互相托管对方的资源,导致单独靠对象的销毁无法使引用计数减为0,于是堆空间无法回收,因而产生内存泄漏。

解决循环引用的方法:weak_ptr

C++库中存在weak_ptr类型的智能指针。weak_ptr类的对象可以指向shared_ptr,并且不会改变shared_ptr的引用计数。一旦最后一个shared_ptr被销毁时,对象就会被释放。

所以为了解决上述循环引用的问题,我们可以将结构体内的_next指针定义为weak_ptr类型指针,这样可以防止循环引用的出现。

weak_ptr常用函数

  • expired():用于判断所观察资源是否已经释放

    • 如果引用计数use_count()==0,那么该函数的返回值就是true
    • 如果use_count() != 0,那么该函数的返回值就是false
  • lock():将weak_ptr提升为一个share_ptr,然后再来判断shared_ptr,进而知道weak_ptr指向的空间还在不在

weak_ptr<int> wp;
shared_ptr<int> sp = wp.lock();
if(sp)
{
    cout << "提升成功" << endl;
    cout << "*sp = " << *sp << endl;
}
else
{
    cout << "提升失败,wp所指向的空间已经被回收" << endl;
}

删除器

默认情况下,智能指针释放时,会用delete释放资源。所以针对FILE或者socket这种资源,需要自定义删除器,使用fclose或者close来释放资源。

#include <iostream>
#include <memory>
#include <string>

using std::cout;
using std::endl;
using std::unique_ptr;
using std::shared_ptr;
using std::string;

void test00()
{
    string msg = "hello,world\n";
    FILE *fp = fopen("wd.txt", "a+");
    fwrite(msg.c_str(), 1, msg.size(), fp);

    fclose(fp);
}

//删除器
struct FILECloser
{
    void operator()(FILE *fp)
    {
        if(fp)
        {
            fclose(fp);
            cout << "fclose(fp)" << endl;
        }
    }
};
void test()
{
    string msg = "hello,world\n";
    /* FILE *fp = fopen("wd.txt", "a+"); */
    unique_ptr<FILE, FILECloser> up(fopen("wd.txt", "a+"));
    //get函数可以从智能指针获取到裸指针
    fwrite(msg.c_str(), 1, msg.size(), up.get());
    /* fclose(up.get()); */
}

void test2()
{
    FILECloser fc;
    //要想从类型调用到对象,可以直接显示使用构造函数,比如:
    //使用无参构造函数
    string msg = "hello,world\n";
    /* shared_ptr<FILE> sp(fopen("wuhan.txt", "a+"), FILECloser()); */
    shared_ptr<FILE> sp(fopen("wuhan.txt", "a+"), fc);
    fwrite(msg.c_str(), 1, msg.size(), sp.get());
    /* fclose(sp.get()); */
}
int main(int argc, char **argv)
{
    test2();
    return 0;
}

智能指针源码剖析

https://www.bilibili.com/read/cv13410407/

标签:std,int,RAII,智能,new,shared,ptr,指针
From: https://www.cnblogs.com/MyXjil/p/17403807.html

相关文章

  • C基础笔记(指针)
    指针&变量   取地址的符号 *:地址得到地址里的东西指针变量通常情况下将指针指向地址赋为NULL被赋为NULL值的指针被称为空指针指针是一个用数值表示的地址,可以对指针进行四种算术运算:++、--、+、-指针可以用关系运算符进行比较,如==、<和>C......
  • 数字蜕变,传统制造业的转折之“机”| 触想智能推出全新C系列扫码款安卓工位机
    信息技术时代,传统制造业的未来在于数字工厂。数字工厂的核心是数据采集与动态监管,抛开大数据、云计算、物联网这些庞大后台概念不谈,承载这一核心要务的现场工具就是产线工位机设备。基于以上关键需求,触想智能早在2019年就已上市适用于制造业数字化转型的生产力工具......
  • 指针进阶(3)————玩转指针
    指针进阶内容不多,但面面俱到,都是精华1.回调函数:2.详解qsort函数参数:3.模拟实现qsort函数回调函数就是,把一个函数的地址,放在函数指针中,然后将该指针作为一个参数,传到另一个函数中,在这个函数内部使用了外部写好的一个函数.举一个例子,看完你一定明白了例子:voidmenu(void){ print......
  • 湖北省智能科教研究会走进璞华,调研璞公英教学平台个性化教学新模式
    2023年5月9日,热烈祝贺湖北省智能科教研究会红5月智能科教走进璞华集团活动圆满成功。会议上,大家畅所欲言,对教育体制改革与教育信息化产品创新,科技成果转化、产教融合、资源互补、学生能力培养等方面展开充分沟通和探讨。5月9日上午,华中师范大学教授、伍伦贡联合研究院院长、湖北......
  • 利用海鸥智能算法SOA优化极限学习机ELM的权值和阈值,用ELM的训练集误差MSE作为fitness,
    利用海鸥智能算法SOA优化极限学习机ELM的权值和阈值,用ELM的训练集误差MSE作为fitness,然后将海鸥算法寻得的最优权值和阈值在输入到ELM中建立回去预测模型,提高模型的预测精度,不会替换数据的,可以讲下怎么替换数据,同时会将测试数据一同发送,正常对照着测试数据就能替换自己的数据。ID:......
  • 基于多智能体系统 一致性算法 电力系统分布式经济调度
    基于多智能体系统一致性算法电力系统分布式经济调度策略关键词:一致性算法多智能体分布式调度仿真平台:MATLAB平台参考文档:中文复现,效果非常好,想看文献和运行效果加好友主要内容:代码主要做的是电力系统的分布式调度策略,具体为基于多智能体一致性算法的分布式经济调度方法,......
  • 访客智能分配-唯一客服系统文档中心
    账号介绍唯一客服系统账号分为三个等级:超级管理员、商户主账号、商户子账号。其中超级管理员对商户都是透明的不可见,每个商户账号之间是独立的互相不可见,商户下可创建商户子账号。系统本身是属于多商家多坐席SaaS客服系统分配原则访客智能分配的意思是,当访客打开聊天界面,会自......
  • 工厂智能电表远程抄表系统项目,成功案例,源代码出售,C#语言,可监控24小时厂区总用电量波形
    工厂智能电表远程抄表系统项目,成功案例,源代码出售,C#语言,可监控24小时厂区总用电量波形图,单表24小时用电量波形图。可自动导出多种不同形式excel表,厂区单月各表用电量,厂区各表电量值,单表每日用电量表,单表每小时用电量表ID:3477654308305764......
  • 2、c++中的指针参数传递和引用参数传递
    指针参数传递本质上是值传递。值传递的过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开空间用以存放由主调函数传递的实际参数,从而形成了实参值得一个副本。而值传递的特点是被调函对形参的任何修改都不会影响实参值。(如果想通过指针参数来修改主调函数的相关变量或......
  • 算法刷题系列之移除元素:快慢指针技巧
    题目+日期移除元素2023年5月14日17点50分基础知识暴力解法这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素,第二个for循环更新数组。双指针法(快慢指针法)通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。定义快慢指针快指针:寻找新数组的元......