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. Solution -「AGC 010C」「AT 2304」Cleaning

    \(\mathcal{Description}\)   Link.   给定一棵 \(n\) 个点的无根树,点有点权,每次选择两个不同的叶子,使它们间的简单路径的所有点权 \(-1\),问能否将所有点 ...

  2. Mybatis第三方PageHelper分页插件原理

    ​ 欢迎关注公号:BiggerBoy,看更多文章 原文链接:https://mp.weixin.qq.com/s?__biz=MzUxNTQyOTIxNA==&mid=2247485158&a ...

  3. kubeadm + containerd 部署 k8s-v1.23.3(含证书升级)

    文章目录 前言 环境准备 答应我,所有节点都要关闭防火墙 答应我,所有节点都要关闭selinux 答应我,所有节点都要关闭swap 答应我,所有节点都要开启内核模块 答应我,所有节点都要开启模块自动加 ...

  4. VS Code开发TypeScript

    TypeScript是JaveScript的超集,为JavaScript增加了很多特性,它可以编译成纯JavaScript在浏览器上运行.TypeScript已经成为各种流行框架和前端应用开发的首选. ...

  5. Linux性能优化之内存性能统计信息

    关于内存的概念及其原理在任何一本介绍操作系统的书本中都可以查阅到. 理论放一遍,在Linux操作系统中如何查看系统内存使用情况呢?看看内存统计信息有哪些维度. 一.内存使用量 详细使用方法,man f ...

  6. 学习java知道这五个网站就够了

    "这个国家的每个人都应该学习编程计算机,因为它教你如何思考." 当乔布斯几年前这么说时,他再次被证明是一个真正的有远见的人. 好吧,这很难反驳!如今,编程比以往任何时候都更加蓬勃发 ...

  7. Windows原理深入学习系列-强制完整性控制

    欢迎关注微信公众号:[信安成长计划] 0x00 目录 0x01 介绍 0x02 完整性等级 0x03 文件读取测试 0x04 进程注入测试 0x05 原理分析 Win10_x64_20H2 0x06 ...

  8. 性能测试:k8s集群监控环境搭建(kube-prometheus)

    选择kube-prometheus版本 k8s集群版本是1.22.x 5个节点 说明:如果你电脑配置低,也可以1个master节点,2个node节点 3个节点 Kube-Prometheus地址:ht ...

  9. kali各工具使用介绍

    各工具kali官方简介(竖排):https://tools.kali.org/tools-listing 安装kali虚拟机可参考:https://www.cnblogs.com/lsdb/p/650 ...

  10. 激活visio pro 2019

    内容来源:http://www.yishimei.cn/catalog.asp?page=2 1.必须彻底关闭windows defender 防火墙 :光笔防火墙的教程:https://www.cn ...