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中栈(虚拟机栈、本地方法栈)是线程私有的内存。其实栈的内存管理很好理解,就是出栈后,随之变量和对象都会被释放,那它是如何释放的呢,我们来看个例子


栈.png

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,373评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,732评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,163评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,700评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,036评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,425评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,737评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,421评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,141评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,398评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,908评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,276评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,907评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,018评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,772评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,448评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,325评论 2 261

推荐阅读更多精彩内容