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. 探秘inter()方法

    最近在阅读<深入理解Jav虚拟机>的运行时常量池章节,看到"java语言并不要求常量池一定只有编译器才能产生...运行期间也可以将新的常量放入常量池,这种特性被开发人员利用得比较 ...

  2. kali linux 安装lanmp遇到的问题

    安装LANMP步骤(需要ROOT权限)2020版本以上的Linux kali需要百度参考修改权限得文章 root@kali:~# wget http://dl.wdlinux.cn/files/lan ...

  3. [Java]Thinking in Java 练习2.14

    题目 在文档中加入各项的HTML列表. 代码 1 // object/Documentation4.java 2 // TIJ4 Chapter Object, Exercise 14, page 9 ...

  4. 财务数据分析工具的选择:Excel还是大数据BI?

    ​财务数据分析一般都采用什么工具?跟财务数据分析的哪些指标有关?要怎样展现财务数据间的紧密关联? 财务报表分析比较复杂,一般来说主要包括以下项目: 1. 趋势:在多个时间段内为财务报表中的关键项目创建 ...

  5. Linux图形界面和命令界面切换

    转至:https://blog.csdn.net/weixin_43683466/article/details/85727723 1.若虚拟机内热键被占用,可通过下图所示修改,移出快捷键ctrl+a ...

  6. 60天shell脚本计划-6/12-渐入佳境

    --作者:飞翔的小胖猪 --创建时间:2021年2月21日 --修改时间:2021年2月25日 说明 每日上传更新一个shell脚本,周期为60天.如有需求的读者可根据自己实际情况选用合适的脚本,也可 ...

  7. Qt:QJsonValue

    0.说明 QJsonValue类用于操作JSON中的各种数据. JSON是用于存储结构化数据的格式,JSON中的数据可以是六种类型: 基本类型 存储类型 bool QJsonValue::Bool d ...

  8. matplotlib.lines.Line2D at 0x328fc10 解决方法

    在plt.plot(Y,X)代码前加一句plt.figure()即可

  9. Qt:QDateTime、QDate、QTime与QDateTimeEdit

    时间日期是经常遇到的数据类型,Qt中的时间日期类如下: QTime:时间类型,只表示时间,如15:23:13: QDate:日期类型,只表示日期,如2017-4-5: QDateTime:日期时间类型 ...

  10. Pycharm:一直connecting to console的解决办法

    方法一: 1.打开Anaconda cmd(也就是Anaconda Prompt,在启动栏Anaconda目录里应该有)2.输入echo %PATH% 获得PATH value3.在PyCharm中, ...