From Java to C++ 之内存管理篇

简介: From Java to C++ 之内存管理篇

前叙


From Java to C++ 第一篇

From Java to C++ 第二篇

From Java to C++ 第三篇

在前面三篇中,从快速入门,再细节到C++实参传递特点,这次我们从最最基础,恰巧也是最最重要的部分,内存管理,为什么说它重要呢?因为在C++中并没有提供像Java一样的完善的垃圾回收机制,就算有也是比较简单的,并不能作为完美的依靠,但恰巧是因为开发可以自己控制内存,来达到更加高效的内存管理,虽然现在这个年代好像说内存并不那么的重要,所以来让Java、Python这种语言火了起来,说白了它们就是用空间换时间,但是作为一个追求完美的内存管理者,我们不光追求更短的时间,也在追求更小的空间,这些就离不开C或者C++,我们都知道Java中的堆栈,其实Java作为C++的后继语言,它其实就是借鉴了C++的做法,但C++中有RALL,是C++所特有的资源管理方式,下面我们先来学习下这三个概念。


在内存管理下,它是函数调用过程中产生的变量,函数参数值、返回变量等的一块内存区域,和栈数据结构类似,遵循后进先出的特点。在Java中栈(虚拟机栈、本地方法栈)是线程私有的内存。其实栈的内存管理很好理解,就是出栈后,随之变量和对象都会被释放,那它是如何释放的呢,我们来看个例子

image.png

简单类型的释放应该很简单,如果是对象的话,它有构造函数等,在释放的时候其实就会调用对应的析构函数,哪怕发生了异常退出,C++内存管理都会执行对象的析构函数来释放。所以你是不是了解了析构函数的作用了呢?


在C++中,和Java一样都是属于动态分配的区域,而且都是靠 new关键字来申请空间,但唯一不同的是,在C++中需要显示的 delete,才可以释放掉,而Java则是通过GC回收。所以C++中,如果你用new来创建对象,那就要和delete成队出现,但C++中还有个问题,一般你不会new完以后直接delete,实际的场景其实是你new完以后,需要很多操作,然后在delete,但这中间有可能发生崩溃,导致程序未能按照以前的想法执行delete操作,所以,这就产生了内存泄漏,内存永远无法释放掉,时间久了就会导致应用内存占满,无法申请新的空间,其实C++给我们提供了智能指针等可以优雅的释放该内存,后续我们专门找个课题研究这个如何更好的回收内存。

image.png

如图,你也看到了当我们new的时候,其实内存管理,它经历了分配内存,其实这块内存在分配和释放时,还会考虑如下场景:

  1. 内存充足,从可用的内存里取出一块合适大小的内存
  2. 内存充足但可用内存中没有合适的大小,这里的情况其实内存管理还会做一个操作就是合并未使用的内存,为什么会是这样呢?请看图你就明白了

比如我要的内存是4,其实这个状态是够用的,但不连续,所以这种情况,就需要内存管理来做整理,其实还好,由于C++有专门的内存碎片管理机制,所以第二种情况也不用你管理什么,我们只关心正确的new和delete就行了。

  1. 内存不足时要从操作系统申请新的内存

RALL


英文是Resource Acquisition Is Initialization,直译是资源获取即初始化,完全不理解,这东西源于C++,其实Java中也有运用,具体怎么用我也不知道,感兴趣的可以研究下。 它的来源:比雅尼·斯特劳斯特鲁普安德鲁·柯尼希在设计C++异常时,为解决资源管理时的异常安全性而使用了它。 RAII要求: 资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。 说了这么多你肯定也跟我一样不懂,再现实一点,RAII 依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理,所以说它管理的东西可多了,栈、堆以及其他资源吧。 在C++中,栈上面是可以创建对象的,但是栈内存一般会很小,且它是一块连续的内存区域,不像堆一样可以使用不连续的内存区域,底层用链表构成,在Window下,栈的大小是2MB,Linux下,默认栈空间大小为8MB,当然也可以修改。所以,如果你将对象都创建的栈上,而不用堆的内存,那肯定是不够的。 所以不管是参数,函数内声明的变量,还是返回值,如果是对象的话,我们大部分是依赖的引用或者指针,而引用的值和指针的值其实是放在堆里的。Java也是一样。 为了能更好的理解RALL,我们先来看个例子

class TestRALL {
public:
    TestRALL() {
        std::cout << "TestRALL done" << std::endl;
    };
    ~TestRALL() {
        std::cout << "~TestRALL done" << std::endl;
    };
    void print() {
        std::cout << 1 << std::endl;
    }
};
TestRALL *createTest() {
    return new TestRALL();
}
void print() {
    auto ta = createTest();
    ta->print();
}
int main() {
    print();
    return 0;
}

执行main后输出如下:

TestRALL done
1

发现,并没有调用析构函数,意味着TestRALL对象一直在。如果我加入这么一行

void print() {
    auto ta = createTest();
    ta->print();
    delete ta;
}

打印

TestRALL done
1
~TestRALL done

其实我这里就是显式的调用了delete,其实平时我们这样用不科学,那我该如何做呢?再来看下面的例子

class TestRALL {
public:
    TestRALL() {
        std::cout << "TestRALL done" << std::endl;
    };
    ~TestRALL() {
        std::cout << "~TestRALL done" << std::endl;
    };
    void print() {
        std::cout << 1 << std::endl;
    }
};
TestRALL *createTest() {
    return new TestRALL();
}
class TRDelete {
public:
    explicit TRDelete(TestRALL *tr = nullptr) : tr_(tr) {}
    ~TRDelete() {
        delete tr_;
    }
    TestRALL *get() const { return tr_; }
private:
    TestRALL *tr_;
};
void print() {
    TRDelete trDelete(createTest());
    trDelete.get()->print();
}
int main() {
    print();
    return 0;
}

首先解释个新东西:

explicit


构造函数被explicit修饰后, 就不能再被隐式调用,什么是隐式调用?请看个例子:

#include <iostream>
using namespace std;
class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0)
        : x(x), y(y) {}
};
void displayPoint(const Point& p) 
{
    cout << "(" << p.x << "," 
         << p.y << ")" << endl;
}
int main()
{
    displayPoint(1);
    Point p = 1;
}

displayPoint就是隐式调用,看着是简化了代码的写法,但为什么会不推荐呢?来自Effective C++,因为如下: 被声明为explicit的构造函数通常比其 non-explicit 兄弟更受欢迎, 因为它们禁止编译器执行非预期 (往往也不被期望) 的类型转换. 除非我有一个好理由允许构造函数被用于隐式类型转换, 否则我会把它声明为explicit. 我鼓励你遵循相同的政策。 回过头来看上面的TRDelete,在它的析构函数中,我们delete TestRALL,在print函数执行完后,我们并没有执行delete trDelete 那它为什么会执行TRDelete的析构函数呢?哈哈其实很简单,因为TRDelete并不是通过new创建的,无需delete,它在函数出栈的时候,自然会调用到TRDelete自己的析构函数,这就是RALL的一个基本用法。其实还有更加智能的用法,以后我们再学习讨论。

简单总结


这次我们对栈、堆、RALL的内存管理特点做了学习和练习,也知道了可以通过RALL对栈和堆的内存统一管理的一个小用法,当然我们学习要循循渐进,一步一个juo印,欢迎你留言讨论哈。


目录
相关文章
|
12天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
106 1
|
1天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
1天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
8 1
WK
|
4天前
|
安全 Java 编译器
C++和Java哪个更好用
C++和Java各具优势,选择取决于项目需求、开发者偏好及目标平台特性。C++性能出色,适合游戏、实时系统等;Java平台独立性强,适合跨平台、安全敏感应用。C++提供硬件访问和灵活编程范式,Java有自动内存管理和丰富库支持。两者各有千秋,需根据具体需求选择。
WK
6 1
|
9天前
|
IDE Java 程序员
C++ 程序员的 Java 指南
一个 C++ 程序员自己总结的 Java 学习中应该注意的点。
16 5
|
7天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 1
|
10天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
15天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
17天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
19天前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
29 3