都说 C++ 没有 GC,RAII: 那么我算个啥?

c++,没有,gc,raii,那么 · 浏览次数 : 155

小编点评

**C++ 中的资源回收** C++ 中没有自动垃圾回收器,这意味着当内存分配结束后,无法自动释放它们。这意味着当在循环中频繁使用变量或对象时,可能导致内存泄漏。 **一般 C++ 程序是如何回收资源的?** 一般 C++ 程序在内存分配后,会将申请的内存释放给系统。这可以通过以下几种方式实现: * **局部变量:**局部变量在函数退出时被自动释放。 * **堆:**类对象在构造函数中申请内存,在析构函数中释放内存。 * **智能指针:**智能指针可以自动管理和释放资源。 **RAII (资源获取是初始化) 的概念** RAII 是指在对象创建之前自动执行资源释放的操作。这可以帮助避免内存泄漏并提高代码可读性。 **C++ 11 中的智能指针** C++ 11 引入智能指针,它们可以自动管理和释放资源。使用智能指针可以简化代码并避免内存泄漏。 **实例** 以下是一个使用智能指针进行资源管理的示例: ```cpp #include #include using namespace std; class Helper { private: int* data; public: Helper() { data = new int; // 在堆上申请内存 } ~Helper() { delete data; // 释放堆上申请的内存 } void do_something_with_data() {} }; int main() { // 实例化智能指针对象,输入需要被管理的内存首地址 unique_ptr data(new int); cout << "data=" << *data << endl; // 使用智能指针调用成员函数 data->do_something_with_data(); // 释放资源 data.release(); return 0; } ``` **注意** 在实例化智能指针对象时,必须传入内存地址。

正文

*以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/7A9-tGZxf4w_7eZl3OUQ4A

学过 Java、C# 或者其他托管语言(managed languages)的同学,回过头来看 C++ 的时候,第一反应就是 C++ 没有自动垃圾回收器(GC),而不能充分利用的资源被称为垃圾。

那么 C++ 真的不能自动回收垃圾吗?带着这个疑问我们来看看一般 C++ 程序都是怎样回收资源的。

内存在计算机系统中是有限的资源,通常申请内存和释放内存是这样子的,假设有个被调用的函数 function():

void function()
{
    int *p = new int; // 申请内存

    // 资源申请下来了,不玩有个 p 用?
    // do something

    delete p; // 释放内存
}

这段示例代码在 function() 函数开始的时候申请了一块内存,大小对应于 int 类型,然后在函数结束的时候释放它。通常来说,这看起来很OK,没毛病,但是,如果遇到了下面几种情况呢?

  • 程序如果中途有逻辑让它提前退出 function() 函数
  • 发生了异常而没有被捕获到

那么在函数尾部执行释放内存的动作有几率不会被执行,意味着发生也会内存泄漏。像上面这段代码,如果调用的次数不多也不碍事,不过,如果循环调用 function(),这时泄露的内存资源会不断累积,而且一直被浪费掉,期间系统无法再次使用这些被浪费的内存,直到进程被终止,严重的话,会导致系统资源被耗尽,跑着跑着系统都崩溃了。这种 bug 在 C 范式的编程语言中真的很常见。

RAII 是什么

众所周知 C++ 具有面向对象的特性,在初始化类对象的时候,系统会调用类构造函数。如果类对象是存放在栈空间的话,比如声明为局部变量,那么当类对象超出生命周期时,比如退出局部变量的作用域,系统会调用这个对象的类析构函数;如果类对象是存放在堆空间的话,比如通过 new 操作符创建的类对象,那么当类对象被销毁时,比如对对象执行 delete 操作,系统同样会调用类析构函数。

C++ 的这个特性可以用来解决上面提到的资源泄露问题,怎么利用呢?

modern C++ 实践建议优先把资源存放在栈上。如果只是个变量类型,完全可以用局部变量的形式定义声明,这样代码块在退出后系统自动回收栈上的资源。

对上面的函数 function() 修改

void function()
{
    // 声明定义为局部变量,资源存储在栈区
    int data = 0;

    // do something with data

    // 函数退出时,自动释放 data 占用的空间
}

当资源比较占空间时,需要在堆上分配资源,可以通过指针引用它,资源的申请放在类的构造函数里,然后在析构函数里释放。下面举个例子

class Helper
{
private:
    int* data;
public:
    Helper() {
        data = new int; // 在堆上申请内存
    }
    ~Helper() {
        delete data; // 释放堆上申请的内存
    }
    void do_something_with_data() {}
};

void function()
{
    // 声明定义为局部变量,对象存储在栈区
    // 调用 Helper 类构造函数在堆上申请资源
    Helper help;

    // 通过对象 help 调用成员 data
    // 如果 data 是 Helper 私有成员
    // 在类外面必须通过类成员方法调用 data
    help.do_something_with_data();

    // 函数退出时,自动释放 help 对象占用的栈空间
    // 就算发生了异常或者中途退出都会执行这一步
    // help 对象被销毁时,调用 Helper 类析构函数
    // Helper 类析构函数释放已申请的堆上资源
}

利用这种特性的行为被 C++ 发明人称呼为 RAII,英文全称是「resource acquisition is initialization」,中文翻译过来是「资源获取即是初始化」。而我喜欢把它叫做上下文管理,实现资源申请释放的类叫做上下文管理器(context manager)。

经典实践--智能指针

上面的示例代码写起来略显啰嗦,为了推广这种设计核心思路和简化代码编写,在 C++ 11 之后标准库里添加了 unique_ptr。

unique_ptr 属于 Smart Points 中的一种,Smart Points 在国内通常翻译为「智能指针」。智能指针负责管理和释放资源。上面的 function() 函数可以改成这样子

#include <memory>
void function()
{
    // 实例化智能指针对象,输入需要被管理的内存首地址
    // 对象为局部变量,存储在栈区
    std::unique_ptr<int> data(new int);

    // 智能指针对象就像普通指针一样调用
    printf("data=%d\n", *data);

    // 函数退出时,自动释放 data 对象占用的栈空间
    // 就算发生了异常或者中途退出都会执行这一步
    // data 对象被销毁时,同步释放被管理的内存资源
}

可见,用了智能指针后,不需要像之前那样定义类 Helper (上下文管理器)了,代码清爽很多。

不过,上面的示例代码中有个地方需要注意,在实例化智能指针对象时必须传入内存地址,有没有其它更好的方式设置被管理的内存地址?

有的,C++ 14 之后标准库添加了 make_unique,演示一下怎么用

std::unique_ptr<int> data = std::make_unique<int>();

与都说 C++ 没有 GC,RAII: 那么我算个啥?相似的内容:

都说 C++ 没有 GC,RAII: 那么我算个啥?

学过 Java、C# 或者其他托管语言(managed languages)的同学,回过头来看 C++ 的时候,第一反应就是 C++ 没有自动垃圾回收器(GC),而不能充分利用的资源被称为垃圾。

记一次 .NET某报关系统 非托管泄露分析

## 一:背景 ### 1. 讲故事 前段时间有位朋友找到我,说他的程序内存会出现暴涨,让我看下是怎么事情?而且还告诉我是在 Linux 环境下,说实话在Linux上分析.NET程序难度会很大,难度大的原因在于Linux上的各种开源工具主要是针对 C/C++, 和 .NET 一毛钱关系都没有,说到底

记一次 .NET 某自动化采集软件 崩溃分析

一:背景 1.讲故事 前段时间有位朋友找到我,说他的程序在客户的机器上跑着跑着会出现偶发卡死,然后就崩掉了,但在本地怎么也没复现,dump也抓到了,让我帮忙看下到底怎么回事,其实崩溃类的dump也有简单的,也有非常复杂的,因为大多情况下都是非托管层面出现的各种故障,非常考验对 C, C++, Win

树状数组

都说树状数组思路很难,那我们今天就给他讲个透彻! 前置知识:`lowbit` 运算 `lowbit` 的作用就是返回一个数从右往左数的第一个1与他前面所有的0所组成的十进制数 举个例子: $114$这个数转换为二进制为$1110010$,而它从右往左数的第一个$1$在第二位,将这位右边的所有$0$放

都说DevOps落地难,到底难在哪里?也许你还没找到套路

当你打开这篇文章的时候,也许你也在为DevOps的落地而苦恼,也许你的组织正在尝试DevOps转型,作为一线的实践者,说说我对这个“落地难”的看法,欢迎交流不同看法~ DevOps是实践摸索出来的,别人的终究是别人的 如下图所示,你可能在不同企业研发效能的分享都看到过,各种关于DevOps的书上有会

vue项目中发布新版本线上自动清缓存

背景 最近项目更新频繁,每次一更新客户都说还跟之前的一样。一查原因是因为客户没有清空浏览器的缓存。所以为了方便客户看到最新版本,开始调研再发布新版本后自动清理缓存。 方案 每次打包后的js和css都加上hash值后缀。当文件发生改变时,hash值也改变。这样就不会走缓存 举个例子 vue.confi

[转帖]redis惰性删除 lazy free 源码剖析,干货满满

目录 前言 数据删除场景 lazy free 概念 配置 源码剖析(版本 6.2.6) 场景一:客户端执行的显示删除/清除命令 场景二:某些指令带有的隐式删除命令 场景三:删除过期数据 场景四:内存淘汰数据删除 场景五:主从同步清空从库 小结 前言 都说 redis 是单线程的,其实并不是说 red

Windows 磁盘部分性能数据获取

Windows 磁盘部分性能数据获取 摘要 每次晚上加班总有收获 这次发现了一个fio for windows版本的压测程序, 准备学习和使用一下. https://github.com/axboe/fio/releases 安装 exe 的正常安装不用都说直接使用就可以了. 注意我这变使用的是 x

9.前端初步设计

空了挺长时间没写了。一些琐事耽误,然后另一方面就是在写前端。因为我不是学前端出身,所以前端相对比较弱,一下子我也搞的不是很全面,最主要的是没法讲的很细致,前端这东西吧,都说简单,但是想做的很好看那还是需要一些深入学习的。上一篇的文章是邮箱发送,意在做注册的时候发送验证码。现在页面基本完成了,毕竟登录

千呼万唤始出来 JDK 21 LTS, 久等了

平地起惊雷!!! 目录英雄的迟暮大人时代变了JDK 21 LTS 前 JAVA并发编程模型JDK 21 LTS 中的 JAVA 并发编程模型虚拟线程 VS 线程池The Last 你可以称呼它为:JDK 8 之后的神,它也是很多人认为的 JDK 8 之后,最值得升级的版本。 以前大家都说: 他发任他