文章首发

【重学C++】01| C++ 如何进行内存资源管理?

前言

大家好,我是只讲技术干货的会玩code,今天是【重学C++】的第一讲,我们来学习下C++的内存管理。

与java、golang等自带垃圾回收机制的语言不同,C++并不会自动回收内存。我们必须手动管理堆上内存分配和释放,这往往会导致内存泄漏和内存溢出等问题。而且,这些问题可能不会立即出现,而是运行一段时间后,才会暴露出现,排查也很困难。因此,了解和掌握C++中的内存管理技巧和工具是非常重要的,可以提高程序性能、减少错误和增加安全性。

内存分区

在C++中,将操作系统分配给程序的内存空间按照用途划分了代码段、数据段、栈、堆几个不同的区域,每个区域都有其独特的内存管理机制。

代码区

代码区是用于存储程序代码的区域,代码段在程序真正执行前就被加载到内存中,在程序执行期间,代码区内存不会被修改和释放。

由于代码区是只读的,所以会被多个进程共享。在多个进程同时执行同一个程序时,操作系统只需要将代码段加载到内存中一次,然后让多个进程共享这个内存区域即可。

数据段

数据段用于存储静态全局变量、静态局部变量和静态常量等静态数据。在程序运行期间,数据段的大小固定不变,但其内容可以被修改。按照变量是否被初始化。数据段可分为已初始化数据段和未初始化数据段。

C++中函数调用以及函数内的局部变量的使用,都是通过栈这个内存分区实现的。栈分区由操作系统自动分配和释放,是一种"后进先出"的一种内存分区。每个栈的大小是固定的,一般只有几MB,所以如果栈变量太大,或者函数调用嵌套太深,容易发生栈溢出(stack overflow)。

先来一段示例代码,看看C++是如何使用栈进行使用栈来进行函数调用的。

#include <iostream>

void inner(int a) {
std::cout << a << std::endl;
}
void outer(int n) {
int a = n + 1;
inner(a);
} int main() {
outer(4);
}

上面这段代码运行过程中的栈变化如下图

每当程序调用一个函数时,该函数的参数、局部变量和返回地址等信息会被压入栈中。当函数执行完毕,再将这些信息从栈中弹出。根据之前压入的外层调用者压入栈的返回地址,返回到外层调用者未执行的代码继续执行。

本地变量是直接存储在栈上的,当函数执行完成后,这些变量占用的内存就会被释放掉了。前面例子中的本地变量是简单类型,在C++中称为POD类型。对于带有构造和析构函数的非POD类型变量,栈上的内存分配同样有效。编译器会在合适的时机,插入对构造函数和析构函数的调用。

这里有个问题,当函数执行发生异常时,析构函数还会被调用吗?

答案是会的,C++对于发生异常时对析构函数的调用称为"栈展开"。通过下面这段代码演示栈展开。

#include <iostream>
#include <string> class Obj {
public:
std::string name_;
Obj(const std::string& name):name_(name){std::cout << "Obj() " << name_ << std::endl;};
~Obj() {std::cout << "~Obj() " << name_ << std::endl;};
}; void bar() {
auto o = Obj{"bar"};
throw "bar exception";
} int main() {
try {
bar();
} catch (const char* e) {
std::cout << "catch Exception: " << e << std::endl;
}
}

执行代码的结果是:

Obj() bar
~Obj() bar
catch Exception: bar exception

可以发现,发生异常时,bar函数中的本地变量o还是能被正常析构。

栈展开的过程实际上是异常发生时,匹配catch子句的过程。

  1. 程序抛出异常,停止当前执行的调用链,开始寻找与异常匹配的catch子句。
  2. 如果异常发生在try中,则会首先检查与该try块匹配的catch子句。如果异常所在函数体没有try捕获异常。则会直接进入下一步。
  3. 如果第二步未找到匹配的catch,则会在外层的try块中查找,直到找到为止。
  4. 如果到了最外层还没有找到匹配的catch,也就是说异常得不到处理,程序会调用标准库函数terminate终止函数的执行。

在这期间,栈上所有的对象都会被自动析构。

堆是C++中用来存储动态分配内存的内存分区,堆内存的分配和释放需要手动管理,可以通过new/delete或malloc/free等函数进行分配和释放。堆内存的大小通常是不固定的,当我们需要动态分配内存时,就可以使用堆内存。

堆内存由程序员手动分配和释放,因此使用堆内存需要注意内存泄漏和内存溢出等问题。当程序员忘记释放已分配的内存时,会导致内存泄漏问题。而当申请的堆内存超过了操作系统所分配给进程的内存限制时,会导致内存溢出问题。

C++程序绝大多数的内存泄露,都是由于忘记调用delete/free来释放堆上的资源。

还是上代码

#include <iostream>
#include <string> class Obj {
public:
std::string name_;
Obj(const std::string& name):name_(name){std::cout << "Obj() " << name_ << std::endl;};
~Obj() {std::cout << "~Obj() " << name_ << std::endl;};
}; Obj* makeObj() {
Obj* obj = nullptr;
try {
obj = new Obj{"makeObj"};
...
} catch(...) {
delete obj;
throw;
}
return obj;
} Obj* foo() {
Obj* obj = nullptr;
try {
obj = makeObj();
...
} catch(...) {
delete obj;
}
return obj;
}
int main() {
Obj* obj = foo();
...
delete obj;
}

可以看到,由makeObj函数创建的堆变量obj, 在每个获取该变量的上层调用中,都需要关心对该变量的处理。这无疑极大得增加了开发者的心智负担。

RAII

想在堆上创建对象,又不想处理这么复杂的内存释放操作。C++没有像java、golang其他语言创建一套垃圾回收机制,而是采用了一种特有的资源管理方式 --- RAII(Resource Acquisition Is Initialization,资源获取即初始化)。

RAII利用栈对象在作用域结束后会自动调用析构函数的特点,通过创建栈对象来管理资源。在栈对象构造函数中获取资源,在栈对象析构函数中负责释放资源,以此保证资源的获取和释放。

下面给出一个通过RAII来自动释放堆内存的例子

#include <iostream>

class AutoIntPtr {
public:
AutoIntPtr(int* p = nullptr) : ptr(p) {}
~AutoIntPtr() { delete ptr; } int& operator*() const { return *ptr; }
int* operator->() const { return ptr; } private:
int* ptr;
}; void foo() {
AutoIntPtr p(new int(5));
std::cout << *p << std::endl; // 5
} int main() {
foo();
}

上面例子中,AutoIntPtr类封装了一个动态分配的int类型的指针,它的构造函数用于获取资源(ptr = p),析构函数用于释放资源(delete ptr)。当AutoIntPtr超出作用域时,自动调用析构函数来释放所包含的资源。

基于RAII,C++11引入了std::unique_ptrstd::shared_ptr等智能指针用于内存管理类,使得内存管理变得更加方便和安全。这些内存管理类可以自动进行内存释放,避免了手动释放内存的繁琐工作。值得一提的是,上面的AutoIntPtr就是一个简化版的智能指针了。

在实际开发中,RAII的应用很广。不仅仅用于自动释放内存。还可以用来关闭文件、释放数据库连接、释放同步锁等。

总结

本文介绍了C++中的内存管理机制,包括内存分区、栈、堆和RAII技术等内容。通过学习本文,我们可以更好地掌握C++的内存管理技巧,避免内存泄漏和内存溢出等问题。

【重学C++】01| C++ 如何进行内存资源管理?的更多相关文章

  1. 重学C语言---01概述

    1.什么是C语言 C语言是一种计算机程序设计语言,它既具有高级语言的特点,又具有汇编语言的特点.计算机语言是从第二次世界大战以后,经历了戏剧性的发展过程.从机器语言到汇编语言和高级语言.C语言是与硬件 ...

  2. 重学Java(一):与《Java编程思想》的不解之缘

    说起来非常惭愧,我在 2008 年的时候就接触了 Java,但一直到现在(2018 年 10 月 10 日),基础知识依然非常薄弱.用一句话自嘲就是:十年 IT 老兵,Java 菜鸡一枚. 于是,我想 ...

  3. 推翻自己和过往,重学自定义View

    http://blog.csdn.net/lfdfhl/article/details/51671038 深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 A ...

  4. 【重学计算机】操作系统D3章:存储管理

    1. 存储管理的基本概念 逻辑地址:用户地址,从零开始编号 一维逻辑地址:(地址) 二维逻辑地址:(段号: 段内地址) 主存储器的复用方式 按分区:主存划分为多个固定/可变分区,一个程序占一个分区 按 ...

  5. 【重学计算机】操作系统D1章:计算机操作系统概述

    1. 计算机软硬件系统 冯诺伊曼结构 以运算单元为核心,控制流由指令流产生 程序和数据存储在主存中 主存是按地址访问,线性编址 指令由操作码和地址码组成 数据以二进制编码 其他:参考<重学计算机 ...

  6. 谷哥的小弟学前端(01)——HTML常用标签(1)

    探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架 ...

  7. 重学js之JavaScript 面向对象的程序设计(创建对象)

    注意: 本文章为 <重学js之JavaScript高级程序设计>系列第五章[JavaScript引用类型]. 关于<重学js之JavaScript高级程序设计>是重新回顾js基 ...

  8. Python重学记录1

    写下这个标题觉得可笑,其实本人2014年就自学过一次python,当时看的是中谷教育的milo老师的视频,也跟着写了一些代码,只是因为当时工作上用不到也就淡忘了.不过说实话当时的水平也很低下,本来也没 ...

  9. 重学 Java 设计模式:实战单例模式

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 5个创建型模式的最后一个 在设计模式中按照不同的处理方式共包含三大类:创建型模式.结 ...

  10. 重学 Java 设计模式:实战享元模式「基于Redis秒杀,提供活动与库存信息查询场景」

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 程序员‍‍的上下文是什么? 很多时候一大部分编程开发的人员都只是关注于功能的实现,只 ...

随机推荐

  1. gerrit部署使用

    前言 gerrit是基于git的工具,code review功能是开发中很方便,此外它可以配合Jenkins进行自动化的编译工作.今天给大家分享一下gerrit在ubuntu的安装使用 作者:良知犹存 ...

  2. NewStarCTF 公开赛 2022 RE WP

    Week 2 Re 前可见古人,后得见来者 chipher = [0x51, 0x5B, 0x4C, 0x56, 0x59, 0x4D, 0x50, 0x56, 0x54, 0x43, 0x7D, 0 ...

  3. fortify Unsafe JNI

    Unsafe JNI 主要解决问题: 1.system.currentTimeMillis(); 使用SystemClock.now()替换. 2.isAssignableFrom(); 使用新定义的 ...

  4. LinkedBlockingQueue出入队实现原理

    类图概述 由类图可以看出,L是单向链表实现的,有两个ReentrantLock实例用来控制元素入队和出队的原子性,takeLock用来控制只有一个线程可以从队头获取元素,putLock控制只有一个线程 ...

  5. 【事故】记一次意外把企业项目放到GitHub并被fork,如何使用DMCA下架政策保障隐私

    前言 缘由 在一个月黑风高的夜晚,正准备休息的我突然接到之前外包老总的亲切问候.一顿输出才知道三年前为了搭建流程化部署,将甲方的测试代码放到github上后忘记删除.现在被甲方的代码扫描机制扫到,并且 ...

  6. 企业实践 | 国产操作系统之光? 银河麒麟KylinOS-V10(SP3)高级服务器操作系统基础安装篇

    [点击 关注「 全栈工程师修炼指南」公众号 ] 设为「️ 星标」带你从基础入门 到 全栈实践 再到 放弃学习! 涉及 网络安全运维.应用开发.物联网IOT.学习路径 .个人感悟 等知识分享. 希望各位 ...

  7. 【牛客小白月赛70】A-F题解【小d和超级泡泡堂】【小d和孤独的区间】【小d的博弈】【小d和送外卖】

    比赛传送门:https://ac.nowcoder.com/acm/contest/53366 难度适中. 作者:Eriktse 简介:19岁,211计算机在读,现役ACM银牌选手力争以通俗易懂的方式 ...

  8. [MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构

    这是一篇系列博文.请关注我,学习更多.NET MAUI开发知识! [MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构 [MAUI 项目实战] 手势控制音乐播放器(二): 手势交互 [MAU ...

  9. systemctl 命令设置开机自启动失败

    哈喽大家好,我是咸鱼.今天跟大家分享一个关于 Linux 服务(service)相关的案例   案例现象 我在 3 月 31日的时候发表了一篇<shell 脚本之一键部署安装 Nginx> ...

  10. Rust中的derive属性详解

    1. Rust中的derive是什么? 在Rust语言中,derive是一个属性,它可以让编译器为一些特性提供基本的实现.这些特性仍然可以手动实现,以获得更复杂的行为. 2. derive的出现解决了 ...