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印,欢迎你留言讨论哈。


目录
相关文章
|
29天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
34 6
|
20天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
1月前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
19天前
|
Java Android开发 C++
Java和C++
Java和C++
34 15
|
22天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
35 8
|
20天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
24天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
49 5
|
22天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
22天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
27天前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####