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. 初识面向对象(Day17-Day18)

    人狗大战的游戏 你现在是一家游戏公司的开发人员,现在需要你开发一款叫做<人狗大战>的游戏,你就思考呀,人狗作战,那至少需要2个角色,一个是人, 一个是狗,且人和狗都有不同的技能,比如人拿棍 ...

  2. 疑难杂症:运用 transform 导致文本模糊的现象探究

    在我们的页面中,经常会出现这样的问题,一块区域内的文本或者边框,在展示的时候,变得特别的模糊,如下(数据经过脱敏处理): 正常而言,应该是这样的: emmm,可能大图不是很明显,我们取一细节对比,就非 ...

  3. 定制Centos7.9镜像

    Ps:因为工作内容:有一部份是需要重装系统:系统版本镜像为centos7.9.可每次装完都需要下载一些基础包:最近因为设备过多:网卡名称太乱:导致做后续配置太繁琐:不规整:索性自己定制个系统: 搭建基 ...

  4. Python中类的多层继承和多重继承

  5. KALI搭建Docker+Vulhub漏洞复现环境

    在学习网络安全的过程中,少不了的就是做漏洞复现,而漏洞复现一般比较常用的方式就是使用docker-vulhub进行环境搭建,我近期也遇到了这个问题,但是网上的教程特别混乱,根本起不到帮助作用,即使有可 ...

  6. PentestBox在win10里打不开工具 显示无系统命令的解决方法

    PentestBox详细安装过程:http://www.cnblogs.com/ESHLkangi/p/8336398.html 在使用PentestBox的时候出现了打不开工具的问题,最后看到一个大 ...

  7. readonly 只读字段的初始化值确定|static 字段的初始值确定

    类的初始化顺序 如下: 第一次实例化Son============================ C#编译器缺省将每一个成员变量初始化为他的默认值Son静态字段Son静态构造函数Son字段Fathe ...

  8. win7下安装Hadoop

    1 下载准备 下载hadoop,官网用一个快一点的镜像,使用迅雷加速下载,二进制格式,解压目录:E:\hadoop\hadoop-2.9.2 下载winutils,这个是别人编译好的hadoop的wi ...

  9. 【用户状态】详细解读Oracle用户ACCOUNT_STATUS的九种状态

    转至:http://blog.itpub.net/519536/viewspace-672276/ DBA_USERS视图中ACCOUNT_STATUS记录的用户的当前状态,一般情况下在使用的正常用户 ...

  10. WPS:为什么无法页眉页脚同前节(同前节是灰的)

    问题:,同前节是灰的 原因:全文只有一节,插入分节符就可以设置同前节了