> 文档中心 > C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

文章目录

  • 一、什么是RAII
    • RAlI的原理
  • 二、智能指针的引入
    • 1.为什么要使用智能指针
  • 三、被弃用的auto_ptr
  • 四、模拟实现auto_ptr
    • 1. (*)和(->)重载
    • 2.重置指向(reset)和释放函数(release)
    • 3. 拷贝构造所带来的问题
      • 版本一:重复析构(两个指针指向同一块内存)
      • 版本二:失去拥有权引发程序崩溃
    • 4.指向一组对象引发的错误
  • END

一、什么是RAII

RAll (Resource Acquisition ls Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存(heap)、网络套接字,互斥量,文件句柄等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

RAlI的原理

资源的使用一般经历三个步骤:
a.获取资源(创建对象)
b.使用资源
c.销毁资源(析构对象)

但是资源的销毁往往是程序员经常忘记的一个环节,所以程序界就想如何在程序员中让资源自动销毁呢?

解决问题的方案是:RAll,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。给一个简单的例子来看下局部对象的自动销毁的特性:

class Student{public:Student(const string name = "", int age = 0): s_name(name), s_age(age) {cout << "Init a Student !" << endl;}~Student() { cout << "Destroy a Stduent !" << endl; }const string& getname() const { return this->s_name; }int getage() const { return this->s_age; }private:const string s_name;int s_age;};void fun() {// 必须是局部对象Student s("Sauron");}int main(void){fun();system("pause");return 0;}

C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

该示例可以看出,当我们在fun函数中声明一个局部对象的时候,会自动调用构造函数进行对象的初始化,当整个fun函数执行完成后,自动调用析构函数来销毁对象,整个过程无需人工介入,由操作系统自动完成;于是,很自然联想到,当我们在使用资源的时候,在构造函数中进行初始化,在析构函数中进行销毁。

整个RAlI过程总结为四个步骤:

a.设计一个类封装资源
b.在构造函数中初始化
c.在析构函数中执行销毁操作
d.使用时定义一个该类的对象

二、智能指针的引入

智能指针是比原始指针更智能的类,解决悬空(dangling)指针或多次删除被指向对象,以及资源泄露问题,通常用来确保指针的寿命和其指向对象的寿命一致。智能指针虽然很智能,但容易被误用,智能也是有代价的。

1.为什么要使用智能指针

因为裸指针存在很多问题,主要是下面这些:

  1. 难以区分指向的是单个对象还是一个数组;
  2. 使用完指针之后无法判断是否应该销毁指针,因为无法判断指针是否“拥有”指向的对象;
  3. 在已经确定需要销毁指针的情况下,也无法确定是用delete关键字删除,还是有其他特殊的销毁机制,例如通过将指针传入某个特定的销毁函数来销毁指针;
  4. 即便已经确定了销毁指针的方法,由于1的原因,仍然无法确定到底是用delete(销毁单个对象)还是delete[] (销毁一个数组);
  5. 假设上述的问题都解决了,也很难保证在代码的所有路径中(分支结构,异常导致的跳转),有且仅有一次销毁指针操作;任何一条路径遗漏都可能导致内存泄露,而销毁多次则会导致未定义行为
  6. 理论上没有方法来分辨一个指针是否处于悬挂状态。

三、被弃用的auto_ptr

在C11中,有四个智能指针:

auto_ptr; // 弃用unique_ptr;shared_ptr;weak_ptr;

在C98中,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。

四、模拟实现auto_ptr

对于auto_ptr类,它的成员除了一个指针,还有一个拥有权成员,它的作用是记录指针是否对所指之物有拥有权。

template<typename _Ty>class my_auto_ptr{private:bool _Owns;  // 拥有权_Ty* _Ptr;public:my_auto_ptr(_Ty* p = nullptr): _Owns(p != nullptr), _Ptr(p){// 构造智能指针}~my_auto_ptr(){if (_Owns) {delete _Ptr;}_Owns = false;_Ptr = nullptr;}};

再利用一个类来测试这个智能指针:

class Object{private:int num;public:Object(int x = 0) : num(x){cout << "Create Object: " << this << endl;}~Object(){cout << "Destroy Object: " << this << endl;}};int main(void){my_auto_ptr<Object> obj(new Object(10));return 0;}

运行结果:

构造
C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

析构
在这里插入图片描述

图示:

在这里插入图片描述

1. (*)和(->)重载

若要用指向符和解引用符来调用智能指针指向对象的方法,可以对其重载:

比如Object对象里有其他方法:

int& value() {return num;}const int& value() const {return num;}

运算符重载:
对于解引用的重载,利用get()得到_Ptr;对其解引用得到_Ptr指向的对象,以引用返回。

对于指向符的重载,直接返回get()即可。

_Ty* get() const { return _Ptr; }_Ty& operator*()const{return *(get());}_Ty* operator->() const{return get();}

执行示例:

int main(void){my_auto_ptr<Object> obj(new Object(10));cout << obj->value() << endl;cout << (*obj).value() << endl;return 0;}

C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

2.重置指向(reset)和释放函数(release)

重置智能指针指向,意思就是不指向当前对象了,指向另一个对象。

void reset(_Ty* p = nullptr){if (_Owns){delete _Ptr;}_Ptr = p;}

释放的意思就是失去对当前对象的拥有权,可以利用返回值方式让其他指针指向当前对象。

_Ty* release() const{_Ty* tmp = nullptr;if (_Owns){((my_auto_ptr*)this)->_Owns = false;tmp = _Ptr;((my_auto_ptr*)this)->_Ptr = nullptr;}return tmp;}

3. 拷贝构造所带来的问题

版本一:重复析构(两个指针指向同一块内存)

如果拷贝构造只是简单的浅拷贝1,那么会有下面的这种情况:

my_auto_ptr(const my_auto_ptr& op): _Owns(op._Owns){if (_Owns) {_Ptr = op._Ptr;}}

示例代码:

int main(void){my_auto_ptr<Object> obja(new Object(10));my_auto_ptr<Object> objb(obja);return 0;}

运行结果:
程序结束时,会对指向的Object对象析构两次,导致堆破坏。

在这里插入图片描述
在这里插入图片描述

C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

版本二:失去拥有权引发程序崩溃

代码:
在拷贝构造中,让被拷贝的智能指针释放资源转移给待拷贝的智能指针

my_auto_ptr(const my_auto_ptr& op): _Owns(op._Owns), _Ptr(op.release()){}

示例代码:
调用fun()函数时将obja作为参数传递过去:

void fun(my_auto_ptr<Object> op){cout << op->value() << endl;}int main(void){my_auto_ptr<Object> obja(new Object(10));fun(obja);cout << obja->value() << endl;return 0;}

传递给形参时,需要调用拷贝构造函数,那么就会将资源转移给形参op,在fun()函数结束时形参op会自动析构掉。

如图:
在进入fun()函数时,对象拥有权已经在形参手里,原来的obja指针已经不拥有Object对象了
在这里插入图片描述

在fun()函数结束时,形参op也自动析构,也析构了Object对象。

在这里插入图片描述

那么再想通过原来的obja指针来访问Object对象,就会引发异常:读取访问权限冲突。
C++:什么是RAII? | 智能指针的初步讲解 | 智能指针是为了避免什么问题?| 被遗弃的auto_ptr

赋值运算符重载和拷贝构造一样,会引发这些问题。

4.指向一组对象引发的错误

由于在auto_ptr的析构函数设计的是 释放当前指针指向的内容,即delete _Ptr; 这样在初始化指向一组对象时,析构函数只析构了一个对象,引发内存泄漏。

至于为什么delete和delete[]的使用时的区别,在这篇文章可以有所了解:
delete、free和delete[] 混用的后果

int main(void){my_auto_ptr<Object> obja(new Object(10)); // 正确my_auto_ptr<Object> objb(new Object[10]); // 错误return 0;}

END

由于一系列的问题,auto_ptr在C11中被弃用,在C17中已经被移除。

风车动漫