1. 概述

在前面两篇文章《面向对象编程(C++篇2)——构造》《面向对象编程(C++篇3)——析构》中,我们论述了C++面向对象中一个比较好的实现,在构造函数中申请动态内存,在析构函数中进行释放。通过这种方式,我们可以实现类对象如何内置数据类型对象一样,自动实现对象的生命周期管理。

其实这个设计早就被c++之父Bjarne Stroustrup提出,叫做RAII(Resource Acquisition Is Initialization),中文的意思就是资源获取即初始化。前文所述的动态内存只是资源的一种,比如说文件的打开与关闭、windows中句柄的获取与释放等等。RAII这个名字取得比较随意,但是这个技术可以说是C++的基石,决定了C++资源管理的方方面面。

2. 详论

2.1. 堆、栈、静态区

更为深入的讲,RAII其实利用的其实程序中栈的特性,实现了对资源的自动管理。我们知道,一般程序中会分成三个内存区域:

  1. 静态内存:用来保存局部static对象,类static数据成员以及任何定义在任何函数之外的变量。
  2. 栈内存:用来保存定义在函数内的非static对象。
  3. 堆内存:用来存储动态分配的对象,例如通过new、malloc等申请的内存对象。

对于分配在静态内存中的对象和栈内存中的对象,其生命周期由编译器自动创建和销毁。而对于堆内存,生存周期由程序显式控制,使用完毕后需要使用delete来释放。我们通过分配在栈中的类对象的RAII机制,来管理分配在堆空间中的内存:

class ImageEx
{
public:
ImageEx()
{
cout << "Execute the constructor!" << endl;
data = new unsigned char[10];
} ~ImageEx()
{
Release();
cout << "Execute the destructor!" << endl;
} private:
unsigned char * data; void Release()
{
delete[] data;
data = nullptr;
}
}; int main()
{
{
ImageEx imageEx;
} return 0;
}

很显然,根据程序栈内存的要求,一旦ImageEx对象离开作用域,就会自动调用析构函数,从而实现了对资源的自动管理。

2.2. 手动管理资源的弊端

远古C/C++需要程序员自己遵循"谁申请,谁释放"的原则细致地管理资源。但实际上,这么做并不是总是能避免内存泄漏的问题。一个很常见的例子如下(这是一个“对图像中的像素进行处理”的函数ImageProcess()):

int doSomething(int* p)
{
return -1;
} bool ImageProcess()
{
int* data = new int[16];
int error = doSomething(data);
if (error)
{
delete data;
data = nullptr;
return false;
} delete data;
data = nullptr;
return true;
}

为了避免内存泄漏,我们必须在这个函数中任何可能出错并返回之前的地方进行释放内存的操作。这样做无疑是低效的。而通过RAII技术改写如下:

int doSomething(ImageEx& imageEx)
{
return -1;
} bool ImageProcess()
{
ImageEx imageEx;
if (doSomething(imageEx))
{
return false;
} return true;
}

这时我们可以完全不用关心动态内存资源释放的问题,因为类对象在超出作用域之后,就调用析构函数自动把申请的动态内存释放掉了。无论从代码量还是编程效率来说,都得到了巨大的提高。

2.3. 间接使用

可以确定地是,无论使用何种的释放内存资源的操作(delete、析构函数以及普通释放资源的函数),都会给程序员带来心智负担,最好不要手动进行释放内存资源的操作,如果能交给程序自动管理就好了。对此,现代C++给出地解决方案就是RAII。

在现代C++中,动态内存推荐使用智能指针类型(shared_ptr、unique_ptr、weak_ptr)来管理动态内存对象。智能指针采用了reference count(引用计数)的RAII,对其指向的内存资源做动态管理,当reference count为0时,就会自动释放其指向的内存对象。

而对于动态数组,现代C++更推荐使用stl容器尤其是std::vector容器。std::vector容器是一个模板类,也是基于RAII实现的,其申请的内存资源同样也会在超出作用域后自动析构。

因此,使用智能指针和stl容器,也就是间接的使用了RAII,是我们可以不用再关心释放资源的问题。

2.4. 自下而上的抽象

当然,实际的情况可能并不会那么好。在程序的底层可能仍然有一些资源需要管理,或者需要接入第三方的库(尤其是C库),他们依然是手动管理内存,而且可能我们用不了智能指针或者stl容器。但是我们仍然可以使用RAII,逐级向上抽象封装,例如:

class ImageEx
{
public:
ImageEx()
{
cout << "Execute the constructor!" << endl;
data = new unsigned char[10];
} ~ImageEx()
{
Release();
cout << "Execute the destructor!" << endl;
} private:
unsigned char* data; void Release()
{
delete[] data;
data = nullptr;
}
}; class Texture
{
public:
Texture() = default; private:
ImageEx imageEx;
}; int main()
{
{
Texture texture;
} return 0;
}

可以认为ImageEx是底层类,需要进行动态内存管理而无法使用std::vector,那么我们对其采用RAII进行管理;Texture是高级类,内部有ImageEx数据成员。此时我们可以发现,Texture类已经无需再进行显示析构了,Texture在离开作用域时会自动销毁ImageEx数据成员,调用其析构函数。也就是说,Texture对象已经彻底无需关心内存资源释放的问题。

那么可以得出一个结论:对于底层无法使用智能指针或者stl容器自动管理资源的情况,最多只要一层的底层类采用RAII设计,那么其高层次的类就无需再进行显示析构管理了。这样一个完美无瑕的世界就出现了:程序员确实自己管理了资源,但无需任何代价,或者只付出了微小的代价(实在需要手动管理资源时采用RAII机制),使得这个管理是自动化的。程序员可以像有GC(垃圾回收)机制的编程语言那样,任意的申请资源而无需关心资源释放的问题。

3. 总结

无论对于哪一门编程语言来说,资源管理都是个很严肃的话题。对于资源管理,现代C++给出的答案就是RAII。通过该技术,减少了内存泄漏的可能行,以及手动管理资源的心智负担。同时自动化管理资源,也保障了性能需求。当然,这也是C++"零成本抽象(zero overhead abstraction)"的设计哲学的体现。

4. 参考

  1. C++中的RAII介绍
  2. RAII:如何编写没有内存泄漏的代码 with C++

上一篇

目录

下一篇

面向对象编程(C++篇4)——RAII的更多相关文章

  1. Python 第六篇(中):面向对象编程中级篇

    面向对象编程中级篇: 编程思想概述: 面向过程:根据业务逻辑从上到下写垒代码  #最low,淘汰 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 #混口饭吃 def add(ho ...

  2. 面向对象编程(C++篇2)——构造

    目录 1. 引述 2. 详述 2.1. 数据类型初始化 2.2. 类初始化 1. 引述 在C++中,学习类的第一课往往就是构造函数.根据构造函数的定义,构造函数式是用于初始化类对象的数据成员的.无论何 ...

  3. 面向对象编程(C++篇1)——引言

    目录 1. 概述 2. 详论 2.1. 类与对象 2.2. 数据类型 3. 目录 1. 概述 现代C++与最原始的版本已经差不多是两种不同的语言了.不断发展的C++标准给C++这门语言带来了更多的范式 ...

  4. 面向对象编程(C++篇3)——析构

    目录 1. 概述 2. 详论 2.1. 对象生命周期 2.2. 不一定需要显式析构 2.3. 析构的必要性 3. 总结 1. 概述 类的析构函数执行与构造函数相反的操作,当对象结束其生命周期,程序就会 ...

  5. Python 第六篇(上):面向对象编程初级篇

    面向:过程.函数.对象: 面向过程:根据业务逻辑从上到下写垒代码! 面向过程的编程弊:每次调用的时候都的重写,代码特别长,代码重用性没有,每次增加新功能所有的代码都的修改!那有什么办法解决上面出现的弊 ...

  6. python - 面向对象编程(初级篇)

    写了这么多python 代码,也常用的类和对象,这里准备系统的对python的面向对象编程做以下介绍. 面向对象编程(Object Oriented Programming,OOP,面向对象程序设计) ...

  7. 面向对象编程-终结篇 es6新增语法

    各位,各位,终于把js完成了一个段落了,这次的章节一过我还没确定下面要学的内容可能是vue也可能是前后端交互,但无论是哪个都挺兴奋的,因为面临着终于可以做点看得过去的大点的案例项目了,先憋住激动地情绪 ...

  8. 洗礼灵魂,修炼python(41)--巩固篇—从游戏《绝地求生-大逃杀》中回顾面向对象编程

    声明:本篇文章仅仅以游戏<绝地求生>作为一个参考话题来介绍面向对象编程,只是作为学术引用,其制作的非常简易的程序也不会作为商业用途,与蓝洞公司无关. <绝地求生>最近很火,笼络 ...

  9. Python(三)基础篇之「模块&面向对象编程」

    [笔记]Python(三)基础篇之「模块&面向对象编程」 2016-12-07 ZOE    编程之魅  Python Notes: ★ 如果你是第一次阅读,推荐先浏览:[重要公告]文章更新. ...

随机推荐

  1. JVM基础学习(一):JVM内存模型

    在Java进阶知识的学习中,JVM都是避不过去的一关,我个人对于JVM的理解其实就是相当于在操作系统的外层再加了一层中间层,从来屏蔽了具体硬件之间的不同实现,使得Java实现了最重要的特性:一次编译, ...

  2. JUC之Java中的阻塞队列及其实现原理

    在文章线程池实现原理 - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中介绍了线程池的组成部分,其中一个组成部分就是阻塞队列.那么JAVA中的阻塞队列如何实现的呢? 阻塞队列,关键字是阻塞 ...

  3. Django数据库与模块models(4)

    上一节做到把一个应用加入到项目中,现在再往里面加一个数据库就可以与数据库进行交互了. Django默认有一个轻量级的数据库叫SQLite,当我们要更换其他的数据库时,则需要绑定数据库,如何绑定?首先打 ...

  4. pytest--conftest.py用法(单独存放fixtrue配置的一个文件)

    1.conftest.py文件是什么 单独存放fixtrue配置的一个文件 用处是可以多个不同的pytest测试脚本共用一套fixture配置,代码维护方便 2.conftest.py使用注意事项 c ...

  5. 通俗理解.NET 6 Minimal APIs

    .NET 6,微软称为"最快的.NET",带有了许多令人兴奋的新功能.语言和性能改进.这是自 .NET Core 3.1 以来的第一个 LTS 版本,将支持三年. 本次大版本发布, ...

  6. 记一次阿里云oss文件上传服务假死

    引言 记得以前刚开始学习web项目的时候,经常涉及到需要上传图片啥的,那时候都是把图片上传到当前项目文件夹下面,每次项目一重启图片就丢了.虽然可以通过修改/tomcat/conf/server.xml ...

  7. 安装CentOS7出现dracut:/#……time解决办法

    当选择install CentOS7以后一会就会出现错误.报错信息:就是dracut:/# ... timeout一大堆.我本来以为是我的启动盘没做好,后来我又重做了好几次都是这问题. 解决 通过搜索 ...

  8. C# typeof() 和object.GetType() 、Type..GetType()使用和区别

    进行学习到表达树了,用动Tpye了.所以整理了以下他们区别和用法 总得来说他们都是为了获取某个实例具体引用的数据类型System.Type.1.GetType()方法继承自Object,所以C#中任何 ...

  9. Scrapy(六):Spider

    总结自:Spiders - Scrapy 2.5.0 documentation Spider 1.综述 ①在回调函数Parse及其他自写的回调函数中,必须返回Item对象.Request对象.或前两 ...

  10. ansible二进制部署kubernetes集群

    kubernetes版本1.21.5 需要的资源文件请自行到我的阿里云盘下载 https://www.aliyundrive.com/s/zVegF78ATDV 修改主机信息 #根据自己的主机信息自行 ...